Merge "Fix not callback onTaskMovedToFront when dismissing split" into tm-dev
diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb
index 0ccd951..6b3278f 100644
--- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb
+++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShimPriv_apk.asciipb
@@ -1,6 +1,6 @@
 drops {
   android_build_drop {
-    build_id: "7552332"
+    build_id: "8572644"
     target: "CtsShim"
     source_file: "aosp_arm64/CtsShimPriv.apk"
   }
@@ -8,7 +8,7 @@
   version: ""
   version_group: ""
   git_project: "platform/frameworks/base"
-  git_branch: "sc-dev"
+  git_branch: "tm-dev"
   transform: TRANSFORM_NONE
   transform_options {
   }
diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb
index 7e85c8f..34c9c4d 100644
--- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb
+++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__arm_CtsShim_apk.asciipb
@@ -1,6 +1,6 @@
 drops {
   android_build_drop {
-    build_id: "7552332"
+    build_id: "8572644"
     target: "CtsShim"
     source_file: "aosp_arm64/CtsShim.apk"
   }
@@ -8,7 +8,7 @@
   version: ""
   version_group: ""
   git_project: "platform/frameworks/base"
-  git_branch: "sc-dev"
+  git_branch: "tm-dev"
   transform: TRANSFORM_NONE
   transform_options {
   }
diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb
index 20c2785..6897a02 100644
--- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb
+++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShimPriv_apk.asciipb
@@ -1,6 +1,6 @@
 drops {
   android_build_drop {
-    build_id: "7552332"
+    build_id: "8572644"
     target: "CtsShim"
     source_file: "aosp_x86_64/CtsShimPriv.apk"
   }
@@ -8,7 +8,7 @@
   version: ""
   version_group: ""
   git_project: "platform/frameworks/base"
-  git_branch: "sc-dev"
+  git_branch: "tm-dev"
   transform: TRANSFORM_NONE
   transform_options {
   }
diff --git a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb
index 13e3ae5..6dfa7810 100644
--- a/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb
+++ b/.prebuilt_info/prebuilt_info_packages_CtsShim_apk__x86_CtsShim_apk.asciipb
@@ -1,6 +1,6 @@
 drops {
   android_build_drop {
-    build_id: "7552332"
+    build_id: "8572644"
     target: "CtsShim"
     source_file: "aosp_x86_64/CtsShim.apk"
   }
@@ -8,7 +8,7 @@
   version: ""
   version_group: ""
   git_project: "platform/frameworks/base"
-  git_branch: "sc-dev"
+  git_branch: "tm-dev"
   transform: TRANSFORM_NONE
   transform_options {
   }
diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
index 1b9cf26..7393bcd 100644
--- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java
+++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java
@@ -27,7 +27,7 @@
 import android.annotation.SystemService;
 import android.annotation.TestApi;
 import android.compat.annotation.ChangeId;
-import android.compat.annotation.EnabledAfter;
+import android.compat.annotation.Disabled;
 import android.compat.annotation.EnabledSince;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
@@ -282,15 +282,14 @@
     public static final long ENABLE_USE_EXACT_ALARM = 218533173L;
 
     /**
-     * For apps targeting {@link Build.VERSION_CODES#TIRAMISU} or above, the permission
-     * {@link Manifest.permission#SCHEDULE_EXACT_ALARM} will be denied, unless the user explicitly
-     * allows it from Settings.
+     * The permission {@link Manifest.permission#SCHEDULE_EXACT_ALARM} will be denied, unless the
+     * user explicitly allows it from Settings.
      *
-     * TODO (b/226439802): change to EnabledSince(T) after SDK finalization.
+     * TODO (b/226439802): Either enable it in the next SDK or replace it with a better alternative.
      * @hide
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S_V2)
+    @Disabled
     public static final long SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = 226439802L;
 
     @UnsupportedAppUsage
diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
index c3fc4d1..dd0fe95 100644
--- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
@@ -59,6 +59,13 @@
         public void onUserInteractionStarted(String packageName, @UserIdInt int userId) {
             // No-op by default
         }
+
+        /**
+         * Optional callback to inform the listener to give the app a temporary quota bump.
+         */
+        public void triggerTemporaryQuotaBump(String packageName, @UserIdInt int userId) {
+            // No-op by default
+        }
     }
 
     void onBootPhase(int phase);
@@ -247,6 +254,28 @@
     int getBroadcastResponseFgThresholdState();
 
     /**
+     * Returns the duration within which any broadcasts occurred will be treated as one broadcast
+     * session.
+     */
+    long getBroadcastSessionsDurationMs();
+
+    /**
+     * Returns the duration within which any broadcasts occurred (with a corresponding response
+     * event) will be treated as one broadcast session. This similar to
+     * {@link #getBroadcastSessionsDurationMs()}, except that this duration will be used to group
+     * only broadcasts that have a corresponding response event into sessions.
+     */
+    long getBroadcastSessionsWithResponseDurationMs();
+
+    /**
+     * Returns {@code true} if the response event should be attributed to all the broadcast
+     * sessions that occurred within the broadcast response window and {@code false} if the
+     * response event should be attributed to only the earliest broadcast session within the
+     * broadcast response window.
+     */
+    boolean shouldNoteResponseEventForAllBroadcastSessions();
+
+    /**
      * Return the last known value corresponding to the {@code key} from
      * {@link android.provider.DeviceConfig#NAMESPACE_APP_STANDBY} in AppStandbyController.
      */
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 2ea8592..4aa9e84 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -30,6 +30,8 @@
 import static android.app.AlarmManager.RTC;
 import static android.app.AlarmManager.RTC_WAKEUP;
 import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+import static android.os.PowerExemptionManager.REASON_ALARM_MANAGER_ALARM_CLOCK;
+import static android.os.PowerExemptionManager.REASON_DENIED;
 import static android.os.PowerExemptionManager.REASON_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED;
 import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
 import static android.os.PowerWhitelistManager.REASON_ALARM_MANAGER_WHILE_IDLE;
@@ -329,6 +331,7 @@
     });
 
     BroadcastOptions mOptsWithFgs = BroadcastOptions.makeBasic();
+    BroadcastOptions mOptsWithFgsForAlarmClock = BroadcastOptions.makeBasic();
     BroadcastOptions mOptsWithoutFgs = BroadcastOptions.makeBasic();
     BroadcastOptions mOptsTimeBroadcast = BroadcastOptions.makeBasic();
     ActivityOptions mActivityOptsRestrictBal = ActivityOptions.makeBasic();
@@ -565,9 +568,6 @@
         @VisibleForTesting
         static final String KEY_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED =
                 "kill_on_schedule_exact_alarm_revoked";
-        @VisibleForTesting
-        static final String KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT =
-                "schedule_exact_alarm_denied_by_default";
 
         private static final long DEFAULT_MIN_FUTURITY = 5 * 1000;
         private static final long DEFAULT_MIN_INTERVAL = 60 * 1000;
@@ -612,8 +612,6 @@
 
         private static final boolean DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED = true;
 
-        private static final boolean DEFAULT_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = true;
-
         // Minimum futurity of a new alarm
         public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY;
 
@@ -701,14 +699,6 @@
         public boolean KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED =
                 DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED;
 
-        /**
-         * When this is {@code true}, apps with the change
-         * {@link AlarmManager#SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT} enabled will not get
-         * {@link Manifest.permission#SCHEDULE_EXACT_ALARM} unless the user grants it to them.
-         */
-        public volatile boolean SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT =
-                DEFAULT_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT;
-
         public boolean USE_TARE_POLICY = Settings.Global.DEFAULT_ENABLE_TARE == 1;
 
         private long mLastAllowWhileIdleWhitelistDuration = -1;
@@ -743,9 +733,12 @@
                 mOptsWithFgs.setTemporaryAppAllowlist(ALLOW_WHILE_IDLE_WHITELIST_DURATION,
                         TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
                         REASON_ALARM_MANAGER_WHILE_IDLE, "");
+                mOptsWithFgsForAlarmClock.setTemporaryAppAllowlist(
+                        ALLOW_WHILE_IDLE_WHITELIST_DURATION,
+                        TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
+                        REASON_ALARM_MANAGER_ALARM_CLOCK, "");
                 mOptsWithoutFgs.setTemporaryAppAllowlist(ALLOW_WHILE_IDLE_WHITELIST_DURATION,
-                        TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED,
-                        REASON_ALARM_MANAGER_WHILE_IDLE, "");
+                        TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED, REASON_DENIED, "");
             }
         }
 
@@ -892,15 +885,6 @@
                                     KEY_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED,
                                     DEFAULT_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED);
                             break;
-                        case KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT:
-                            final boolean oldValue = SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT;
-
-                            SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = properties.getBoolean(
-                                    KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT,
-                                    DEFAULT_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT);
-                            handleScheduleExactAlarmDeniedByDefaultChange(oldValue,
-                                    SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT);
-                            break;
                         default:
                             if (name.startsWith(KEY_PREFIX_STANDBY_QUOTA) && !standbyQuotaUpdated) {
                                 // The quotas need to be updated in order, so we can't just rely
@@ -971,15 +955,6 @@
             }
         }
 
-        private void handleScheduleExactAlarmDeniedByDefaultChange(boolean oldValue,
-                boolean newValue) {
-            if (oldValue == newValue) {
-                return;
-            }
-            mHandler.obtainMessage(AlarmHandler.CHECK_EXACT_ALARM_PERMISSION_ON_FEATURE_TOGGLE,
-                    newValue).sendToTarget();
-        }
-
         private void migrateAlarmsToNewStoreLocked() {
             final AlarmStore newStore = LAZY_BATCHING ? new LazyAlarmStore()
                     : new BatchingAlarmStore();
@@ -1156,9 +1131,6 @@
             pw.print(KEY_KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED,
                     KILL_ON_SCHEDULE_EXACT_ALARM_REVOKED);
             pw.println();
-            pw.print(KEY_SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT,
-                    SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT);
-            pw.println();
 
             pw.print(Settings.Global.ENABLE_TARE, USE_TARE_POLICY);
             pw.println();
@@ -1744,6 +1716,7 @@
     public void onStart() {
         mInjector.init();
         mOptsWithFgs.setPendingIntentBackgroundActivityLaunchAllowed(false);
+        mOptsWithFgsForAlarmClock.setPendingIntentBackgroundActivityLaunchAllowed(false);
         mOptsWithoutFgs.setPendingIntentBackgroundActivityLaunchAllowed(false);
         mOptsTimeBroadcast.setPendingIntentBackgroundActivityLaunchAllowed(false);
         mActivityOptsRestrictBal.setPendingIntentBackgroundActivityLaunchAllowed(false);
@@ -2741,7 +2714,12 @@
                 if (isExactAlarmChangeEnabled(callingPackage, callingUserId)) {
                     needsPermission = exact;
                     lowerQuota = !exact;
-                    idleOptions = exact ? mOptsWithFgs.toBundle() : mOptsWithoutFgs.toBundle();
+                    if (exact) {
+                        idleOptions = (alarmClock != null) ? mOptsWithFgsForAlarmClock.toBundle()
+                                : mOptsWithFgs.toBundle();
+                    } else {
+                        idleOptions = mOptsWithoutFgs.toBundle();
+                    }
                 } else {
                     changeDisabled = true;
                     needsPermission = false;
@@ -2928,10 +2906,8 @@
     }
 
     private boolean isScheduleExactAlarmDeniedByDefault(String packageName, int userId) {
-        return mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT
-                && CompatChanges.isChangeEnabled(
-                AlarmManager.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, packageName,
-                UserHandle.of(userId));
+        return CompatChanges.isChangeEnabled(AlarmManager.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT,
+                packageName, UserHandle.of(userId));
     }
 
     @NeverCompile // Avoid size overhead of debugging code.
@@ -4707,7 +4683,6 @@
         public static final int REFRESH_EXACT_ALARM_CANDIDATES = 11;
         public static final int TARE_AFFORDABILITY_CHANGED = 12;
         public static final int CHECK_EXACT_ALARM_PERMISSION_ON_UPDATE = 13;
-        public static final int CHECK_EXACT_ALARM_PERMISSION_ON_FEATURE_TOGGLE = 14;
 
         AlarmHandler() {
             super(Looper.myLooper());
@@ -4827,32 +4802,6 @@
                         removeExactAlarmsOnPermissionRevoked(uid, packageName, /*killUid = */false);
                     }
                     break;
-                case CHECK_EXACT_ALARM_PERMISSION_ON_FEATURE_TOGGLE:
-                    final boolean defaultDenied = (Boolean) msg.obj;
-
-                    final int[] startedUserIds = mActivityManagerInternal.getStartedUserIds();
-                    for (int appId : mExactAlarmCandidates) {
-                        for (int userId : startedUserIds) {
-                            uid = UserHandle.getUid(userId, appId);
-
-                            final AndroidPackage packageForUid =
-                                    mPackageManagerInternal.getPackage(uid);
-                            if (packageForUid == null) {
-                                continue;
-                            }
-                            final String pkg = packageForUid.getPackageName();
-                            if (defaultDenied) {
-                                if (!hasScheduleExactAlarmInternal(pkg, uid)
-                                        && !hasUseExactAlarmInternal(pkg, uid)) {
-                                    removeExactAlarmsOnPermissionRevoked(uid, pkg, true);
-                                }
-                            } else if (hasScheduleExactAlarmInternal(pkg, uid)) {
-                                sendScheduleExactAlarmPermissionStateChangedBroadcast(pkg,
-                                        UserHandle.getUserId(uid));
-                            }
-                        }
-                    }
-                    break;
                 default:
                     // nope, just ignore it
                     break;
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 afe36b5..d5a7f28 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -959,7 +959,7 @@
         for (int i = 0; i < mActiveServices.size(); i++) {
             JobServiceContext jsc = mActiveServices.get(i);
             final JobStatus executing = jsc.getRunningJobLocked();
-            if (executing != null && executing.matches(job.getUid(), job.getJobId())) {
+            if (executing == job) {
                 jsc.cancelExecutingJobLocked(reason, internalReasonCode, debugReason);
                 return true;
             }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
index 428f2cb..0f385ef 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
@@ -17,6 +17,7 @@
 package com.android.server.job.controllers;
 
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 import static com.android.server.job.JobSchedulerService.sSystemClock;
@@ -90,6 +91,14 @@
     @CurrentTimeMillisLong
     private long mLaunchTimeThresholdMs = PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS;
 
+    /**
+     * The additional time we'll add to a launch time estimate before considering it obsolete and
+     * try to get a new estimate. This will help make prefetch jobs more viable in case an estimate
+     * is a few minutes early.
+     */
+    @GuardedBy("mLock")
+    private long mLaunchTimeAllowanceMs = PcConstants.DEFAULT_LAUNCH_TIME_ALLOWANCE_MS;
+
     @SuppressWarnings("FieldCanBeLocal")
     private final EstimatedLaunchTimeChangedListener mEstimatedLaunchTimeChangedListener =
             new EstimatedLaunchTimeChangedListener() {
@@ -204,7 +213,8 @@
     private long getNextEstimatedLaunchTimeLocked(int userId, @NonNull String pkgName,
             @CurrentTimeMillisLong long now) {
         final Long nextEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName);
-        if (nextEstimatedLaunchTime == null || nextEstimatedLaunchTime < now) {
+        if (nextEstimatedLaunchTime == null
+                || nextEstimatedLaunchTime < now - mLaunchTimeAllowanceMs) {
             // Don't query usage stats here because it may have to read from disk.
             mHandler.obtainMessage(MSG_RETRIEVE_ESTIMATED_LAUNCH_TIME, userId, 0, pkgName)
                     .sendToTarget();
@@ -335,7 +345,9 @@
         }
 
         final long nextEstimatedLaunchTime = getNextEstimatedLaunchTimeLocked(userId, pkgName, now);
-        if (nextEstimatedLaunchTime - now > mLaunchTimeThresholdMs) {
+        // Avoid setting an alarm for the end of time.
+        if (nextEstimatedLaunchTime != Long.MAX_VALUE
+                && nextEstimatedLaunchTime - now > mLaunchTimeThresholdMs) {
             // Set alarm to be notified when this crosses the threshold.
             final long timeToCrossThresholdMs =
                     nextEstimatedLaunchTime - (now + mLaunchTimeThresholdMs);
@@ -354,7 +366,7 @@
     private boolean willBeLaunchedSoonLocked(int userId, @NonNull String pkgName,
             @CurrentTimeMillisLong long now) {
         return getNextEstimatedLaunchTimeLocked(userId, pkgName, now)
-                <= now + mLaunchTimeThresholdMs;
+                <= now + mLaunchTimeThresholdMs - mLaunchTimeAllowanceMs;
     }
 
     @Override
@@ -494,16 +506,37 @@
         @VisibleForTesting
         static final String KEY_LAUNCH_TIME_THRESHOLD_MS =
                 PC_CONSTANT_PREFIX + "launch_time_threshold_ms";
+        @VisibleForTesting
+        static final String KEY_LAUNCH_TIME_ALLOWANCE_MS =
+                PC_CONSTANT_PREFIX + "launch_time_allowance_ms";
 
         private static final long DEFAULT_LAUNCH_TIME_THRESHOLD_MS = 7 * HOUR_IN_MILLIS;
+        private static final long DEFAULT_LAUNCH_TIME_ALLOWANCE_MS = 20 * MINUTE_IN_MILLIS;
 
         /** How much time each app will have to run jobs within their standby bucket window. */
         public long LAUNCH_TIME_THRESHOLD_MS = DEFAULT_LAUNCH_TIME_THRESHOLD_MS;
 
+        /**
+         * How much additional time to add to an estimated launch time before considering it
+         * unusable.
+         */
+        public long LAUNCH_TIME_ALLOWANCE_MS = DEFAULT_LAUNCH_TIME_ALLOWANCE_MS;
+
         @GuardedBy("mLock")
         public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
                 @NonNull String key) {
             switch (key) {
+                case KEY_LAUNCH_TIME_ALLOWANCE_MS:
+                    LAUNCH_TIME_ALLOWANCE_MS =
+                            properties.getLong(key, DEFAULT_LAUNCH_TIME_ALLOWANCE_MS);
+                    // Limit the allowance to the range [0 minutes, 2 hours].
+                    long newLaunchTimeAllowanceMs = Math.min(2 * HOUR_IN_MILLIS,
+                            Math.max(0, LAUNCH_TIME_ALLOWANCE_MS));
+                    if (mLaunchTimeAllowanceMs != newLaunchTimeAllowanceMs) {
+                        mLaunchTimeAllowanceMs = newLaunchTimeAllowanceMs;
+                        mShouldReevaluateConstraints = true;
+                    }
+                    break;
                 case KEY_LAUNCH_TIME_THRESHOLD_MS:
                     LAUNCH_TIME_THRESHOLD_MS =
                             properties.getLong(key, DEFAULT_LAUNCH_TIME_THRESHOLD_MS);
@@ -528,6 +561,7 @@
             pw.increaseIndent();
 
             pw.print(KEY_LAUNCH_TIME_THRESHOLD_MS, LAUNCH_TIME_THRESHOLD_MS).println();
+            pw.print(KEY_LAUNCH_TIME_ALLOWANCE_MS, LAUNCH_TIME_ALLOWANCE_MS).println();
 
             pw.decreaseIndent();
         }
@@ -536,6 +570,11 @@
     //////////////////////// TESTING HELPERS /////////////////////////////
 
     @VisibleForTesting
+    long getLaunchTimeAllowanceMs() {
+        return mLaunchTimeAllowanceMs;
+    }
+
+    @VisibleForTesting
     long getLaunchTimeThresholdMs() {
         return mLaunchTimeThresholdMs;
     }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index c6ba1ea..bb8d175 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -270,13 +270,12 @@
     private final SparseArrayMap<String, Timer> mEJPkgTimers = new SparseArrayMap<>();
 
     /** List of all regular timing sessions for a package-userId combo, in chronological order. */
-    private final SparseArrayMap<String, List<TimingSession>> mTimingSessions =
-            new SparseArrayMap<>();
+    private final SparseArrayMap<String, List<TimedEvent>> mTimingEvents = new SparseArrayMap<>();
 
     /**
      * List of all expedited job timing sessions for a package-userId combo, in chronological order.
      */
-    private final SparseArrayMap<String, List<TimingSession>> mEJTimingSessions =
+    private final SparseArrayMap<String, List<TimedEvent>> mEJTimingSessions =
             new SparseArrayMap<>();
 
     /**
@@ -519,6 +518,14 @@
 
     private long mEJGracePeriodTopAppMs = QcConstants.DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS;
 
+    private long mQuotaBumpAdditionalDurationMs =
+            QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS;
+    private int mQuotaBumpAdditionalJobCount = QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT;
+    private int mQuotaBumpAdditionalSessionCount =
+            QcConstants.DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT;
+    private long mQuotaBumpWindowSizeMs = QcConstants.DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS;
+    private int mQuotaBumpLimit = QcConstants.DEFAULT_QUOTA_BUMP_LIMIT;
+
     /**
      * List of system apps with the {@link android.Manifest.permission#INSTALL_PACKAGES} permission
      * granted for each user.
@@ -707,7 +714,7 @@
         mTrackedJobs.delete(userId);
         mPkgTimers.delete(userId);
         mEJPkgTimers.delete(userId);
-        mTimingSessions.delete(userId);
+        mTimingEvents.delete(userId);
         mEJTimingSessions.delete(userId);
         mInQuotaAlarmQueue.removeAlarmsForUserId(userId);
         mExecutionStatsCache.delete(userId);
@@ -738,7 +745,7 @@
                 timer.dropEverythingLocked();
             }
         }
-        mTimingSessions.delete(userId, packageName);
+        mTimingEvents.delete(userId, packageName);
         mEJTimingSessions.delete(userId, packageName);
         mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
         mExecutionStatsCache.delete(userId, packageName);
@@ -928,7 +935,7 @@
                 (stats.jobRateLimitExpirationTimeElapsed <= now
                         || stats.jobCountInRateLimitingWindow < mMaxJobCountPerRateLimitingWindow);
         return isUnderAllowedTimeQuota
-                && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]);
+                && stats.bgJobCountInWindow < stats.jobCountLimit;
     }
 
     private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats,
@@ -937,7 +944,7 @@
         final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now
                 || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow);
         return isUnderAllowedTimeQuota
-                && stats.sessionCountInWindow < mMaxBucketSessionCounts[standbyBucket];
+                && stats.sessionCountInWindow < stats.sessionCountLimit;
     }
 
     @VisibleForTesting
@@ -984,12 +991,12 @@
         long remainingMs = limitMs - quota.getTallyLocked();
 
         // Stale sessions may still be factored into tally. Make sure they're removed.
-        List<TimingSession> timingSessions = mEJTimingSessions.get(userId, packageName);
+        List<TimedEvent> timingSessions = mEJTimingSessions.get(userId, packageName);
         final long nowElapsed = sElapsedRealtimeClock.millis();
         final long windowStartTimeElapsed = nowElapsed - mEJLimitWindowSizeMs;
         if (timingSessions != null) {
             while (timingSessions.size() > 0) {
-                TimingSession ts = timingSessions.get(0);
+                TimingSession ts = (TimingSession) timingSessions.get(0);
                 if (ts.endTimeElapsed < windowStartTimeElapsed) {
                     final long duration = ts.endTimeElapsed - ts.startTimeElapsed;
                     remainingMs += duration;
@@ -1042,9 +1049,9 @@
             return 0;
         }
 
-        List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
+        List<TimedEvent> events = mTimingEvents.get(userId, packageName);
         final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
-        if (sessions == null || sessions.size() == 0) {
+        if (events == null || events.size() == 0) {
             // Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can
             // essentially run until they reach the maximum limit.
             if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) {
@@ -1064,7 +1071,7 @@
         // essentially run until they reach the maximum limit.
         if (stats.windowSizeMs == mAllowedTimePerPeriodMs[standbyBucket]) {
             return calculateTimeUntilQuotaConsumedLocked(
-                    sessions, startMaxElapsed, maxExecutionTimeRemainingMs);
+                    events, startMaxElapsed, maxExecutionTimeRemainingMs, false);
         }
 
         // Need to check both max time and period time in case one is less than the other.
@@ -1073,9 +1080,9 @@
         // bucket value.
         return Math.min(
                 calculateTimeUntilQuotaConsumedLocked(
-                        sessions, startMaxElapsed, maxExecutionTimeRemainingMs),
+                        events, startMaxElapsed, maxExecutionTimeRemainingMs, false),
                 calculateTimeUntilQuotaConsumedLocked(
-                        sessions, startWindowElapsed, allowedTimeRemainingMs));
+                        events, startWindowElapsed, allowedTimeRemainingMs, true));
     }
 
     /**
@@ -1084,12 +1091,36 @@
      * @param windowStartElapsed The start of the window, in the elapsed realtime timebase.
      * @param deadSpaceMs        How much time can be allowed to count towards the quota
      */
-    private long calculateTimeUntilQuotaConsumedLocked(@NonNull List<TimingSession> sessions,
-            final long windowStartElapsed, long deadSpaceMs) {
+    private long calculateTimeUntilQuotaConsumedLocked(@NonNull List<TimedEvent> sessions,
+            final long windowStartElapsed, long deadSpaceMs, boolean allowQuotaBumps) {
         long timeUntilQuotaConsumedMs = 0;
         long start = windowStartElapsed;
-        for (int i = 0; i < sessions.size(); ++i) {
-            TimingSession session = sessions.get(i);
+        int numQuotaBumps = 0;
+        final long quotaBumpWindowStartElapsed =
+                sElapsedRealtimeClock.millis() - mQuotaBumpWindowSizeMs;
+        final int numSessions = sessions.size();
+        if (allowQuotaBumps) {
+            for (int i = numSessions - 1; i >= 0; --i) {
+                TimedEvent event = sessions.get(i);
+
+                if (event instanceof QuotaBump) {
+                    if (event.getEndTimeElapsed() >= quotaBumpWindowStartElapsed
+                            && numQuotaBumps++ < mQuotaBumpLimit) {
+                        deadSpaceMs += mQuotaBumpAdditionalDurationMs;
+                    } else {
+                        break;
+                    }
+                }
+            }
+        }
+        for (int i = 0; i < numSessions; ++i) {
+            TimedEvent event = sessions.get(i);
+
+            if (event instanceof QuotaBump) {
+                continue;
+            }
+
+            TimingSession session = (TimingSession) event;
 
             if (session.endTimeElapsed < windowStartElapsed) {
                 // Outside of window. Ignore.
@@ -1129,7 +1160,7 @@
         final long remainingExecutionTimeMs =
                 getRemainingEJExecutionTimeLocked(userId, packageName);
 
-        List<TimingSession> sessions = mEJTimingSessions.get(userId, packageName);
+        List<TimedEvent> sessions = mEJTimingSessions.get(userId, packageName);
         if (sessions == null || sessions.size() == 0) {
             return remainingExecutionTimeMs;
         }
@@ -1146,7 +1177,7 @@
         long phasedOutSessionTimeMs = 0;
 
         for (int i = 0; i < sessions.size(); ++i) {
-            TimingSession session = sessions.get(i);
+            TimingSession session = (TimingSession) sessions.get(i);
             if (session.endTimeElapsed < startWindowElapsed) {
                 // Edge case where a session became stale in the time between the call to
                 // getRemainingEJExecutionTimeLocked and this line.
@@ -1159,7 +1190,7 @@
             } else {
                 // Session fully inside window
                 final long timeBetweenSessions = session.startTimeElapsed
-                        - (i == 0 ? startWindowElapsed : sessions.get(i - 1).endTimeElapsed);
+                        - (i == 0 ? startWindowElapsed : sessions.get(i - 1).getEndTimeElapsed());
                 final long usedDeadSpaceMs = Math.min(remainingDeadSpaceMs, timeBetweenSessions);
                 deadSpaceMs += usedDeadSpaceMs;
                 if (usedDeadSpaceMs == timeBetweenSessions) {
@@ -1267,22 +1298,49 @@
             }
         }
 
-        List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
-        if (sessions == null || sessions.size() == 0) {
+        List<TimedEvent> events = mTimingEvents.get(userId, packageName);
+        if (events == null || events.size() == 0) {
             return;
         }
 
         final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
         final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
         int sessionCountInWindow = 0;
-        // The minimum time between the start time and the beginning of the sessions that were
+        int numQuotaBumps = 0;
+        final long quotaBumpWindowStartElapsed = nowElapsed - mQuotaBumpWindowSizeMs;
+        // The minimum time between the start time and the beginning of the events that were
         // looked at --> how much time the stats will be valid for.
         long emptyTimeMs = Long.MAX_VALUE;
         // Sessions are non-overlapping and in order of occurrence, so iterating backwards will get
         // the most recent ones.
-        final int loopStart = sessions.size() - 1;
+        final int loopStart = events.size() - 1;
+        // Process QuotaBumps first to ensure the limits are properly adjusted.
         for (int i = loopStart; i >= 0; --i) {
-            TimingSession session = sessions.get(i);
+            TimedEvent event = events.get(i);
+
+            if (event.getEndTimeElapsed() < quotaBumpWindowStartElapsed
+                    || numQuotaBumps >= mQuotaBumpLimit) {
+                break;
+            }
+
+            if (event instanceof QuotaBump) {
+                stats.allowedTimePerPeriodMs += mQuotaBumpAdditionalDurationMs;
+                stats.jobCountLimit += mQuotaBumpAdditionalJobCount;
+                stats.sessionCountLimit += mQuotaBumpAdditionalSessionCount;
+                emptyTimeMs = Math.min(emptyTimeMs,
+                        event.getEndTimeElapsed() - quotaBumpWindowStartElapsed);
+                numQuotaBumps++;
+            }
+        }
+        TimingSession lastSeenTimingSession = null;
+        for (int i = loopStart; i >= 0; --i) {
+            TimedEvent event = events.get(i);
+
+            if (event instanceof QuotaBump) {
+                continue;
+            }
+
+            TimingSession session = (TimingSession) event;
 
             // Window management.
             if (startWindowElapsed < session.endTimeElapsed) {
@@ -1309,10 +1367,11 @@
                     final long inQuotaTime = session.endTimeElapsed + stats.windowSizeMs;
                     stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed, inQuotaTime);
                 }
-                if (i == loopStart
-                        || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed)
-                                > mTimingSessionCoalescingDurationMs) {
-                    // Coalesce sessions if they are very close to each other in time
+                // Coalesce sessions if they are very close to each other in time
+                boolean shouldCoalesce = lastSeenTimingSession != null
+                        && lastSeenTimingSession.startTimeElapsed - session.endTimeElapsed
+                        <= mTimingSessionCoalescingDurationMs;
+                if (!shouldCoalesce) {
                     sessionCountInWindow++;
 
                     if (sessionCountInWindow >= stats.sessionCountLimit) {
@@ -1348,6 +1407,8 @@
                 // This session ended before the window. No point in going any further.
                 break;
             }
+
+            lastSeenTimingSession = session;
         }
         stats.expirationTimeElapsed = nowElapsed + emptyTimeMs;
         stats.sessionCountInWindow = sessionCountInWindow;
@@ -1437,9 +1498,9 @@
     private void saveTimingSession(final int userId, @NonNull final String packageName,
             @NonNull final TimingSession session, boolean isExpedited, long debitAdjustment) {
         synchronized (mLock) {
-            final SparseArrayMap<String, List<TimingSession>> sessionMap =
-                    isExpedited ? mEJTimingSessions : mTimingSessions;
-            List<TimingSession> sessions = sessionMap.get(userId, packageName);
+            final SparseArrayMap<String, List<TimedEvent>> sessionMap =
+                    isExpedited ? mEJTimingSessions : mTimingEvents;
+            List<TimedEvent> sessions = sessionMap.get(userId, packageName);
             if (sessions == null) {
                 sessions = new ArrayList<>();
                 sessionMap.add(userId, packageName, sessions);
@@ -1460,6 +1521,9 @@
 
     private void grantRewardForInstantEvent(
             final int userId, @NonNull final String packageName, final long credit) {
+        if (credit == 0) {
+            return;
+        }
         synchronized (mLock) {
             final long nowElapsed = sElapsedRealtimeClock.millis();
             final ShrinkableDebits quota = getEJDebitsLocked(userId, packageName);
@@ -1489,13 +1553,14 @@
         return changed;
     }
 
-    private final class EarliestEndTimeFunctor implements Consumer<List<TimingSession>> {
+    private final class EarliestEndTimeFunctor implements Consumer<List<TimedEvent>> {
         public long earliestEndElapsed = Long.MAX_VALUE;
 
         @Override
-        public void accept(List<TimingSession> sessions) {
-            if (sessions != null && sessions.size() > 0) {
-                earliestEndElapsed = Math.min(earliestEndElapsed, sessions.get(0).endTimeElapsed);
+        public void accept(List<TimedEvent> events) {
+            if (events != null && events.size() > 0) {
+                earliestEndElapsed =
+                        Math.min(earliestEndElapsed, events.get(0).getEndTimeElapsed());
             }
         }
 
@@ -1521,7 +1586,7 @@
             return;
         }
         mEarliestEndTimeFunctor.reset();
-        mTimingSessions.forEach(mEarliestEndTimeFunctor);
+        mTimingEvents.forEach(mEarliestEndTimeFunctor);
         mEJTimingSessions.forEach(mEarliestEndTimeFunctor);
         final long earliestEndElapsed = mEarliestEndTimeFunctor.earliestEndElapsed;
         if (earliestEndElapsed == Long.MAX_VALUE) {
@@ -1809,10 +1874,10 @@
                     inEJQuotaTimeElapsed = (nowElapsed - limitMs) + mEJLimitWindowSizeMs;
                 }
             }
-            List<TimingSession> timingSessions = mEJTimingSessions.get(userId, packageName);
+            List<TimedEvent> timingSessions = mEJTimingSessions.get(userId, packageName);
             if (timingSessions != null) {
                 for (int i = timingSessions.size() - 1; i >= 0; --i) {
-                    TimingSession ts = timingSessions.get(i);
+                    TimingSession ts = (TimingSession) timingSessions.get(i);
                     final long durationMs = ts.endTimeElapsed - ts.startTimeElapsed;
                     sumMs += durationMs;
                     if (sumMs >= limitMs) {
@@ -1881,7 +1946,14 @@
     }
 
     @VisibleForTesting
-    static final class TimingSession {
+    interface TimedEvent {
+        long getEndTimeElapsed();
+
+        void dump(IndentingPrintWriter pw);
+    }
+
+    @VisibleForTesting
+    static final class TimingSession implements TimedEvent {
         // Start timestamp in elapsed realtime timebase.
         public final long startTimeElapsed;
         // End timestamp in elapsed realtime timebase.
@@ -1904,6 +1976,11 @@
         }
 
         @Override
+        public long getEndTimeElapsed() {
+            return endTimeElapsed;
+        }
+
+        @Override
         public String toString() {
             return "TimingSession{" + startTimeElapsed + "->" + endTimeElapsed + ", " + bgJobCount
                     + "}";
@@ -1926,6 +2003,7 @@
             return mHashCode;
         }
 
+        @Override
         public void dump(IndentingPrintWriter pw) {
             pw.print(startTimeElapsed);
             pw.print(" -> ");
@@ -1953,6 +2031,28 @@
     }
 
     @VisibleForTesting
+    static final class QuotaBump implements TimedEvent {
+        // Event timestamp in elapsed realtime timebase.
+        public final long eventTimeElapsed;
+
+        QuotaBump(long eventElapsed) {
+            this.eventTimeElapsed = eventElapsed;
+        }
+
+        @Override
+        public long getEndTimeElapsed() {
+            return eventTimeElapsed;
+        }
+
+        @Override
+        public void dump(IndentingPrintWriter pw) {
+            pw.print("Quota bump @ ");
+            pw.print(eventTimeElapsed);
+            pw.println();
+        }
+    }
+
+    @VisibleForTesting
     static final class ShrinkableDebits {
         /** The amount of quota remaining. Can be negative if limit changes. */
         private long mDebitTally;
@@ -2381,6 +2481,21 @@
                 updateStandbyBucket(userId, packageName, bucketIndex);
             });
         }
+
+        @Override
+        public void triggerTemporaryQuotaBump(String packageName, @UserIdInt int userId) {
+            synchronized (mLock) {
+                List<TimedEvent> events = mTimingEvents.get(userId, packageName);
+                if (events == null || events.size() == 0) {
+                    // If the app hasn't run any jobs, there's no point giving it a quota bump.
+                    return;
+                }
+                events.add(new QuotaBump(sElapsedRealtimeClock.millis()));
+                invalidateAllExecutionStatsLocked(userId, packageName);
+            }
+            // Update jobs out of band.
+            mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
+        }
     }
 
     @VisibleForTesting
@@ -2476,7 +2591,7 @@
         }
     }
 
-    private static final class TimingSessionTooOldPredicate implements Predicate<TimingSession> {
+    private static final class TimedEventTooOldPredicate implements Predicate<TimedEvent> {
         private long mNowElapsed;
 
         private void updateNow() {
@@ -2484,27 +2599,26 @@
         }
 
         @Override
-        public boolean test(TimingSession ts) {
-            return ts.endTimeElapsed <= mNowElapsed - MAX_PERIOD_MS;
+        public boolean test(TimedEvent ts) {
+            return ts.getEndTimeElapsed() <= mNowElapsed - MAX_PERIOD_MS;
         }
     }
 
-    private final TimingSessionTooOldPredicate mTimingSessionTooOld =
-            new TimingSessionTooOldPredicate();
+    private final TimedEventTooOldPredicate mTimedEventTooOld = new TimedEventTooOldPredicate();
 
-    private final Consumer<List<TimingSession>> mDeleteOldSessionsFunctor = sessions -> {
-        if (sessions != null) {
+    private final Consumer<List<TimedEvent>> mDeleteOldEventsFunctor = events -> {
+        if (events != null) {
             // Remove everything older than MAX_PERIOD_MS time ago.
-            sessions.removeIf(mTimingSessionTooOld);
+            events.removeIf(mTimedEventTooOld);
         }
     };
 
     @VisibleForTesting
     void deleteObsoleteSessionsLocked() {
-        mTimingSessionTooOld.updateNow();
+        mTimedEventTooOld.updateNow();
 
         // Regular sessions
-        mTimingSessions.forEach(mDeleteOldSessionsFunctor);
+        mTimingEvents.forEach(mDeleteOldEventsFunctor);
 
         // EJ sessions
         for (int uIdx = 0; uIdx < mEJTimingSessions.numMaps(); ++uIdx) {
@@ -2512,14 +2626,14 @@
             for (int pIdx = 0; pIdx < mEJTimingSessions.numElementsForKey(userId); ++pIdx) {
                 final String packageName = mEJTimingSessions.keyAt(uIdx, pIdx);
                 final ShrinkableDebits debits = getEJDebitsLocked(userId, packageName);
-                final List<TimingSession> sessions = mEJTimingSessions.get(userId, packageName);
+                final List<TimedEvent> sessions = mEJTimingSessions.get(userId, packageName);
                 if (sessions == null) {
                     continue;
                 }
 
                 while (sessions.size() > 0) {
-                    final TimingSession ts = sessions.get(0);
-                    if (mTimingSessionTooOld.test(ts)) {
+                    final TimingSession ts = (TimingSession) sessions.get(0);
+                    if (mTimedEventTooOld.test(ts)) {
                         // Stale sessions may still be factored into tally. Remove them.
                         final long duration = ts.endTimeElapsed - ts.startTimeElapsed;
                         debits.transactLocked(-duration);
@@ -2801,6 +2915,7 @@
         mQcConstants.mRateLimitingConstantsUpdated = false;
         mQcConstants.mExecutionPeriodConstantsUpdated = false;
         mQcConstants.mEJLimitConstantsUpdated = false;
+        mQcConstants.mQuotaBumpConstantsUpdated = false;
     }
 
     @Override
@@ -2828,6 +2943,7 @@
         private boolean mRateLimitingConstantsUpdated = false;
         private boolean mExecutionPeriodConstantsUpdated = false;
         private boolean mEJLimitConstantsUpdated = false;
+        private boolean mQuotaBumpConstantsUpdated = false;
 
         /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */
         private static final String QC_CONSTANT_PREFIX = "qc_";
@@ -2975,6 +3091,21 @@
         @VisibleForTesting
         static final String KEY_EJ_GRACE_PERIOD_TOP_APP_MS =
                 QC_CONSTANT_PREFIX + "ej_grace_period_top_app_ms";
+        @VisibleForTesting
+        static final String KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS =
+                QC_CONSTANT_PREFIX + "quota_bump_additional_duration_ms";
+        @VisibleForTesting
+        static final String KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT =
+                QC_CONSTANT_PREFIX + "quota_bump_additional_job_count";
+        @VisibleForTesting
+        static final String KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT =
+                QC_CONSTANT_PREFIX + "quota_bump_additional_session_count";
+        @VisibleForTesting
+        static final String KEY_QUOTA_BUMP_WINDOW_SIZE_MS =
+                QC_CONSTANT_PREFIX + "quota_bump_window_size_ms";
+        @VisibleForTesting
+        static final String KEY_QUOTA_BUMP_LIMIT =
+                QC_CONSTANT_PREFIX + "quota_bump_limit";
 
         private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS =
                 10 * 60 * 1000L; // 10 minutes
@@ -3046,6 +3177,11 @@
         private static final long DEFAULT_EJ_REWARD_NOTIFICATION_SEEN_MS = 0;
         private static final long DEFAULT_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS = 3 * MINUTE_IN_MILLIS;
         private static final long DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS = 1 * MINUTE_IN_MILLIS;
+        private static final long DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS = 1 * MINUTE_IN_MILLIS;
+        private static final int DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT = 2;
+        private static final int DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = 1;
+        private static final long DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS = 8 * HOUR_IN_MILLIS;
+        private static final int DEFAULT_QUOTA_BUMP_LIMIT = 8;
 
         /**
          * How much time each app in the exempted bucket will have to run jobs within their standby
@@ -3347,6 +3483,33 @@
          */
         public long EJ_GRACE_PERIOD_TOP_APP_MS = DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS;
 
+        /**
+         * How much additional session duration to give an app for each accepted quota bump.
+         */
+        public long QUOTA_BUMP_ADDITIONAL_DURATION_MS = DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS;
+
+        /**
+         * How many additional regular jobs to give an app for each accepted quota bump.
+         */
+        public int QUOTA_BUMP_ADDITIONAL_JOB_COUNT = DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT;
+
+        /**
+         * How many additional sessions to give an app for each accepted quota bump.
+         */
+        public int QUOTA_BUMP_ADDITIONAL_SESSION_COUNT =
+                DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT;
+
+        /**
+         * The rolling window size within which to accept and apply quota bump events.
+         */
+        public long QUOTA_BUMP_WINDOW_SIZE_MS = DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS;
+
+        /**
+         * The maximum number of quota bumps to accept and apply within the
+         * {@link #QUOTA_BUMP_WINDOW_SIZE_MS window}.
+         */
+        public int QUOTA_BUMP_LIMIT = DEFAULT_QUOTA_BUMP_LIMIT;
+
         public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
                 @NonNull String key) {
             switch (key) {
@@ -3383,6 +3546,14 @@
                     updateEJLimitConstantsLocked();
                     break;
 
+                case KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS:
+                case KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT:
+                case KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT:
+                case KEY_QUOTA_BUMP_WINDOW_SIZE_MS:
+                case KEY_QUOTA_BUMP_LIMIT:
+                    updateQuotaBumpConstantsLocked();
+                    break;
+
                 case KEY_MAX_JOB_COUNT_EXEMPTED:
                     MAX_JOB_COUNT_EXEMPTED = properties.getInt(key, DEFAULT_MAX_JOB_COUNT_EXEMPTED);
                     int newExemptedMaxJobCount =
@@ -3881,6 +4052,65 @@
             }
         }
 
+        private void updateQuotaBumpConstantsLocked() {
+            if (mQuotaBumpConstantsUpdated) {
+                return;
+            }
+            mQuotaBumpConstantsUpdated = true;
+
+            // Query the values as an atomic set.
+            final DeviceConfig.Properties properties = DeviceConfig.getProperties(
+                    DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+                    KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS,
+                    KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT,
+                    KEY_QUOTA_BUMP_WINDOW_SIZE_MS, KEY_QUOTA_BUMP_LIMIT);
+            QUOTA_BUMP_ADDITIONAL_DURATION_MS = properties.getLong(
+                    KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS,
+                    DEFAULT_QUOTA_BUMP_ADDITIONAL_DURATION_MS);
+            QUOTA_BUMP_ADDITIONAL_JOB_COUNT = properties.getInt(
+                    KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, DEFAULT_QUOTA_BUMP_ADDITIONAL_JOB_COUNT);
+            QUOTA_BUMP_ADDITIONAL_SESSION_COUNT = properties.getInt(
+                    KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT,
+                    DEFAULT_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT);
+            QUOTA_BUMP_WINDOW_SIZE_MS = properties.getLong(
+                    KEY_QUOTA_BUMP_WINDOW_SIZE_MS, DEFAULT_QUOTA_BUMP_WINDOW_SIZE_MS);
+            QUOTA_BUMP_LIMIT = properties.getInt(
+                    KEY_QUOTA_BUMP_LIMIT, DEFAULT_QUOTA_BUMP_LIMIT);
+
+            // The window must be in the range [1 hour, 24 hours].
+            long newWindowSizeMs = Math.max(HOUR_IN_MILLIS,
+                    Math.min(MAX_PERIOD_MS, QUOTA_BUMP_WINDOW_SIZE_MS));
+            if (mQuotaBumpWindowSizeMs != newWindowSizeMs) {
+                mQuotaBumpWindowSizeMs = newWindowSizeMs;
+                mShouldReevaluateConstraints = true;
+            }
+            // The limit must be nonnegative.
+            int newLimit = Math.max(0, QUOTA_BUMP_LIMIT);
+            if (mQuotaBumpLimit != newLimit) {
+                mQuotaBumpLimit = newLimit;
+                mShouldReevaluateConstraints = true;
+            }
+            // The job count must be nonnegative.
+            int newJobAddition = Math.max(0, QUOTA_BUMP_ADDITIONAL_JOB_COUNT);
+            if (mQuotaBumpAdditionalJobCount != newJobAddition) {
+                mQuotaBumpAdditionalJobCount = newJobAddition;
+                mShouldReevaluateConstraints = true;
+            }
+            // The session count must be nonnegative.
+            int newSessionAddition = Math.max(0, QUOTA_BUMP_ADDITIONAL_SESSION_COUNT);
+            if (mQuotaBumpAdditionalSessionCount != newSessionAddition) {
+                mQuotaBumpAdditionalSessionCount = newSessionAddition;
+                mShouldReevaluateConstraints = true;
+            }
+            // The additional duration must be in the range [0, 10 minutes].
+            long newAdditionalDuration = Math.max(0,
+                    Math.min(10 * MINUTE_IN_MILLIS, QUOTA_BUMP_ADDITIONAL_DURATION_MS));
+            if (mQuotaBumpAdditionalDurationMs != newAdditionalDuration) {
+                mQuotaBumpAdditionalDurationMs = newAdditionalDuration;
+                mShouldReevaluateConstraints = true;
+            }
+        }
+
         private void dump(IndentingPrintWriter pw) {
             pw.println();
             pw.println("QuotaController:");
@@ -3943,6 +4173,15 @@
                     EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS).println();
             pw.print(KEY_EJ_GRACE_PERIOD_TOP_APP_MS, EJ_GRACE_PERIOD_TOP_APP_MS).println();
 
+            pw.print(KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS,
+                    QUOTA_BUMP_ADDITIONAL_DURATION_MS).println();
+            pw.print(KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT,
+                    QUOTA_BUMP_ADDITIONAL_JOB_COUNT).println();
+            pw.print(KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT,
+                    QUOTA_BUMP_ADDITIONAL_SESSION_COUNT).println();
+            pw.print(KEY_QUOTA_BUMP_WINDOW_SIZE_MS, QUOTA_BUMP_WINDOW_SIZE_MS).println();
+            pw.print(KEY_QUOTA_BUMP_LIMIT, QUOTA_BUMP_LIMIT).println();
+
             pw.decreaseIndent();
         }
 
@@ -4103,7 +4342,7 @@
 
     @VisibleForTesting
     @Nullable
-    List<TimingSession> getEJTimingSessions(int userId, String packageName) {
+    List<TimedEvent> getEJTimingSessions(int userId, String packageName) {
         return mEJTimingSessions.get(userId, packageName);
     }
 
@@ -4150,8 +4389,8 @@
 
     @VisibleForTesting
     @Nullable
-    List<TimingSession> getTimingSessions(int userId, String packageName) {
-        return mTimingSessions.get(userId, packageName);
+    List<TimedEvent> getTimingSessions(int userId, String packageName) {
+        return mTimingEvents.get(userId, packageName);
     }
 
     @VisibleForTesting
@@ -4160,6 +4399,31 @@
         return mQcConstants;
     }
 
+    @VisibleForTesting
+    long getQuotaBumpAdditionDurationMs() {
+        return mQuotaBumpAdditionalDurationMs;
+    }
+
+    @VisibleForTesting
+    int getQuotaBumpAdditionJobCount() {
+        return mQuotaBumpAdditionalJobCount;
+    }
+
+    @VisibleForTesting
+    int getQuotaBumpAdditionSessionCount() {
+        return mQuotaBumpAdditionalSessionCount;
+    }
+
+    @VisibleForTesting
+    int getQuotaBumpLimit() {
+        return mQuotaBumpLimit;
+    }
+
+    @VisibleForTesting
+    long getQuotaBumpWindowSizeMs() {
+        return mQuotaBumpWindowSizeMs;
+    }
+
     //////////////////////////// DATA DUMP //////////////////////////////
 
     @NeverCompile // Avoid size overhead of debugging code.
@@ -4250,14 +4514,14 @@
                 final String pkgName = mPkgTimers.keyAt(u, p);
                 mPkgTimers.valueAt(u, p).dump(pw, predicate);
                 pw.println();
-                List<TimingSession> sessions = mTimingSessions.get(userId, pkgName);
-                if (sessions != null) {
+                List<TimedEvent> events = mTimingEvents.get(userId, pkgName);
+                if (events != null) {
                     pw.increaseIndent();
-                    pw.println("Saved sessions:");
+                    pw.println("Saved events:");
                     pw.increaseIndent();
-                    for (int j = sessions.size() - 1; j >= 0; j--) {
-                        TimingSession session = sessions.get(j);
-                        session.dump(pw);
+                    for (int j = events.size() - 1; j >= 0; j--) {
+                        TimedEvent event = events.get(j);
+                        event.dump(pw);
                     }
                     pw.decreaseIndent();
                     pw.decreaseIndent();
@@ -4273,13 +4537,13 @@
                 final String pkgName = mEJPkgTimers.keyAt(u, p);
                 mEJPkgTimers.valueAt(u, p).dump(pw, predicate);
                 pw.println();
-                List<TimingSession> sessions = mEJTimingSessions.get(userId, pkgName);
+                List<TimedEvent> sessions = mEJTimingSessions.get(userId, pkgName);
                 if (sessions != null) {
                     pw.increaseIndent();
                     pw.println("Saved sessions:");
                     pw.increaseIndent();
                     for (int j = sessions.size() - 1; j >= 0; j--) {
-                        TimingSession session = sessions.get(j);
+                        TimedEvent session = sessions.get(j);
                         session.dump(pw);
                     }
                     pw.decreaseIndent();
@@ -4398,10 +4662,14 @@
                             predicate);
                 }
 
-                List<TimingSession> sessions = mTimingSessions.get(userId, pkgName);
-                if (sessions != null) {
-                    for (int j = sessions.size() - 1; j >= 0; j--) {
-                        TimingSession session = sessions.get(j);
+                List<TimedEvent> events = mTimingEvents.get(userId, pkgName);
+                if (events != null) {
+                    for (int j = events.size() - 1; j >= 0; j--) {
+                        TimedEvent event = events.get(j);
+                        if (!(event instanceof TimingSession)) {
+                            continue;
+                        }
+                        TimingSession session = (TimingSession) event;
                         session.dump(proto,
                                 StateControllerProto.QuotaController.PackageStats.SAVED_SESSIONS);
                     }
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
index f4faec8..8b8a57d 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
@@ -73,6 +73,7 @@
 
     private final Object mLock;
     private final Handler mHandler;
+    private final Analyst mAnalyst;
     private final InternalResourceService mIrs;
     private final Scribe mScribe;
 
@@ -110,10 +111,11 @@
      */
     private static final int MSG_CHECK_INDIVIDUAL_AFFORDABILITY = 1;
 
-    Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe) {
+    Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe, @NonNull Analyst analyst) {
         mLock = irs.getLock();
         mIrs = irs;
         mScribe = scribe;
+        mAnalyst = analyst;
         mHandler = new AgentHandler(TareHandlerThread.get().getLooper());
         mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
         mBalanceThresholdAlarmQueue = new BalanceThresholdAlarmQueue(
@@ -443,7 +445,7 @@
     void recordTransactionLocked(final int userId, @NonNull final String pkgName,
             @NonNull Ledger ledger, @NonNull Ledger.Transaction transaction,
             final boolean notifyOnAffordabilityChange) {
-        if (transaction.delta == 0) {
+        if (!DEBUG && transaction.delta == 0) {
             // Skip recording transactions with a delta of 0 to save on space.
             return;
         }
@@ -471,6 +473,7 @@
         }
         ledger.recordTransaction(transaction);
         mScribe.adjustRemainingConsumableCakesLocked(-transaction.ctp);
+        mAnalyst.noteTransaction(transaction);
         if (transaction.delta != 0 && notifyOnAffordabilityChange) {
             final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes =
                     mActionAffordabilityNotes.get(userId, pkgName);
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java b/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java
new file mode 100644
index 0000000..bc6fe7e5
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java
@@ -0,0 +1,290 @@
+/*
+ * 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.tare;
+
+import static com.android.server.tare.EconomicPolicy.TYPE_ACTION;
+import static com.android.server.tare.EconomicPolicy.TYPE_REGULATION;
+import static com.android.server.tare.EconomicPolicy.TYPE_REWARD;
+import static com.android.server.tare.EconomicPolicy.getEventType;
+import static com.android.server.tare.TareUtils.cakeToString;
+
+import android.annotation.NonNull;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Responsible for maintaining statistics and analysis of TARE's performance.
+ */
+public class Analyst {
+    private static final String TAG = "TARE-" + Analyst.class.getSimpleName();
+    private static final boolean DEBUG = InternalResourceService.DEBUG
+            || Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final int NUM_PERIODS_TO_RETAIN = 8;
+
+    static final class Report {
+        /** How much the battery was discharged over the tracked period. */
+        public int cumulativeBatteryDischarge = 0;
+        public int currentBatteryLevel = 0;
+        /**
+         * Profit from performing actions. This excludes special circumstances where we charge the
+         * app
+         * less than the action's CTP.
+         */
+        public long cumulativeProfit = 0;
+        public int numProfitableActions = 0;
+        /**
+         * Losses from performing actions for special circumstances (eg. for a TOP app) where we
+         * charge
+         * the app less than the action's CTP.
+         */
+        public long cumulativeLoss = 0;
+        public int numUnprofitableActions = 0;
+        /**
+         * The total number of rewards given to apps over this period.
+         */
+        public long cumulativeRewards = 0;
+        public int numRewards = 0;
+        /**
+         * Regulations that increased an app's balance.
+         */
+        public long cumulativePositiveRegulations = 0;
+        public int numPositiveRegulations = 0;
+        /**
+         * Regulations that decreased an app's balance.
+         */
+        public long cumulativeNegativeRegulations = 0;
+        public int numNegativeRegulations = 0;
+
+        private void clear() {
+            cumulativeBatteryDischarge = 0;
+            currentBatteryLevel = 0;
+            cumulativeProfit = 0;
+            numProfitableActions = 0;
+            cumulativeLoss = 0;
+            numUnprofitableActions = 0;
+            cumulativeRewards = 0;
+            numRewards = 0;
+            cumulativePositiveRegulations = 0;
+            numPositiveRegulations = 0;
+            cumulativeNegativeRegulations = 0;
+            numNegativeRegulations = 0;
+        }
+    }
+
+    private int mPeriodIndex = 0;
+    /** How much the battery was discharged over the tracked period. */
+    private final Report[] mReports = new Report[NUM_PERIODS_TO_RETAIN];
+
+    /** Returns the list of most recent reports, with the oldest report first. */
+    @NonNull
+    List<Report> getReports() {
+        final List<Report> list = new ArrayList<>(NUM_PERIODS_TO_RETAIN);
+        for (int i = 1; i <= NUM_PERIODS_TO_RETAIN; ++i) {
+            final int idx = (mPeriodIndex + i) % NUM_PERIODS_TO_RETAIN;
+            final Report report = mReports[idx];
+            if (report != null) {
+                list.add(report);
+            }
+        }
+        return list;
+    }
+
+    /**
+     * Tracks the given reports instead of whatever is currently saved. Reports should be ordered
+     * oldest to most recent.
+     */
+    void loadReports(@NonNull List<Report> reports) {
+        final int numReports = reports.size();
+        mPeriodIndex = Math.max(0, numReports - 1);
+        for (int i = 0; i < NUM_PERIODS_TO_RETAIN; ++i) {
+            if (i < numReports) {
+                mReports[i] = reports.get(i);
+            } else {
+                mReports[i] = null;
+            }
+        }
+    }
+
+    void noteBatteryLevelChange(int newBatteryLevel) {
+        if (newBatteryLevel == 100 && mReports[mPeriodIndex] != null
+                && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel) {
+            mPeriodIndex = (mPeriodIndex + 1) % NUM_PERIODS_TO_RETAIN;
+            if (mReports[mPeriodIndex] != null) {
+                final Report report = mReports[mPeriodIndex];
+                report.clear();
+                report.currentBatteryLevel = newBatteryLevel;
+                return;
+            }
+        }
+
+        if (mReports[mPeriodIndex] == null) {
+            Report report = new Report();
+            mReports[mPeriodIndex] = report;
+            report.currentBatteryLevel = newBatteryLevel;
+            return;
+        }
+
+        final Report report = mReports[mPeriodIndex];
+        if (newBatteryLevel < report.currentBatteryLevel) {
+            report.cumulativeBatteryDischarge += (report.currentBatteryLevel - newBatteryLevel);
+        }
+        report.currentBatteryLevel = newBatteryLevel;
+    }
+
+    void noteTransaction(@NonNull Ledger.Transaction transaction) {
+        if (mReports[mPeriodIndex] == null) {
+            mReports[mPeriodIndex] = new Report();
+        }
+        final Report report = mReports[mPeriodIndex];
+        switch (getEventType(transaction.eventId)) {
+            case TYPE_ACTION:
+                // For now, assume all instances where price < CTP is a special instance.
+                // TODO: add an explicit signal for special circumstances
+                if (-transaction.delta > transaction.ctp) {
+                    report.cumulativeProfit += (-transaction.delta - transaction.ctp);
+                    report.numProfitableActions++;
+                } else if (-transaction.delta < transaction.ctp) {
+                    report.cumulativeLoss += (transaction.ctp + transaction.delta);
+                    report.numUnprofitableActions++;
+                }
+                break;
+            case TYPE_REGULATION:
+                if (transaction.delta > 0) {
+                    report.cumulativePositiveRegulations += transaction.delta;
+                    report.numPositiveRegulations++;
+                } else if (transaction.delta < 0) {
+                    report.cumulativeNegativeRegulations -= transaction.delta;
+                    report.numNegativeRegulations++;
+                }
+                break;
+            case TYPE_REWARD:
+                if (transaction.delta != 0) {
+                    report.cumulativeRewards += transaction.delta;
+                    report.numRewards++;
+                }
+                break;
+        }
+    }
+
+    void tearDown() {
+        for (int i = 0; i < mReports.length; ++i) {
+            mReports[i] = null;
+        }
+        mPeriodIndex = 0;
+    }
+
+    @NonNull
+    private String padStringWithSpaces(@NonNull String text, int targetLength) {
+        // Make sure to have at least one space on either side.
+        final int padding = Math.max(2, targetLength - text.length()) >>> 1;
+        return " ".repeat(padding) + text + " ".repeat(padding);
+    }
+
+    void dump(IndentingPrintWriter pw) {
+        pw.println("Reports:");
+        pw.increaseIndent();
+        pw.print("      Total Discharge");
+        final int statColsLength = 47;
+        pw.print(padStringWithSpaces("Profit (avg/action : avg/discharge)", statColsLength));
+        pw.print(padStringWithSpaces("Loss (avg/action : avg/discharge)", statColsLength));
+        pw.print(padStringWithSpaces("Rewards (avg/reward : avg/discharge)", statColsLength));
+        pw.print(padStringWithSpaces("+Regs (avg/reg : avg/discharge)", statColsLength));
+        pw.print(padStringWithSpaces("-Regs (avg/reg : avg/discharge)", statColsLength));
+        pw.println();
+        for (int r = 0; r < NUM_PERIODS_TO_RETAIN; ++r) {
+            final int idx = (mPeriodIndex - r + NUM_PERIODS_TO_RETAIN) % NUM_PERIODS_TO_RETAIN;
+            final Report report = mReports[idx];
+            if (report == null) {
+                continue;
+            }
+            pw.print("t-");
+            pw.print(r);
+            pw.print(":  ");
+            pw.print(padStringWithSpaces(Integer.toString(report.cumulativeBatteryDischarge), 15));
+            if (report.numProfitableActions > 0) {
+                final String perDischarge = report.cumulativeBatteryDischarge > 0
+                        ? cakeToString(report.cumulativeProfit / report.cumulativeBatteryDischarge)
+                        : "N/A";
+                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
+                                cakeToString(report.cumulativeProfit),
+                                cakeToString(report.cumulativeProfit / report.numProfitableActions),
+                                perDischarge),
+                        statColsLength));
+            } else {
+                pw.print(padStringWithSpaces("N/A", statColsLength));
+            }
+            if (report.numUnprofitableActions > 0) {
+                final String perDischarge = report.cumulativeBatteryDischarge > 0
+                        ? cakeToString(report.cumulativeLoss / report.cumulativeBatteryDischarge)
+                        : "N/A";
+                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
+                                cakeToString(report.cumulativeLoss),
+                                cakeToString(report.cumulativeLoss / report.numUnprofitableActions),
+                                perDischarge),
+                        statColsLength));
+            } else {
+                pw.print(padStringWithSpaces("N/A", statColsLength));
+            }
+            if (report.numRewards > 0) {
+                final String perDischarge = report.cumulativeBatteryDischarge > 0
+                        ? cakeToString(report.cumulativeRewards / report.cumulativeBatteryDischarge)
+                        : "N/A";
+                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
+                                cakeToString(report.cumulativeRewards),
+                                cakeToString(report.cumulativeRewards / report.numRewards),
+                                perDischarge),
+                        statColsLength));
+            } else {
+                pw.print(padStringWithSpaces("N/A", statColsLength));
+            }
+            if (report.numPositiveRegulations > 0) {
+                final String perDischarge = report.cumulativeBatteryDischarge > 0
+                        ? cakeToString(
+                        report.cumulativePositiveRegulations / report.cumulativeBatteryDischarge)
+                        : "N/A";
+                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
+                                cakeToString(report.cumulativePositiveRegulations),
+                                cakeToString(report.cumulativePositiveRegulations
+                                        / report.numPositiveRegulations),
+                                perDischarge),
+                        statColsLength));
+            } else {
+                pw.print(padStringWithSpaces("N/A", statColsLength));
+            }
+            if (report.numNegativeRegulations > 0) {
+                final String perDischarge = report.cumulativeBatteryDischarge > 0
+                        ? cakeToString(
+                        report.cumulativeNegativeRegulations / report.cumulativeBatteryDischarge)
+                        : "N/A";
+                pw.print(padStringWithSpaces(String.format("%s (%s : %s)",
+                                cakeToString(report.cumulativeNegativeRegulations),
+                                cakeToString(report.cumulativeNegativeRegulations
+                                        / report.numNegativeRegulations),
+                                perDischarge),
+                        statColsLength));
+            } else {
+                pw.print(padStringWithSpaces("N/A", statColsLength));
+            }
+            pw.println();
+        }
+        pw.decreaseIndent();
+    }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
index 2118eeb..9c7d702 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -121,6 +121,7 @@
     private IDeviceIdleController mDeviceIdleController;
 
     private final Agent mAgent;
+    private final Analyst mAnalyst;
     private final ConfigObserver mConfigObserver;
     private final EconomyManagerStub mEconomyManagerStub;
     private final Scribe mScribe;
@@ -254,9 +255,10 @@
         mPackageManager = context.getPackageManager();
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         mEconomyManagerStub = new EconomyManagerStub();
-        mScribe = new Scribe(this);
+        mAnalyst = new Analyst();
+        mScribe = new Scribe(this, mAnalyst);
         mCompleteEconomicPolicy = new CompleteEconomicPolicy(this);
-        mAgent = new Agent(this, mScribe);
+        mAgent = new Agent(this, mScribe, mAnalyst);
 
         mConfigObserver = new ConfigObserver(mHandler, context);
 
@@ -375,6 +377,7 @@
     void onBatteryLevelChanged() {
         synchronized (mLock) {
             final int newBatteryLevel = getCurrentBatteryLevel();
+            mAnalyst.noteBatteryLevelChange(newBatteryLevel);
             final boolean increased = newBatteryLevel > mCurrentBatteryLevel;
             if (increased) {
                 mAgent.distributeBasicIncomeLocked(newBatteryLevel);
@@ -741,6 +744,7 @@
         }
         synchronized (mLock) {
             mAgent.tearDownLocked();
+            mAnalyst.tearDown();
             mCompleteEconomicPolicy.tearDown();
             mExemptedApps.clear();
             mExemptListLoaded = false;
@@ -1145,6 +1149,9 @@
 
             pw.println();
             mAgent.dumpLocked(pw);
+
+            pw.println();
+            mAnalyst.dump(pw);
         }
     }
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
index 7442877..941cc39 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
@@ -72,6 +72,7 @@
     private static final String XML_TAG_TARE = "tare";
     private static final String XML_TAG_TRANSACTION = "transaction";
     private static final String XML_TAG_USER = "user";
+    private static final String XML_TAG_PERIOD_REPORT = "report";
 
     private static final String XML_ATTR_CTP = "ctp";
     private static final String XML_ATTR_DELTA = "delta";
@@ -86,6 +87,18 @@
     private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime";
     private static final String XML_ATTR_REMAINING_CONSUMABLE_CAKES = "remainingConsumableCakes";
     private static final String XML_ATTR_CONSUMPTION_LIMIT = "consumptionLimit";
+    private static final String XML_ATTR_PR_DISCHARGE = "discharge";
+    private static final String XML_ATTR_PR_BATTERY_LEVEL = "batteryLevel";
+    private static final String XML_ATTR_PR_PROFIT = "profit";
+    private static final String XML_ATTR_PR_NUM_PROFIT = "numProfits";
+    private static final String XML_ATTR_PR_LOSS = "loss";
+    private static final String XML_ATTR_PR_NUM_LOSS = "numLoss";
+    private static final String XML_ATTR_PR_REWARDS = "rewards";
+    private static final String XML_ATTR_PR_NUM_REWARDS = "numRewards";
+    private static final String XML_ATTR_PR_POS_REGULATIONS = "posRegulations";
+    private static final String XML_ATTR_PR_NUM_POS_REGULATIONS = "numPosRegulations";
+    private static final String XML_ATTR_PR_NEG_REGULATIONS = "negRegulations";
+    private static final String XML_ATTR_PR_NUM_NEG_REGULATIONS = "numNegRegulations";
 
     /** Version of the file schema. */
     private static final int STATE_FILE_VERSION = 0;
@@ -94,6 +107,7 @@
 
     private final AtomicFile mStateFile;
     private final InternalResourceService mIrs;
+    private final Analyst mAnalyst;
 
     @GuardedBy("mIrs.getLock()")
     private long mLastReclamationTime;
@@ -107,13 +121,14 @@
     private final Runnable mCleanRunnable = this::cleanupLedgers;
     private final Runnable mWriteRunnable = this::writeState;
 
-    Scribe(InternalResourceService irs) {
-        this(irs, Environment.getDataSystemDirectory());
+    Scribe(InternalResourceService irs, Analyst analyst) {
+        this(irs, analyst, Environment.getDataSystemDirectory());
     }
 
     @VisibleForTesting
-    Scribe(InternalResourceService irs, File dataDir) {
+    Scribe(InternalResourceService irs, Analyst analyst, File dataDir) {
         mIrs = irs;
+        mAnalyst = analyst;
 
         final File tareDir = new File(dataDir, "tare");
         //noinspection ResultOfMethodCallIgnored
@@ -210,6 +225,7 @@
             }
         }
 
+        final List<Analyst.Report> reports = new ArrayList<>();
         try (FileInputStream fis = mStateFile.openRead()) {
             TypedXmlPullParser parser = Xml.resolvePullParser(fis);
 
@@ -263,11 +279,15 @@
                                 readUserFromXmlLocked(
                                         parser, installedPackagesPerUser, endTimeCutoff));
                         break;
+                    case XML_TAG_PERIOD_REPORT:
+                        reports.add(readReportFromXml(parser));
+                        break;
                     default:
                         Slog.e(TAG, "Unexpected tag: " + tagName);
                         break;
                 }
             }
+            mAnalyst.loadReports(reports);
             scheduleCleanup(earliestEndTime);
         } catch (IOException | XmlPullParserException e) {
             Slog.wtf(TAG, "Error reading state from disk", e);
@@ -457,6 +477,37 @@
         return earliestEndTime;
     }
 
+
+    /**
+     * @param parser Xml parser at the beginning of a {@link #XML_TAG_PERIOD_REPORT} tag. The next
+     *               "parser.next()" call will take the parser into the body of the report tag.
+     * @return Newly instantiated Report holding all the information we just read out of the xml tag
+     */
+    @NonNull
+    private static Analyst.Report readReportFromXml(TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        final Analyst.Report report = new Analyst.Report();
+
+        report.cumulativeBatteryDischarge = parser.getAttributeInt(null, XML_ATTR_PR_DISCHARGE);
+        report.currentBatteryLevel = parser.getAttributeInt(null, XML_ATTR_PR_BATTERY_LEVEL);
+        report.cumulativeProfit = parser.getAttributeLong(null, XML_ATTR_PR_PROFIT);
+        report.numProfitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_PROFIT);
+        report.cumulativeLoss = parser.getAttributeLong(null, XML_ATTR_PR_LOSS);
+        report.numUnprofitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_LOSS);
+        report.cumulativeRewards = parser.getAttributeLong(null, XML_ATTR_PR_REWARDS);
+        report.numRewards = parser.getAttributeInt(null, XML_ATTR_PR_NUM_REWARDS);
+        report.cumulativePositiveRegulations =
+                parser.getAttributeLong(null, XML_ATTR_PR_POS_REGULATIONS);
+        report.numPositiveRegulations =
+                parser.getAttributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS);
+        report.cumulativeNegativeRegulations =
+                parser.getAttributeLong(null, XML_ATTR_PR_NEG_REGULATIONS);
+        report.numNegativeRegulations =
+                parser.getAttributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS);
+
+        return report;
+    }
+
     private void scheduleCleanup(long earliestEndTime) {
         if (earliestEndTime == Long.MAX_VALUE) {
             return;
@@ -501,6 +552,11 @@
                             writeUserLocked(out, userId));
                 }
 
+                List<Analyst.Report> reports = mAnalyst.getReports();
+                for (int i = 0, size = reports.size(); i < size; ++i) {
+                    writeReport(out, reports.get(i));
+                }
+
                 out.endTag(null, XML_TAG_TARE);
 
                 out.endDocument();
@@ -560,6 +616,24 @@
         out.endTag(null, XML_TAG_TRANSACTION);
     }
 
+    private static void writeReport(@NonNull TypedXmlSerializer out,
+            @NonNull Analyst.Report report) throws IOException {
+        out.startTag(null, XML_TAG_PERIOD_REPORT);
+        out.attributeInt(null, XML_ATTR_PR_DISCHARGE, report.cumulativeBatteryDischarge);
+        out.attributeInt(null, XML_ATTR_PR_BATTERY_LEVEL, report.currentBatteryLevel);
+        out.attributeLong(null, XML_ATTR_PR_PROFIT, report.cumulativeProfit);
+        out.attributeInt(null, XML_ATTR_PR_NUM_PROFIT, report.numProfitableActions);
+        out.attributeLong(null, XML_ATTR_PR_LOSS, report.cumulativeLoss);
+        out.attributeInt(null, XML_ATTR_PR_NUM_LOSS, report.numUnprofitableActions);
+        out.attributeLong(null, XML_ATTR_PR_REWARDS, report.cumulativeRewards);
+        out.attributeInt(null, XML_ATTR_PR_NUM_REWARDS, report.numRewards);
+        out.attributeLong(null, XML_ATTR_PR_POS_REGULATIONS, report.cumulativePositiveRegulations);
+        out.attributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS, report.numPositiveRegulations);
+        out.attributeLong(null, XML_ATTR_PR_NEG_REGULATIONS, report.cumulativeNegativeRegulations);
+        out.attributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS, report.numNegativeRegulations);
+        out.endTag(null, XML_TAG_PERIOD_REPORT);
+    }
+
     @GuardedBy("mIrs.getLock()")
     void dumpLocked(IndentingPrintWriter pw, boolean dumpAll) {
         pw.println("Ledgers:");
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 1891e06..1c4ec85 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -289,6 +289,7 @@
     static final int MSG_INFORM_LISTENERS = 3;
     static final int MSG_FORCE_IDLE_STATE = 4;
     static final int MSG_CHECK_IDLE_STATES = 5;
+    static final int MSG_TRIGGER_LISTENER_QUOTA_BUMP = 7;
     static final int MSG_REPORT_CONTENT_PROVIDER_USAGE = 8;
     static final int MSG_PAROLE_STATE_CHANGED = 9;
     static final int MSG_ONE_TIME_CHECK_IDLE_STATES = 10;
@@ -318,6 +319,12 @@
     /** The standby bucket that an app will be promoted on a notification-seen event */
     int mNotificationSeenPromotedBucket =
             ConstantsObserver.DEFAULT_NOTIFICATION_SEEN_PROMOTED_BUCKET;
+    /**
+     * If true, tell each {@link AppIdleStateChangeListener} to give quota bump for each
+     * notification seen event.
+     */
+    private boolean mTriggerQuotaBumpOnNotificationSeen =
+            ConstantsObserver.DEFAULT_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN;
     /** Minimum time a system update event should keep the buckets elevated. */
     long mSystemUpdateUsageTimeoutMillis = ConstantsObserver.DEFAULT_SYSTEM_UPDATE_TIMEOUT;
     /** Maximum time to wait for a prediction before using simple timeouts to downgrade buckets. */
@@ -377,6 +384,32 @@
             ConstantsObserver.DEFAULT_BROADCAST_RESPONSE_FG_THRESHOLD_STATE;
 
     /**
+     * Duration (in millis) for the window within which any broadcasts occurred will be
+     * treated as one broadcast session.
+     */
+    volatile long mBroadcastSessionsDurationMs =
+            ConstantsObserver.DEFAULT_BROADCAST_SESSIONS_DURATION_MS;
+
+    /**
+     * Duration (in millis) for the window within which any broadcasts occurred ((with a
+     * corresponding response event) will be treated as one broadcast session. This similar to
+     * {@link #mBroadcastSessionsDurationMs}, except that this duration will be used to group only
+     * broadcasts that have a corresponding response event into sessions.
+     */
+    volatile long mBroadcastSessionsWithResponseDurationMs =
+            ConstantsObserver.DEFAULT_BROADCAST_SESSIONS_WITH_RESPONSE_DURATION_MS;
+
+    /**
+     * Denotes whether the response event should be attributed to all broadcast sessions or not.
+     * If this is {@code true}, then the response event should be attributed to all the broadcast
+     * sessions that occurred within the broadcast response window. Otherwise, the
+     * response event should be attributed to only the earliest broadcast session within the
+     * broadcast response window.
+     */
+    volatile boolean mNoteResponseEventForAllBroadcastSessions =
+            ConstantsObserver.DEFAULT_NOTE_RESPONSE_EVENT_FOR_ALL_BROADCAST_SESSIONS;
+
+    /**
      * Map of last known values of keys in {@link DeviceConfig#NAMESPACE_APP_STANDBY}.
      *
      * Note: We are intentionally not guarding this by any lock since this is only updated on
@@ -741,6 +774,17 @@
                 appUsage.bucketingReason, false);
     }
 
+    /** Trigger a quota bump in the listeners. */
+    private void triggerListenerQuotaBump(String packageName, int userId) {
+        if (!mAppIdleEnabled) return;
+
+        synchronized (mPackageAccessListeners) {
+            for (AppIdleStateChangeListener listener : mPackageAccessListeners) {
+                listener.triggerTemporaryQuotaBump(packageName, userId);
+            }
+        }
+    }
+
     @VisibleForTesting
     void setChargingState(boolean isCharging) {
         if (mIsCharging != isCharging) {
@@ -1054,6 +1098,10 @@
         final int subReason = usageEventToSubReason(eventType);
         final int reason = REASON_MAIN_USAGE | subReason;
         if (eventType == UsageEvents.Event.NOTIFICATION_SEEN) {
+            if (mTriggerQuotaBumpOnNotificationSeen) {
+                mHandler.obtainMessage(MSG_TRIGGER_LISTENER_QUOTA_BUMP, userId, -1, pkg)
+                        .sendToTarget();
+            }
             // Notification-seen elevates to a higher bucket (depending on
             // {@link ConstantsObserver#KEY_NOTIFICATION_SEEN_PROMOTED_BUCKET}) but doesn't
             // change usage time.
@@ -1869,6 +1917,21 @@
     }
 
     @Override
+    public long getBroadcastSessionsDurationMs() {
+        return mBroadcastSessionsDurationMs;
+    }
+
+    @Override
+    public long getBroadcastSessionsWithResponseDurationMs() {
+        return mBroadcastSessionsWithResponseDurationMs;
+    }
+
+    @Override
+    public boolean shouldNoteResponseEventForAllBroadcastSessions() {
+        return mNoteResponseEventForAllBroadcastSessions;
+    }
+
+    @Override
     @Nullable
     public String getAppStandbyConstant(@NonNull String key) {
         return mAppStandbyProperties.get(key);
@@ -2160,6 +2223,9 @@
         pw.print("  mNotificationSeenPromotedBucket=");
         pw.print(standbyBucketToString(mNotificationSeenPromotedBucket));
         pw.println();
+        pw.print("  mTriggerQuotaBumpOnNotificationSeen=");
+        pw.print(mTriggerQuotaBumpOnNotificationSeen);
+        pw.println();
         pw.print("  mSlicePinnedTimeoutMillis=");
         TimeUtils.formatDuration(mSlicePinnedTimeoutMillis, pw);
         pw.println();
@@ -2202,6 +2268,18 @@
         pw.print(ActivityManager.procStateToString(mBroadcastResponseFgThresholdState));
         pw.println();
 
+        pw.print("  mBroadcastSessionsDurationMs=");
+        TimeUtils.formatDuration(mBroadcastSessionsDurationMs, pw);
+        pw.println();
+
+        pw.print("  mBroadcastSessionsWithResponseDurationMs=");
+        TimeUtils.formatDuration(mBroadcastSessionsWithResponseDurationMs, pw);
+        pw.println();
+
+        pw.print("  mNoteResponseEventForAllBroadcastSessions=");
+        pw.print(mNoteResponseEventForAllBroadcastSessions);
+        pw.println();
+
         pw.println();
         pw.print("mAppIdleEnabled="); pw.print(mAppIdleEnabled);
         pw.print(" mAllowRestrictedBucket=");
@@ -2544,6 +2622,10 @@
                     checkIdleStates(UserHandle.USER_ALL);
                     break;
 
+                case MSG_TRIGGER_LISTENER_QUOTA_BUMP:
+                    triggerListenerQuotaBump((String) msg.obj, msg.arg1);
+                    break;
+
                 case MSG_REPORT_CONTENT_PROVIDER_USAGE:
                     ContentProviderUsageRecord record = (ContentProviderUsageRecord) msg.obj;
                     reportContentProviderUsage(record.name, record.packageName, record.userId);
@@ -2630,6 +2712,8 @@
                 "notification_seen_duration";
         private static final String KEY_NOTIFICATION_SEEN_PROMOTED_BUCKET =
                 "notification_seen_promoted_bucket";
+        private static final String KEY_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN =
+                "trigger_quota_bump_on_notification_seen";
         private static final String KEY_SLICE_PINNED_HOLD_DURATION =
                 "slice_pinned_duration";
         private static final String KEY_SYSTEM_UPDATE_HOLD_DURATION =
@@ -2672,6 +2756,13 @@
                 "broadcast_response_window_timeout_ms";
         private static final String KEY_BROADCAST_RESPONSE_FG_THRESHOLD_STATE =
                 "broadcast_response_fg_threshold_state";
+        private static final String KEY_BROADCAST_SESSIONS_DURATION_MS =
+                "broadcast_sessions_duration_ms";
+        private static final String KEY_BROADCAST_SESSIONS_WITH_RESPONSE_DURATION_MS =
+                "broadcast_sessions_with_response_duration_ms";
+        private static final String KEY_NOTE_RESPONSE_EVENT_FOR_ALL_BROADCAST_SESSIONS =
+                "note_response_event_for_all_broadcast_sessions";
+
         public static final long DEFAULT_CHECK_IDLE_INTERVAL_MS =
                 COMPRESS_TIME ? ONE_MINUTE : 4 * ONE_HOUR;
         public static final long DEFAULT_STRONG_USAGE_TIMEOUT =
@@ -2682,6 +2773,7 @@
                 COMPRESS_TIME ? 12 * ONE_MINUTE : 12 * ONE_HOUR;
         public static final int DEFAULT_NOTIFICATION_SEEN_PROMOTED_BUCKET =
                 STANDBY_BUCKET_WORKING_SET;
+        public static final boolean DEFAULT_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN = false;
         public static final long DEFAULT_SYSTEM_UPDATE_TIMEOUT =
                 COMPRESS_TIME ? 2 * ONE_MINUTE : 2 * ONE_HOUR;
         public static final long DEFAULT_SYSTEM_INTERACTION_TIMEOUT =
@@ -2705,6 +2797,12 @@
                 2 * ONE_MINUTE;
         public static final int DEFAULT_BROADCAST_RESPONSE_FG_THRESHOLD_STATE =
                 ActivityManager.PROCESS_STATE_TOP;
+        public static final long DEFAULT_BROADCAST_SESSIONS_DURATION_MS =
+                2 * ONE_MINUTE;
+        public static final long DEFAULT_BROADCAST_SESSIONS_WITH_RESPONSE_DURATION_MS =
+                2 * ONE_MINUTE;
+        public static final boolean DEFAULT_NOTE_RESPONSE_EVENT_FOR_ALL_BROADCAST_SESSIONS =
+                true;
 
         ConstantsObserver(Handler handler) {
             super(handler);
@@ -2776,6 +2874,11 @@
                                     KEY_NOTIFICATION_SEEN_PROMOTED_BUCKET,
                                     DEFAULT_NOTIFICATION_SEEN_PROMOTED_BUCKET);
                             break;
+                        case KEY_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN:
+                            mTriggerQuotaBumpOnNotificationSeen = properties.getBoolean(
+                                    KEY_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN,
+                                    DEFAULT_TRIGGER_QUOTA_BUMP_ON_NOTIFICATION_SEEN);
+                            break;
                         case KEY_SLICE_PINNED_HOLD_DURATION:
                             mSlicePinnedTimeoutMillis = properties.getLong(
                                     KEY_SLICE_PINNED_HOLD_DURATION,
@@ -2832,6 +2935,21 @@
                                     KEY_BROADCAST_RESPONSE_FG_THRESHOLD_STATE,
                                     DEFAULT_BROADCAST_RESPONSE_FG_THRESHOLD_STATE);
                             break;
+                        case KEY_BROADCAST_SESSIONS_DURATION_MS:
+                            mBroadcastSessionsDurationMs = properties.getLong(
+                                    KEY_BROADCAST_SESSIONS_DURATION_MS,
+                                    DEFAULT_BROADCAST_SESSIONS_DURATION_MS);
+                            break;
+                        case KEY_BROADCAST_SESSIONS_WITH_RESPONSE_DURATION_MS:
+                            mBroadcastSessionsWithResponseDurationMs = properties.getLong(
+                                    KEY_BROADCAST_SESSIONS_WITH_RESPONSE_DURATION_MS,
+                                    DEFAULT_BROADCAST_SESSIONS_WITH_RESPONSE_DURATION_MS);
+                            break;
+                        case KEY_NOTE_RESPONSE_EVENT_FOR_ALL_BROADCAST_SESSIONS:
+                            mNoteResponseEventForAllBroadcastSessions = properties.getBoolean(
+                                    KEY_NOTE_RESPONSE_EVENT_FOR_ALL_BROADCAST_SESSIONS,
+                                    DEFAULT_NOTE_RESPONSE_EVENT_FOR_ALL_BROADCAST_SESSIONS);
+                            break;
                         default:
                             if (!timeThresholdsUpdated
                                     && (name.startsWith(KEY_PREFIX_SCREEN_TIME_THRESHOLD)
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 36e1c94..fe99c71 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -320,7 +320,6 @@
     method public void setDemoted(boolean);
     method public void setFgServiceShown(boolean);
     method public void setImportanceLockedByCriticalDeviceFunction(boolean);
-    method public void setImportanceLockedByOEM(boolean);
     method public void setImportantConversation(boolean);
     method public void setOriginalImportance(int);
   }
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index 668dc6b..73678d9 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -98,6 +98,15 @@
         }
     }
 
+    /** Reports the activity starts local relaunch. */
+    public void activityLocalRelaunch(IBinder token) {
+        try {
+            getActivityClientController().activityLocalRelaunch(token);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
     /** Reports the activity has completed relaunched. */
     public void activityRelaunched(IBinder token) {
         try {
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index abd6017..5d1d225 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -4617,8 +4617,8 @@
         try {
             getService().broadcastIntentWithFeature(
                     null, null, intent, null, null, Activity.RESULT_OK, null, null,
-                    null /*requiredPermissions*/, null /*excludedPermissions*/, appOp, null, false,
-                    true, userId);
+                    null /*requiredPermissions*/, null /*excludedPermissions*/,
+                    null /*excludedPackages*/, appOp, null, false, true, userId);
         } catch (RemoteException ex) {
         }
     }
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 49a5c9f..f2ea060 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -568,14 +568,15 @@
     public abstract void unregisterProcessObserver(IProcessObserver processObserver);
 
     /**
-     * Checks if there is an unfinished instrumentation that targets the given uid.
+     * Gets the uid of the instrumentation source if there is an unfinished instrumentation that
+     * targets the given uid.
      *
      * @param uid The uid to be checked for
      *
-     * @return True, if there is an instrumentation whose target application uid matches the given
-     * uid, false otherwise
+     * @return the uid of the instrumentation source, if there is an instrumentation whose target
+     * application uid matches the given uid, and {@link android.os.Process#INVALID_UID} otherwise.
      */
-    public abstract boolean isUidCurrentlyInstrumented(int uid);
+    public abstract int getInstrumentationSourceUid(int uid);
 
     /** Is this a device owner app? */
     public abstract boolean isDeviceOwner(int uid);
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index b4cabad..c6f5920 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -873,6 +873,7 @@
         String processName;
         @UnsupportedAppUsage
         ApplicationInfo appInfo;
+        String sdkSandboxClientAppVolumeUuid;
         String sdkSandboxClientAppPackage;
         @UnsupportedAppUsage
         List<ProviderInfo> providers;
@@ -1119,9 +1120,10 @@
 
         @Override
         public final void bindApplication(String processName, ApplicationInfo appInfo,
-                String sdkSandboxClientAppPackage, ProviderInfoList providerList,
-                ComponentName instrumentationName, ProfilerInfo profilerInfo,
-                Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher,
+                String sdkSandboxClientAppVolumeUuid, String sdkSandboxClientAppPackage,
+                ProviderInfoList providerList, ComponentName instrumentationName,
+                ProfilerInfo profilerInfo, Bundle instrumentationArgs,
+                IInstrumentationWatcher instrumentationWatcher,
                 IUiAutomationConnection instrumentationUiConnection, int debugMode,
                 boolean enableBinderTracking, boolean trackAllocation,
                 boolean isRestrictedBackupMode, boolean persistent, Configuration config,
@@ -1161,6 +1163,7 @@
             AppBindData data = new AppBindData();
             data.processName = processName;
             data.appInfo = appInfo;
+            data.sdkSandboxClientAppVolumeUuid = sdkSandboxClientAppVolumeUuid;
             data.sdkSandboxClientAppPackage = sdkSandboxClientAppPackage;
             data.providers = providerList.getList();
             data.instrumentationName = instrumentationName;
@@ -2561,6 +2564,11 @@
         return getPackageInfo(ai, compatInfo, null, false, true, false);
     }
 
+    private LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo,
+            boolean isSdkSandbox) {
+        return getPackageInfo(ai, compatInfo, null, false, true, false, isSdkSandbox);
+    }
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     public final LoadedApk peekPackageInfo(String packageName, boolean includeCode) {
         synchronized (mResourcesManager) {
@@ -2577,11 +2585,18 @@
     private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
             ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
             boolean registerPackage) {
+        return getPackageInfo(aInfo, compatInfo, baseLoader, securityViolation, includeCode,
+                registerPackage, /*isSdkSandbox=*/false);
+    }
+
+    private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
+            ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
+            boolean registerPackage, boolean isSdkSandbox) {
         final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
         synchronized (mResourcesManager) {
             WeakReference<LoadedApk> ref;
-            if (differentUser) {
-                // Caching not supported across users
+            if (differentUser || isSdkSandbox) {
+                // Caching not supported across users and for sdk sandboxes
                 ref = null;
             } else if (includeCode) {
                 ref = mPackages.get(aInfo.packageName);
@@ -2628,8 +2643,8 @@
                         getSystemContext().mPackageInfo.getClassLoader());
             }
 
-            if (differentUser) {
-                // Caching not supported across users
+            if (differentUser || isSdkSandbox) {
+                // Caching not supported across users and for sdk sandboxes
             } else if (includeCode) {
                 mPackages.put(aInfo.packageName,
                         new WeakReference<LoadedApk>(packageInfo));
@@ -5728,6 +5743,7 @@
             return;
         }
 
+        ActivityClient.getInstance().activityLocalRelaunch(r.token);
         // Initialize a relaunch request.
         final MergedConfiguration mergedConfiguration = new MergedConfiguration(
                 r.createdConfig != null
@@ -6574,9 +6590,11 @@
             mConfigurationController.applyCompatConfiguration();
         }
 
-        data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
-        if (data.sdkSandboxClientAppPackage != null) {
-            data.info.setSdkSandboxStorage(data.sdkSandboxClientAppPackage);
+        final boolean isSdkSandbox = data.sdkSandboxClientAppPackage != null;
+        data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo, isSdkSandbox);
+        if (isSdkSandbox) {
+            data.info.setSdkSandboxStorage(data.sdkSandboxClientAppVolumeUuid,
+                    data.sdkSandboxClientAppPackage);
         }
 
         if (agent != null) {
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 3b1943b..cb64173 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1335,9 +1335,17 @@
     public static final int OP_ACCESS_RESTRICTED_SETTINGS =
             AppProtoEnums.APP_OP_ACCESS_RESTRICTED_SETTINGS;
 
+    /**
+     * Receive microphone audio from an ambient sound detection event
+     *
+     * @hide
+     */
+    public static final int OP_RECEIVE_AMBIENT_TRIGGER_AUDIO =
+            AppProtoEnums.APP_OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 120;
+    public static final int _NUM_OP = 121;
 
     /** Access to coarse location information. */
     public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -1800,6 +1808,14 @@
     public static final String OPSTR_ACCESS_RESTRICTED_SETTINGS =
             "android:access_restricted_settings";
 
+    /**
+     * Receive microphone audio from an ambient sound detection event
+     *
+     * @hide
+     */
+    public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO =
+            "android:receive_ambient_trigger_audio";
+
     /** {@link #sAppOpsToNote} not initialized yet for this op */
     private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
     /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */
@@ -2021,6 +2037,7 @@
             OP_ESTABLISH_VPN_SERVICE,           // OP_ESTABLISH_VPN_SERVICE
             OP_ESTABLISH_VPN_MANAGER,           // OP_ESTABLISH_VPN_MANAGER
             OP_ACCESS_RESTRICTED_SETTINGS,      // OP_ACCESS_RESTRICTED_SETTINGS
+            OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,      // RECEIVE_SOUNDTRIGGER_AUDIO
     };
 
     /**
@@ -2147,6 +2164,7 @@
             OPSTR_ESTABLISH_VPN_SERVICE,
             OPSTR_ESTABLISH_VPN_MANAGER,
             OPSTR_ACCESS_RESTRICTED_SETTINGS,
+            OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO,
     };
 
     /**
@@ -2274,6 +2292,7 @@
             "ESTABLISH_VPN_SERVICE",
             "ESTABLISH_VPN_MANAGER",
             "ACCESS_RESTRICTED_SETTINGS",
+            "RECEIVE_SOUNDTRIGGER_AUDIO",
     };
 
     /**
@@ -2402,6 +2421,7 @@
             null, // no permission for OP_ESTABLISH_VPN_SERVICE
             null, // no permission for OP_ESTABLISH_VPN_MANAGER
             null, // no permission for OP_ACCESS_RESTRICTED_SETTINGS,
+            null, // no permission for OP_RECEIVE_SOUNDTRIGGER_AUDIO
     };
 
     /**
@@ -2529,7 +2549,8 @@
             null, // NEARBY_WIFI_DEVICES
             null, // ESTABLISH_VPN_SERVICE
             null, // ESTABLISH_VPN_MANAGER
-            null, // ACCESS_RESTRICTED_SETTINGS,
+            null, // ACCESS_RESTRICTED_SETTINGS
+            null, // RECEIVE_SOUNDTRIGGER_AUDIO
     };
 
     /**
@@ -2656,7 +2677,8 @@
             null, // NEARBY_WIFI_DEVICES
             null, // ESTABLISH_VPN_SERVICE
             null, // ESTABLISH_VPN_MANAGER
-            null, // ACCESS_RESTRICTED_SETTINGS,
+            null, // ACCESS_RESTRICTED_SETTINGS
+            null, // RECEIVE_SOUNDTRIGGER_AUDIO
     };
 
     /**
@@ -2687,7 +2709,7 @@
             AppOpsManager.MODE_ALLOWED, // READ_ICC_SMS
             AppOpsManager.MODE_ALLOWED, // WRITE_ICC_SMS
             AppOpsManager.MODE_DEFAULT, // WRITE_SETTINGS
-            AppOpsManager.MODE_DEFAULT, // SYSTEM_ALERT_WINDOW /*Overridden in opToDefaultMode()*/
+            getSystemAlertWindowDefault(), // SYSTEM_ALERT_WINDOW
             AppOpsManager.MODE_ALLOWED, // ACCESS_NOTIFICATIONS
             AppOpsManager.MODE_ALLOWED, // CAMERA
             AppOpsManager.MODE_ALLOWED, // RECORD_AUDIO
@@ -2765,7 +2787,7 @@
             AppOpsManager.MODE_ERRORED, // OP_NO_ISOLATED_STORAGE
             AppOpsManager.MODE_ALLOWED, // PHONE_CALL_MICROPHONE
             AppOpsManager.MODE_ALLOWED, // PHONE_CALL_CAMERA
-            AppOpsManager.MODE_ALLOWED, // OP_RECORD_AUDIO_HOTWORD
+            AppOpsManager.MODE_ALLOWED, // RECORD_AUDIO_HOTWORD
             AppOpsManager.MODE_DEFAULT, // MANAGE_ONGOING_CALLS
             AppOpsManager.MODE_DEFAULT, // MANAGE_CREDENTIALS
             AppOpsManager.MODE_DEFAULT, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
@@ -2783,6 +2805,7 @@
             AppOpsManager.MODE_ALLOWED, // ESTABLISH_VPN_SERVICE
             AppOpsManager.MODE_ALLOWED, // ESTABLISH_VPN_MANAGER
             AppOpsManager.MODE_ALLOWED, // ACCESS_RESTRICTED_SETTINGS,
+            AppOpsManager.MODE_ALLOWED, // RECEIVE_SOUNDTRIGGER_AUDIO
     };
 
     /**
@@ -2913,6 +2936,7 @@
             false, // OP_ESTABLISH_VPN_SERVICE
             false, // OP_ESTABLISH_VPN_MANAGER
             true, // ACCESS_RESTRICTED_SETTINGS
+            false, // RECEIVE_SOUNDTRIGGER_AUDIO
     };
 
     /**
@@ -3040,6 +3064,7 @@
             false, // OP_ESTABLISH_VPN_SERVICE
             false, // OP_ESTABLISH_VPN_MANAGER
             true, // ACCESS_RESTRICTED_SETTINGS
+            false, // RECEIVE_SOUNDTRIGGER_AUDIO
     };
 
     /**
@@ -3138,8 +3163,6 @@
     private static final String DEBUG_LOGGING_OPS_PROP = "appops.logging_ops";
     private static final String DEBUG_LOGGING_TAG = "AppOpsManager";
 
-    private static volatile Integer sOpSystemAlertWindowDefaultMode;
-
     /**
      * Retrieve the op switch that controls the given operation.
      * @hide
@@ -3238,9 +3261,6 @@
      * @hide
      */
     public static @Mode int opToDefaultMode(int op) {
-        if (op == OP_SYSTEM_ALERT_WINDOW) {
-            return getSystemAlertWindowDefault();
-        }
         return sOpDefaultMode[op];
     }
 
@@ -10258,11 +10278,6 @@
     }
 
     private static int getSystemAlertWindowDefault() {
-        // This is indeed racy but we aren't expecting the result to change so it's not worth
-        // the synchronization.
-        if (sOpSystemAlertWindowDefaultMode != null) {
-            return sOpSystemAlertWindowDefaultMode;
-        }
         final Context context = ActivityThread.currentApplication();
         if (context == null) {
             return AppOpsManager.MODE_DEFAULT;
@@ -10273,11 +10288,10 @@
         // TVs are constantly plugged in and has less concern for memory/power
         if (ActivityManager.isLowRamDeviceStatic()
                 && !pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK, 0)) {
-            sOpSystemAlertWindowDefaultMode = AppOpsManager.MODE_IGNORED;
-        } else {
-            sOpSystemAlertWindowDefaultMode = AppOpsManager.MODE_DEFAULT;
+            return AppOpsManager.MODE_IGNORED;
         }
-        return sOpSystemAlertWindowDefaultMode;
+
+        return AppOpsManager.MODE_DEFAULT;
     }
 
     /**
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index ac46066..6d982ced 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1193,7 +1193,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/,
-                    AppOpsManager.OP_NONE, null, false, false, getUserId());
+                    null, AppOpsManager.OP_NONE, null, false, false, getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1210,7 +1210,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, receiverPermissions,
-                    null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, false, false,
+                    null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, false, false,
                     getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -1226,7 +1226,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, receiverPermissions,
-                    null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, false, false,
+                    null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, false, false,
                     getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -1243,8 +1243,8 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, receiverPermissions,
-                    null /*excludedPermissions=*/, AppOpsManager.OP_NONE, options, false, false,
-                    getUserId());
+                    null /*excludedPermissions=*/, null /*excludedPackages*/,
+                    AppOpsManager.OP_NONE, options, false, false, getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1259,7 +1259,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, receiverPermissions,
-                    null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, false, false,
+                    null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, false, false,
                     user.getIdentifier());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -1268,7 +1268,7 @@
 
     @Override
     public void sendBroadcastMultiplePermissions(Intent intent, String[] receiverPermissions,
-            String[] excludedPermissions) {
+            String[] excludedPermissions, String[] excludedPackages) {
         warnIfCallingFromSystemProcess();
         String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
         try {
@@ -1276,7 +1276,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, receiverPermissions, excludedPermissions,
-                    AppOpsManager.OP_NONE, null, false, false, getUserId());
+                    excludedPackages, AppOpsManager.OP_NONE, null, false, false, getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1303,7 +1303,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, receiverPermissions,
-                    excludedPermissions, AppOpsManager.OP_NONE, options, false, false,
+                    excludedPermissions, null, AppOpsManager.OP_NONE, options, false, false,
                     getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -1321,7 +1321,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, receiverPermissions,
-                    null /*excludedPermissions=*/, appOp, null, false, false, getUserId());
+                    null /*excludedPermissions=*/, null, appOp, null, false, false, getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1338,7 +1338,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, receiverPermissions,
-                    null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, true, false,
+                    null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, true, false,
                     getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -1402,7 +1402,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     rd, initialCode, initialData, initialExtras, receiverPermissions,
-                    null /*excludedPermissions=*/, appOp, options, true, false, getUserId());
+                    null /*excludedPermissions=*/, null, appOp, options, true, false, getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1416,7 +1416,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/,
-                    AppOpsManager.OP_NONE, null, false, false, user.getIdentifier());
+                    null, AppOpsManager.OP_NONE, null, false, false, user.getIdentifier());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1439,8 +1439,8 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, receiverPermissions,
-                    null /*excludedPermissions=*/, AppOpsManager.OP_NONE, options, false, false,
-                    user.getIdentifier());
+                    null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, options, false,
+                    false, user.getIdentifier());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1457,7 +1457,8 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, receiverPermissions,
-                    null /*excludedPermissions=*/, appOp, null, false, false, user.getIdentifier());
+                    null /*excludedPermissions=*/, null, appOp, null, false, false,
+                    user.getIdentifier());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1508,7 +1509,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     rd, initialCode, initialData, initialExtras, receiverPermissions,
-                    null /*excludedPermissions=*/, appOp, options, true, false,
+                    null /*excludedPermissions=*/, null, appOp, options, true, false,
                     user.getIdentifier());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -1550,7 +1551,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/,
-                    AppOpsManager.OP_NONE, null, false, true, getUserId());
+                    null, AppOpsManager.OP_NONE, null, false, true, getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1589,7 +1590,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/,
-                    AppOpsManager.OP_NONE, options, false, true, getUserId());
+                    null, AppOpsManager.OP_NONE, options, false, true, getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1625,7 +1626,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     rd, initialCode, initialData, initialExtras, null,
-                    null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, true, true,
+                    null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, true, true,
                     getUserId());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -1658,7 +1659,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/,
-                    AppOpsManager.OP_NONE, null, false, true, user.getIdentifier());
+                    null, AppOpsManager.OP_NONE, null, false, true, user.getIdentifier());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1673,7 +1674,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/,
-                    AppOpsManager.OP_NONE, options, false, true, user.getIdentifier());
+                    null, AppOpsManager.OP_NONE, options, false, true, user.getIdentifier());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1708,7 +1709,7 @@
             ActivityManager.getService().broadcastIntentWithFeature(
                     mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
                     rd, initialCode, initialData, initialExtras, null,
-                    null /*excludedPermissions=*/, AppOpsManager.OP_NONE, null, true, true,
+                    null /*excludedPermissions=*/, null, AppOpsManager.OP_NONE, null, true, true,
                     user.getIdentifier());
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 1307161..0138186 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -53,6 +53,7 @@
     oneway void activityStopped(in IBinder token, in Bundle state,
             in PersistableBundle persistentState, in CharSequence description);
     oneway void activityDestroyed(in IBinder token);
+    oneway void activityLocalRelaunch(in IBinder token);
     oneway void activityRelaunched(in IBinder token);
 
     oneway void reportSizeConfigurations(in IBinder token,
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 4efe9df..8367441 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -141,7 +141,7 @@
     int broadcastIntentWithFeature(in IApplicationThread caller, in String callingFeatureId,
             in Intent intent, in String resolvedType, in IIntentReceiver resultTo, int resultCode,
             in String resultData, in Bundle map, in String[] requiredPermissions, in String[] excludePermissions,
-            int appOp, in Bundle options, boolean serialized, boolean sticky, int userId);
+            in String[] excludePackages, int appOp, in Bundle options, boolean serialized, boolean sticky, int userId);
     void unbroadcastIntent(in IApplicationThread caller, in Intent intent, int userId);
     @UnsupportedAppUsage
     oneway void finishReceiver(in IBinder who, int resultCode, in String resultData, in Bundle map,
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index f4fbcce..75ad363 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -72,7 +72,7 @@
     @UnsupportedAppUsage
     void scheduleStopService(IBinder token);
     void bindApplication(in String packageName, in ApplicationInfo info,
-            in String sdkSandboxClientAppPackage,
+            in String sdkSandboxClientAppVolumeUuid, in String sdkSandboxClientAppPackage,
             in ProviderInfoList providerList, in ComponentName testName,
             in ProfilerInfo profilerInfo, in Bundle testArguments,
             IInstrumentationWatcher testWatcher, IUiAutomationConnection uiAutomationConnection,
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index df9f2a3..da6a551 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -77,6 +77,7 @@
     boolean areNotificationsEnabledForPackage(String pkg, int uid);
     boolean areNotificationsEnabled(String pkg);
     int getPackageImportance(String pkg);
+    boolean isImportanceLocked(String pkg, int uid);
 
     List<String> getAllowedAssistantAdjustments(String pkg);
     void allowAssistantAdjustment(String adjustmentType);
diff --git a/core/java/android/app/ITaskStackListener.aidl b/core/java/android/app/ITaskStackListener.aidl
index 7bb5d9a..36e5762 100644
--- a/core/java/android/app/ITaskStackListener.aidl
+++ b/core/java/android/app/ITaskStackListener.aidl
@@ -137,7 +137,7 @@
      * activities inside it belong to a managed profile user, and that user has just
      * been locked.
      */
-    void onTaskProfileLocked(int taskId, int userId);
+    void onTaskProfileLocked(in ActivityManager.RunningTaskInfo taskInfo);
 
     /**
      * Called when a task snapshot got updated.
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 99d7c63..8984c42 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -1058,10 +1058,11 @@
     }
     
     /**
-     * Sends the key events corresponding to the text to the app being
-     * instrumented.
-     * 
-     * @param text The text to be sent. 
+     * Sends the key events that result in the given text being typed into the currently focused
+     * window, and waits for it to be processed.
+     *
+     * @param text The text to be sent.
+     * @see #sendKeySync(KeyEvent)
      */
     public void sendStringSync(String text) {
         if (text == null) {
@@ -1084,11 +1085,12 @@
     }
 
     /**
-     * Send a key event to the currently focused window/view and wait for it to
-     * be processed.  Finished at some point after the recipient has returned
-     * from its event processing, though it may <em>not</em> have completely
-     * finished reacting from the event -- for example, if it needs to update
-     * its display as a result, it may still be in the process of doing that.
+     * Sends a key event to the currently focused window, and waits for it to be processed.
+     * <p>
+     * This method blocks until the recipient has finished handling the event. Note that the
+     * recipient may <em>not</em> have completely finished reacting from the event when this method
+     * returns. For example, it may still be in the process of updating its display or UI contents
+     * upon reacting to the injected event.
      *
      * @param event The event to send to the current focus.
      */
@@ -1116,34 +1118,42 @@
     }
 
     /**
-     * Sends an up and down key event sync to the currently focused window.
+     * Sends up and down key events with the given key code to the currently focused window, and
+     * waits for it to be processed.
      * 
-     * @param key The integer keycode for the event.
+     * @param keyCode The key code for the events to send.
+     * @see #sendKeySync(KeyEvent)
      */
-    public void sendKeyDownUpSync(int key) {        
-        sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, key));
-        sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, key));
-    }
-
-    /**
-     * Higher-level method for sending both the down and up key events for a
-     * particular character key code.  Equivalent to creating both KeyEvent
-     * objects by hand and calling {@link #sendKeySync}.  The event appears
-     * as if it came from keyboard 0, the built in one.
-     * 
-     * @param keyCode The key code of the character to send.
-     */
-    public void sendCharacterSync(int keyCode) {
+    public void sendKeyDownUpSync(int keyCode) {
         sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
         sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
     }
-    
+
     /**
-     * Dispatch a pointer event. Finished at some point after the recipient has
-     * returned from its event processing, though it may <em>not</em> have
-     * completely finished reacting from the event -- for example, if it needs
-     * to update its display as a result, it may still be in the process of
-     * doing that.
+     * Sends up and down key events with the given key code to the currently focused window, and
+     * waits for it to be processed.
+     * <p>
+     * Equivalent to {@link #sendKeyDownUpSync(int)}.
+     *
+     * @param keyCode The key code of the character to send.
+     * @see #sendKeySync(KeyEvent)
+     */
+    public void sendCharacterSync(int keyCode) {
+        sendKeyDownUpSync(keyCode);
+    }
+
+    /**
+     * Dispatches a pointer event into a window owned by the instrumented application, and waits for
+     * it to be processed.
+     * <p>
+     * If the motion event being injected is targeted at a window that is not owned by the
+     * instrumented application, the input injection will fail. See {@link #getUiAutomation()} for
+     * injecting events into all windows.
+     * <p>
+     * This method blocks until the recipient has finished handling the event. Note that the
+     * recipient may <em>not</em> have completely finished reacting from the event when this method
+     * returns. For example, it may still be in the process of updating its display or UI contents
+     * upon reacting to the injected event.
      * 
      * @param event A motion event describing the pointer action.  (As noted in 
      * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use 
@@ -1155,10 +1165,10 @@
             event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
         }
 
-        syncInputTransactionsAndInjectEvent(event);
+        syncInputTransactionsAndInjectEventIntoSelf(event);
     }
 
-    private void syncInputTransactionsAndInjectEvent(MotionEvent event) {
+    private void syncInputTransactionsAndInjectEventIntoSelf(MotionEvent event) {
         final boolean syncBefore = event.getAction() == MotionEvent.ACTION_DOWN
                 || event.isFromSource(InputDevice.SOURCE_MOUSE);
         final boolean syncAfter = event.getAction() == MotionEvent.ACTION_UP;
@@ -1169,8 +1179,9 @@
                         .syncInputTransactions(true /*waitForAnimations*/);
             }
 
+            // Direct the injected event into windows owned by the instrumentation target.
             InputManager.getInstance().injectInputEvent(
-                    event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
+                    event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH, Process.myUid());
 
             if (syncAfter) {
                 WindowManagerGlobal.getWindowManagerService()
@@ -1182,19 +1193,21 @@
     }
 
     /**
-     * Dispatch a trackball event. Finished at some point after the recipient has
-     * returned from its event processing, though it may <em>not</em> have
-     * completely finished reacting from the event -- for example, if it needs
-     * to update its display as a result, it may still be in the process of
-     * doing that.
-     * 
+     * Dispatches a trackball event into the currently focused window, and waits for it to be
+     * processed.
+     * <p>
+     * This method blocks until the recipient has finished handling the event. Note that the
+     * recipient may <em>not</em> have completely finished reacting from the event when this method
+     * returns. For example, it may still be in the process of updating its display or UI contents
+     * upon reacting to the injected event.
+     *
      * @param event A motion event describing the trackball action.  (As noted in 
      * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use 
      * {@link SystemClock#uptimeMillis()} as the timebase.
      */
     public void sendTrackballEventSync(MotionEvent event) {
         validateNotAppThread();
-        if ((event.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) == 0) {
+        if (!event.isFromSource(InputDevice.SOURCE_CLASS_TRACKBALL)) {
             event.setSource(InputDevice.SOURCE_TRACKBALL);
         }
         InputManager.getInstance().injectInputEvent(event,
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index deefea8..0849a6f 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -400,7 +400,8 @@
         mLibDir = aInfo.nativeLibraryDir;
         mDataDirFile = FileUtils.newFileOrNull(aInfo.dataDir);
         mDeviceProtectedDataDirFile = FileUtils.newFileOrNull(aInfo.deviceProtectedDataDir);
-        mCredentialProtectedDataDirFile = FileUtils.newFileOrNull(aInfo.credentialProtectedDataDir);
+        mCredentialProtectedDataDirFile = FileUtils.newFileOrNull(
+                aInfo.credentialProtectedDataDir);
 
         mSplitNames = aInfo.splitNames;
         mSplitAppDirs = aInfo.splitSourceDirs;
@@ -412,14 +413,16 @@
         }
     }
 
-    /** @hide */
-    void setSdkSandboxStorage(String sdkSandboxClientAppPackage) {
+    void setSdkSandboxStorage(@Nullable String sdkSandboxClientAppVolumeUuid,
+            String sdkSandboxClientAppPackage) {
         int userId = UserHandle.myUserId();
         mDeviceProtectedDataDirFile = Environment
-                .getDataMiscDeSharedSdkSandboxDirectory(userId, sdkSandboxClientAppPackage)
+                .getDataMiscDeSharedSdkSandboxDirectory(sdkSandboxClientAppVolumeUuid, userId,
+                        sdkSandboxClientAppPackage)
                 .getAbsoluteFile();
         mCredentialProtectedDataDirFile = Environment
-                .getDataMiscCeSharedSdkSandboxDirectory(userId, sdkSandboxClientAppPackage)
+                .getDataMiscCeSharedSdkSandboxDirectory(sdkSandboxClientAppVolumeUuid, userId,
+                        sdkSandboxClientAppPackage)
                 .getAbsoluteFile();
 
         if ((mApplicationInfo.privateFlags
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 6f0b03a..c9cc1a1 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -253,7 +253,6 @@
     // If this is a blockable system notification channel.
     private boolean mBlockableSystem = false;
     private int mAllowBubbles = DEFAULT_ALLOW_BUBBLE;
-    private boolean mImportanceLockedByOEM;
     private boolean mImportanceLockedDefaultApp;
     private String mParentId = null;
     private String mConversationId = null;
@@ -322,13 +321,13 @@
         mLightColor = in.readInt();
         mBlockableSystem = in.readBoolean();
         mAllowBubbles = in.readInt();
-        mImportanceLockedByOEM = in.readBoolean();
         mOriginalImportance = in.readInt();
         mParentId = in.readString();
         mConversationId = in.readString();
         mDemoted = in.readBoolean();
         mImportantConvo = in.readBoolean();
         mDeletedTime = in.readLong();
+        mImportanceLockedDefaultApp = in.readBoolean();
     }
 
     @Override
@@ -382,13 +381,13 @@
         dest.writeInt(mLightColor);
         dest.writeBoolean(mBlockableSystem);
         dest.writeInt(mAllowBubbles);
-        dest.writeBoolean(mImportanceLockedByOEM);
         dest.writeInt(mOriginalImportance);
         dest.writeString(mParentId);
         dest.writeString(mConversationId);
         dest.writeBoolean(mDemoted);
         dest.writeBoolean(mImportantConvo);
         dest.writeLong(mDeletedTime);
+        dest.writeBoolean(mImportanceLockedDefaultApp);
     }
 
     /**
@@ -851,14 +850,6 @@
      * @hide
      */
     @TestApi
-    public void setImportanceLockedByOEM(boolean locked) {
-        mImportanceLockedByOEM = locked;
-    }
-
-    /**
-     * @hide
-     */
-    @TestApi
     public void setImportanceLockedByCriticalDeviceFunction(boolean locked) {
         mImportanceLockedDefaultApp = locked;
     }
@@ -1107,8 +1098,8 @@
             out.attributeBoolean(null, ATT_IMP_CONVERSATION, isImportantConversation());
         }
 
-        // mImportanceLockedDefaultApp and mImportanceLockedByOEM have a different source of
-        // truth and so aren't written to this xml file
+        // mImportanceLockedDefaultApp has a different source of truth and so isn't written to
+        // this xml file
 
         out.endTag(null, TAG_CHANNEL);
     }
@@ -1251,7 +1242,6 @@
                 && Arrays.equals(mVibration, that.mVibration)
                 && Objects.equals(getGroup(), that.getGroup())
                 && Objects.equals(getAudioAttributes(), that.getAudioAttributes())
-                && mImportanceLockedByOEM == that.mImportanceLockedByOEM
                 && mImportanceLockedDefaultApp == that.mImportanceLockedDefaultApp
                 && mOriginalImportance == that.mOriginalImportance
                 && Objects.equals(getParentChannelId(), that.getParentChannelId())
@@ -1267,7 +1257,7 @@
                 getUserLockedFields(),
                 isFgServiceShown(), mVibrationEnabled, mShowBadge, isDeleted(), getDeletedTimeMs(),
                 getGroup(), getAudioAttributes(), isBlockable(), mAllowBubbles,
-                mImportanceLockedByOEM, mImportanceLockedDefaultApp, mOriginalImportance,
+                mImportanceLockedDefaultApp, mOriginalImportance,
                 mParentId, mConversationId, mDemoted, mImportantConvo);
         result = 31 * result + Arrays.hashCode(mVibration);
         return result;
@@ -1312,7 +1302,6 @@
                 + ", mAudioAttributes=" + mAudioAttributes
                 + ", mBlockableSystem=" + mBlockableSystem
                 + ", mAllowBubbles=" + mAllowBubbles
-                + ", mImportanceLockedByOEM=" + mImportanceLockedByOEM
                 + ", mImportanceLockedDefaultApp=" + mImportanceLockedDefaultApp
                 + ", mOriginalImp=" + mOriginalImportance
                 + ", mParent=" + mParentId
diff --git a/core/java/android/app/TaskStackListener.java b/core/java/android/app/TaskStackListener.java
index 83fe29f..774bc06 100644
--- a/core/java/android/app/TaskStackListener.java
+++ b/core/java/android/app/TaskStackListener.java
@@ -155,7 +155,7 @@
 
     @Override
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public void onTaskProfileLocked(int taskId, int userId) throws RemoteException {
+    public void onTaskProfileLocked(RunningTaskInfo taskInfo) throws RemoteException {
     }
 
     @Override
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 0a2b421..93f91e5 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -11210,7 +11210,9 @@
      * for enterprise use.
      *
      * An example of a supported preferential network service is the Enterprise
-     * slice on 5G networks.
+     * slice on 5G networks. For devices on 4G networks, the profile owner needs to additionally
+     * configure enterprise APN to set up data call for the preferential network service.
+     * These APNs can be added using {@link #addOverrideApn}.
      *
      * By default, preferential network service is disabled on the work profile and
      * fully managed devices, on supported carriers and devices.
@@ -11260,7 +11262,9 @@
      * {@see PreferentialNetworkServiceConfig}
      *
      * An example of a supported preferential network service is the Enterprise
-     * slice on 5G networks.
+     * slice on 5G networks. For devices on 4G networks, the profile owner needs to additionally
+     * configure enterprise APN to set up data call for the preferential network service.
+     * These APNs can be added using {@link #addOverrideApn}.
      *
      * By default, preferential network service is disabled on the work profile and fully managed
      * devices, on supported carriers and devices. Admins can explicitly enable it with this API.
@@ -11409,7 +11413,9 @@
 
     /**
      * Called by a device owner or a profile owner of an organization-owned managed profile to
-     * control whether the user can change networks configured by the admin.
+     * control whether the user can change networks configured by the admin. When this lockdown is
+     * enabled, the user can still configure and connect to other Wi-Fi networks, or use other Wi-Fi
+     * capabilities such as tethering.
      * <p>
      * WiFi network configuration lockdown is controlled by a global settings
      * {@link android.provider.Settings.Global#WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN} and calling
@@ -13782,18 +13788,13 @@
     }
 
     /**
-     * Called by device owner or profile owner to add an override APN.
+     * Called by device owner or managed profile owner to add an override APN.
      *
      * <p>This method may returns {@code -1} if {@code apnSetting} conflicts with an existing
      * override APN. Update the existing conflicted APN with
      * {@link #updateOverrideApn(ComponentName, int, ApnSetting)} instead of adding a new entry.
      * <p>Two override APNs are considered to conflict when all the following APIs return
      * the same values on both override APNs:
-     * <p> Before Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}:
-     * Only device owners can add APNs.
-     * <p> Starting from Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}:
-     * Device and profile owners can add enterprise APNs
-     * ({@link ApnSetting#TYPE_ENTERPRISE}), while only device owners can add other type of APNs.
      * <ul>
      *   <li>{@link ApnSetting#getOperatorNumeric()}</li>
      *   <li>{@link ApnSetting#getApnName()}</li>
@@ -13808,6 +13809,15 @@
      *   <li>{@link ApnSetting#getRoamingProtocol()}</li>
      * </ul>
      *
+     * <p> Before Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}:
+     * Only device owners can add APNs.
+     * <p> Starting from Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}:
+     * Both device owners and managed profile owners can add enterprise APNs
+     * ({@link ApnSetting#TYPE_ENTERPRISE}), while only device owners can add other type of APNs.
+     * Enterprise APNs are specific to the managed profile and do not override any user-configured
+     * VPNs. They are prerequisites for enabling preferential network service on the managed
+     * profile on 4G networks ({@link #setPreferentialNetworkServiceConfigs}).
+     *
      * @param admin which {@link DeviceAdminReceiver} this request is associated with
      * @param apnSetting the override APN to insert
      * @return The {@code id} of inserted override APN. Or {@code -1} when failed to insert into
@@ -13830,7 +13840,7 @@
     }
 
     /**
-     * Called by device owner or profile owner to update an override APN.
+     * Called by device owner or managed profile owner to update an override APN.
      *
      * <p>This method may returns {@code false} if there is no override APN with the given
      * {@code apnId}.
@@ -13840,7 +13850,7 @@
      * <p> Before Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}:
      * Only device owners can update APNs.
      * <p> Starting from Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}:
-     * Device and profile owners can update enterprise APNs
+     * Both device owners and managed profile owners can update enterprise APNs
      * ({@link ApnSetting#TYPE_ENTERPRISE}), while only device owners can update other type of APNs.
      *
      * @param admin which {@link DeviceAdminReceiver} this request is associated with
@@ -13867,14 +13877,14 @@
     }
 
     /**
-     * Called by device owner or profile owner to remove an override APN.
+     * Called by device owner or managed profile owner to remove an override APN.
      *
      * <p>This method may returns {@code false} if there is no override APN with the given
      * {@code apnId}.
      * <p> Before Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}:
      * Only device owners can remove APNs.
      * <p> Starting from Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}:
-     * Device and profile owners can remove enterprise APNs
+     * Both device owners and managed profile owners can remove enterprise APNs
      * ({@link ApnSetting#TYPE_ENTERPRISE}), while only device owners can remove other type of APNs.
      *
      * @param admin which {@link DeviceAdminReceiver} this request is associated with
@@ -13899,7 +13909,8 @@
     }
 
     /**
-     * Called by device owner to get all override APNs inserted by device owner.
+     * Called by device owner or managed profile owner to get all override APNs inserted by
+     * device owner or managed profile owner previously using {@link #addOverrideApn}.
      *
      * @param admin which {@link DeviceAdminReceiver} this request is associated with
      * @return A list of override APNs inserted by device owner.
@@ -13924,6 +13935,9 @@
      * <p> Override APNs are separated from other APNs on the device, and can only be inserted or
      * modified by the device owner. When enabled, only override APNs are in use, any other APNs
      * are ignored.
+     * <p>Note: Enterprise APNs added by managed profile owners do not need to be enabled by
+     * this API. They are part of the preferential network service config and is controlled by
+     * {@link #setPreferentialNetworkServiceConfigs}.
      *
      * @param admin which {@link DeviceAdminReceiver} this request is associated with
      * @param enabled {@code true} if override APNs should be enabled, {@code false} otherwise
@@ -14569,12 +14583,13 @@
     }
 
     /**
-     * Called by Device owner to disable user control over apps. User will not be able to clear
-     * app data or force-stop packages.
+     * Called by a device owner or a profile owner to disable user control over apps. User will not
+     * be able to clear app data or force-stop packages. When called by a device owner, applies to
+     * all users on the device.
      *
      * @param admin which {@link DeviceAdminReceiver} this request is associated with
      * @param packages The package names for the apps.
-     * @throws SecurityException if {@code admin} is not a device owner.
+     * @throws SecurityException if {@code admin} is not a device owner or a profile owner.
      */
     public void setUserControlDisabledPackages(@NonNull ComponentName admin,
             @NonNull List<String> packages) {
@@ -14589,12 +14604,14 @@
     }
 
     /**
-     * Returns the list of packages over which user control is disabled by the device owner.
+     * Returns the list of packages over which user control is disabled by a device or profile
+     * owner.
      *
      * @param admin which {@link DeviceAdminReceiver} this request is associated with
-     * @throws SecurityException if {@code admin} is not a device owner.
+     * @throws SecurityException if {@code admin} is not a device or profile owner.
      */
-    public @NonNull List<String> getUserControlDisabledPackages(@NonNull ComponentName admin) {
+    @NonNull
+    public List<String> getUserControlDisabledPackages(@NonNull ComponentName admin) {
         throwIfParentInstance("getUserControlDisabledPackages");
         if (mService != null) {
             try {
diff --git a/core/java/android/app/admin/DevicePolicyResources.java b/core/java/android/app/admin/DevicePolicyResources.java
index c8033fa..11b840f 100644
--- a/core/java/android/app/admin/DevicePolicyResources.java
+++ b/core/java/android/app/admin/DevicePolicyResources.java
@@ -184,6 +184,12 @@
                     PREFIX + "WORK_PROFILE_IT_ADMIN_CANT_RESET_SCREEN_LOCK";
 
             /**
+             * Text shown on the CTA link shown to user to set a separate lock for work apps
+             */
+            public static final String WORK_PROFILE_IT_ADMIN_CANT_RESET_SCREEN_LOCK_ACTION =
+                    PREFIX + "WORK_PROFILE_IT_ADMIN_CANT_RESET_SCREEN_LOCK_ACTION";
+
+            /**
              * Message shown in screen lock picker for setting up a work profile screen lock
              */
             public static final String WORK_PROFILE_SCREEN_LOCK_SETUP_MESSAGE =
@@ -1492,6 +1498,45 @@
              * Content description for the work profile lock screen.
              */
             public static final String WORK_LOCK_ACCESSIBILITY = PREFIX + "WORK_LOCK_ACCESSIBILITY";
+
+            /**
+             * Notification text displayed when screenshots are blocked by an IT admin.
+             */
+            public static final String SCREENSHOT_BLOCKED_BY_ADMIN =
+                    PREFIX + "SCREENSHOT_BLOCKED_BY_ADMIN";
+
+            /**
+             * Message shown when user is almost at the limit of password attempts where the
+             * profile will be removed. Accepts number of failed attempts and remaining failed
+             * attempts as params.
+             */
+            public static final String KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE =
+                    PREFIX + "KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE";
+
+            /**
+             * Message shown in dialog when user has exceeded the maximum attempts and the profile
+             * will be removed. Accepts number of failed attempts as a param.
+             */
+            public static final String KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE =
+                    PREFIX + "KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE";
+
+            /**
+             * Monitoring dialog subtitle for the section describing VPN.
+             */
+            public static final String QS_DIALOG_MONITORING_VPN_SUBTITLE =
+                    PREFIX + "QS_DIALOG_MONITORING_VPN_SUBTITLE";
+
+            /**
+             * Monitoring dialog subtitle for the section describing network logging.
+             */
+            public static final String QS_DIALOG_MONITORING_NETWORK_SUBTITLE =
+                    PREFIX + "QS_DIALOG_MONITORING_NETWORK_SUBTITLE";
+
+            /**
+             * Monitoring dialog subtitle for the section describing certificate authorities.
+             */
+            public static final String QS_DIALOG_MONITORING_CA_CERT_SUBTITLE =
+                    PREFIX + "QS_DIALOG_MONITORING_CA_CERT_SUBTITLE";
         }
 
         /**
diff --git a/core/java/android/app/admin/DevicePolicyResourcesManager.java b/core/java/android/app/admin/DevicePolicyResourcesManager.java
index e8eb792..2cc189f 100644
--- a/core/java/android/app/admin/DevicePolicyResourcesManager.java
+++ b/core/java/android/app/admin/DevicePolicyResourcesManager.java
@@ -26,6 +26,7 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.os.RemoteException;
+import android.provider.DeviceConfig;
 import android.util.DisplayMetrics;
 import android.util.Log;
 
@@ -40,6 +41,9 @@
 public class DevicePolicyResourcesManager {
     private static String TAG = "DevicePolicyResourcesManager";
 
+    private static String DISABLE_RESOURCES_UPDATABILITY_FLAG = "disable_resources_updatability";
+    private static boolean DEFAULT_DISABLE_RESOURCES_UPDATABILITY = false;
+
     private final Context mContext;
     private final IDevicePolicyManager mService;
 
@@ -194,16 +198,20 @@
         Objects.requireNonNull(drawableSource, "drawableSource can't be null");
         Objects.requireNonNull(defaultDrawableLoader, "defaultDrawableLoader can't be null");
 
-        if (drawableId.equals(DevicePolicyResources.UNDEFINED)) {
+        if (drawableId.equals(DevicePolicyResources.UNDEFINED)
+                || DeviceConfig.getBoolean(
+                        DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER,
+                        DISABLE_RESOURCES_UPDATABILITY_FLAG,
+                        DEFAULT_DISABLE_RESOURCES_UPDATABILITY)) {
             return ParcelableResource.loadDefaultDrawable(defaultDrawableLoader);
         }
+
         if (mService != null) {
             try {
                 ParcelableResource resource = mService.getDrawable(
                         drawableId, drawableStyle, drawableSource);
                 if (resource == null) {
-                    return ParcelableResource.loadDefaultDrawable(
-                            defaultDrawableLoader);
+                    return ParcelableResource.loadDefaultDrawable(defaultDrawableLoader);
                 }
                 return resource.getDrawable(
                         mContext,
@@ -287,16 +295,20 @@
         Objects.requireNonNull(drawableSource, "drawableSource can't be null");
         Objects.requireNonNull(defaultDrawableLoader, "defaultDrawableLoader can't be null");
 
-        if (drawableId.equals(DevicePolicyResources.UNDEFINED)) {
+        if (drawableId.equals(DevicePolicyResources.UNDEFINED)
+                || DeviceConfig.getBoolean(
+                        DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER,
+                        DISABLE_RESOURCES_UPDATABILITY_FLAG,
+                        DEFAULT_DISABLE_RESOURCES_UPDATABILITY)) {
             return ParcelableResource.loadDefaultDrawable(defaultDrawableLoader);
         }
+
         if (mService != null) {
             try {
                 ParcelableResource resource = mService.getDrawable(
                         drawableId, drawableStyle, drawableSource);
                 if (resource == null) {
-                    return ParcelableResource.loadDefaultDrawable(
-                            defaultDrawableLoader);
+                    return ParcelableResource.loadDefaultDrawable(defaultDrawableLoader);
                 }
                 return resource.getDrawable(mContext, density, defaultDrawableLoader);
             } catch (RemoteException e) {
@@ -330,9 +342,14 @@
         Objects.requireNonNull(drawableSource, "drawableSource can't be null");
         Objects.requireNonNull(defaultIcon, "defaultIcon can't be null");
 
-        if (drawableId.equals(DevicePolicyResources.UNDEFINED)) {
+        if (drawableId.equals(DevicePolicyResources.UNDEFINED)
+                || DeviceConfig.getBoolean(
+                        DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER,
+                        DISABLE_RESOURCES_UPDATABILITY_FLAG,
+                        DEFAULT_DISABLE_RESOURCES_UPDATABILITY)) {
             return defaultIcon;
         }
+
         if (mService != null) {
             try {
                 ParcelableResource resource = mService.getDrawable(
@@ -463,7 +480,10 @@
         Objects.requireNonNull(stringId, "stringId can't be null");
         Objects.requireNonNull(defaultStringLoader, "defaultStringLoader can't be null");
 
-        if (stringId.equals(DevicePolicyResources.UNDEFINED)) {
+        if (stringId.equals(DevicePolicyResources.UNDEFINED) || DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER,
+                DISABLE_RESOURCES_UPDATABILITY_FLAG,
+                DEFAULT_DISABLE_RESOURCES_UPDATABILITY)) {
             return ParcelableResource.loadDefaultString(defaultStringLoader);
         }
         if (mService != null) {
@@ -508,7 +528,10 @@
         Objects.requireNonNull(stringId, "stringId can't be null");
         Objects.requireNonNull(defaultStringLoader, "defaultStringLoader can't be null");
 
-        if (stringId.equals(DevicePolicyResources.UNDEFINED)) {
+        if (stringId.equals(DevicePolicyResources.UNDEFINED) || DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_DEVICE_POLICY_MANAGER,
+                DISABLE_RESOURCES_UPDATABILITY_FLAG,
+                DEFAULT_DISABLE_RESOURCES_UPDATABILITY)) {
             return ParcelableResource.loadDefaultString(defaultStringLoader);
         }
         if (mService != null) {
diff --git a/core/java/android/app/backup/BackupTransport.java b/core/java/android/app/backup/BackupTransport.java
index 9bb048d..f6de72b 100644
--- a/core/java/android/app/backup/BackupTransport.java
+++ b/core/java/android/app/backup/BackupTransport.java
@@ -665,184 +665,292 @@
 
         @Override
         public void name(AndroidFuture<String> resultFuture) throws RemoteException {
-            String result = BackupTransport.this.name();
-            resultFuture.complete(result);
+            try {
+                String result = BackupTransport.this.name();
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void configurationIntent(AndroidFuture<Intent> resultFuture)
                 throws RemoteException {
-            Intent result = BackupTransport.this.configurationIntent();
-            resultFuture.complete(result);
+            try {
+                Intent result = BackupTransport.this.configurationIntent();
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void currentDestinationString(AndroidFuture<String> resultFuture)
                 throws RemoteException {
-            String result = BackupTransport.this.currentDestinationString();
-            resultFuture.complete(result);
+            try {
+                String result = BackupTransport.this.currentDestinationString();
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void dataManagementIntent(AndroidFuture<Intent> resultFuture)
                 throws RemoteException {
-            Intent result = BackupTransport.this.dataManagementIntent();
-            resultFuture.complete(result);
+            try {
+                Intent result = BackupTransport.this.dataManagementIntent();
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void dataManagementIntentLabel(AndroidFuture<CharSequence> resultFuture)
                 throws RemoteException {
-            CharSequence result = BackupTransport.this.dataManagementIntentLabel();
-            resultFuture.complete(result);
+            try {
+                CharSequence result = BackupTransport.this.dataManagementIntentLabel();
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void transportDirName(AndroidFuture<String> resultFuture) throws RemoteException {
-            String result = BackupTransport.this.transportDirName();
-            resultFuture.complete(result);
+            try {
+                String result = BackupTransport.this.transportDirName();
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void requestBackupTime(AndroidFuture<Long> resultFuture) throws RemoteException {
-            long result = BackupTransport.this.requestBackupTime();
-            resultFuture.complete(result);
+            try {
+                long result = BackupTransport.this.requestBackupTime();
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void initializeDevice(ITransportStatusCallback callback) throws RemoteException {
-            int result = BackupTransport.this.initializeDevice();
-            callback.onOperationCompleteWithStatus(result);
+            try {
+                int result = BackupTransport.this.initializeDevice();
+                callback.onOperationCompleteWithStatus(result);
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd, int flags,
                 ITransportStatusCallback callback) throws RemoteException {
-            int result = BackupTransport.this.performBackup(packageInfo, inFd, flags);
-            callback.onOperationCompleteWithStatus(result);
+            try {
+                int result = BackupTransport.this.performBackup(packageInfo, inFd, flags);
+                callback.onOperationCompleteWithStatus(result);
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void clearBackupData(PackageInfo packageInfo, ITransportStatusCallback callback)
                 throws RemoteException {
-            int result = BackupTransport.this.clearBackupData(packageInfo);
-            callback.onOperationCompleteWithStatus(result);
+            try {
+                int result = BackupTransport.this.clearBackupData(packageInfo);
+                callback.onOperationCompleteWithStatus(result);
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void finishBackup(ITransportStatusCallback callback) throws RemoteException {
-            int result = BackupTransport.this.finishBackup();
-            callback.onOperationCompleteWithStatus(result);
+            try {
+                int result = BackupTransport.this.finishBackup();
+                callback.onOperationCompleteWithStatus(result);
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void getAvailableRestoreSets(AndroidFuture<List<RestoreSet>> resultFuture)
                 throws RemoteException {
-            RestoreSet[] result = BackupTransport.this.getAvailableRestoreSets();
-            resultFuture.complete(Arrays.asList(result));
+            try {
+                RestoreSet[] result = BackupTransport.this.getAvailableRestoreSets();
+                resultFuture.complete(Arrays.asList(result));
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void getCurrentRestoreSet(AndroidFuture<Long> resultFuture)
                 throws RemoteException {
-            long result = BackupTransport.this.getCurrentRestoreSet();
-            resultFuture.complete(result);
+            try {
+                long result = BackupTransport.this.getCurrentRestoreSet();
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void startRestore(long token, PackageInfo[] packages,
                 ITransportStatusCallback callback)  throws RemoteException {
-            int result = BackupTransport.this.startRestore(token, packages);
-            callback.onOperationCompleteWithStatus(result);
+            try {
+                int result = BackupTransport.this.startRestore(token, packages);
+                callback.onOperationCompleteWithStatus(result);
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void nextRestorePackage(AndroidFuture<RestoreDescription> resultFuture)
                 throws RemoteException {
-            RestoreDescription result = BackupTransport.this.nextRestorePackage();
-            resultFuture.complete(result);
+            try {
+                RestoreDescription result = BackupTransport.this.nextRestorePackage();
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void getRestoreData(ParcelFileDescriptor outFd,
                 ITransportStatusCallback callback) throws RemoteException {
-            int result = BackupTransport.this.getRestoreData(outFd);
-            callback.onOperationCompleteWithStatus(result);
+            try {
+                int result = BackupTransport.this.getRestoreData(outFd);
+                callback.onOperationCompleteWithStatus(result);
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void finishRestore(ITransportStatusCallback callback)
                 throws RemoteException {
-            BackupTransport.this.finishRestore();
-            callback.onOperationComplete();
+            try {
+                BackupTransport.this.finishRestore();
+                callback.onOperationComplete();
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void requestFullBackupTime(AndroidFuture<Long> resultFuture)
                 throws RemoteException {
-            long result = BackupTransport.this.requestFullBackupTime();
-            resultFuture.complete(result);
+            try {
+                long result = BackupTransport.this.requestFullBackupTime();
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket,
                 int flags, ITransportStatusCallback callback) throws RemoteException {
-            int result = BackupTransport.this.performFullBackup(targetPackage, socket, flags);
-            callback.onOperationCompleteWithStatus(result);
+            try {
+                int result = BackupTransport.this.performFullBackup(targetPackage, socket, flags);
+                callback.onOperationCompleteWithStatus(result);
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void checkFullBackupSize(long size, ITransportStatusCallback callback)
                 throws RemoteException {
-            int result = BackupTransport.this.checkFullBackupSize(size);
-            callback.onOperationCompleteWithStatus(result);
+            try {
+                int result = BackupTransport.this.checkFullBackupSize(size);
+                callback.onOperationCompleteWithStatus(result);
+            } catch (RuntimeException e) {
+            callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void sendBackupData(int numBytes, ITransportStatusCallback callback)
                 throws RemoteException {
-            int result = BackupTransport.this.sendBackupData(numBytes);
-            callback.onOperationCompleteWithStatus(result);
+            try {
+                int result = BackupTransport.this.sendBackupData(numBytes);
+                callback.onOperationCompleteWithStatus(result);
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void cancelFullBackup(ITransportStatusCallback callback) throws RemoteException {
-            BackupTransport.this.cancelFullBackup();
-            callback.onOperationComplete();
+            try {
+                BackupTransport.this.cancelFullBackup();
+                callback.onOperationComplete();
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void isAppEligibleForBackup(PackageInfo targetPackage, boolean isFullBackup,
                 AndroidFuture<Boolean> resultFuture) throws RemoteException {
-            boolean result = BackupTransport.this.isAppEligibleForBackup(targetPackage,
-                    isFullBackup);
-            resultFuture.complete(result);
+            try {
+                boolean result = BackupTransport.this.isAppEligibleForBackup(targetPackage,
+                        isFullBackup);
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void getBackupQuota(String packageName, boolean isFullBackup,
                 AndroidFuture<Long> resultFuture) throws RemoteException {
-            long result = BackupTransport.this.getBackupQuota(packageName, isFullBackup);
-            resultFuture.complete(result);
+            try {
+                long result = BackupTransport.this.getBackupQuota(packageName, isFullBackup);
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void getTransportFlags(AndroidFuture<Integer> resultFuture) throws RemoteException {
-            int result = BackupTransport.this.getTransportFlags();
-            resultFuture.complete(result);
+            try {
+                int result = BackupTransport.this.getTransportFlags();
+                resultFuture.complete(result);
+            } catch (RuntimeException e) {
+                resultFuture.cancel(/* mayInterruptIfRunning */ true);
+            }
         }
 
         @Override
         public void getNextFullRestoreDataChunk(ParcelFileDescriptor socket,
                 ITransportStatusCallback callback) throws RemoteException {
-            int result = BackupTransport.this.getNextFullRestoreDataChunk(socket);
-            callback.onOperationCompleteWithStatus(result);
+            try {
+                int result = BackupTransport.this.getNextFullRestoreDataChunk(socket);
+                callback.onOperationCompleteWithStatus(result);
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
 
         @Override
         public void abortFullRestore(ITransportStatusCallback callback) throws RemoteException {
-            int result = BackupTransport.this.abortFullRestore();
-            callback.onOperationCompleteWithStatus(result);
+            try {
+                int result = BackupTransport.this.abortFullRestore();
+                callback.onOperationCompleteWithStatus(result);
+            } catch (RuntimeException e) {
+                callback.onOperationCompleteWithStatus(BackupTransport.TRANSPORT_ERROR);
+            }
         }
     }
 }
diff --git a/core/java/android/app/smartspace/SmartspaceTarget.java b/core/java/android/app/smartspace/SmartspaceTarget.java
index c5982fc..79d7b21 100644
--- a/core/java/android/app/smartspace/SmartspaceTarget.java
+++ b/core/java/android/app/smartspace/SmartspaceTarget.java
@@ -174,7 +174,7 @@
     public static final int FEATURE_MEDIA_HEADS_UP = 36;
     public static final int FEATURE_STEP_COUNTING = 37;
     public static final int FEATURE_EARTHQUAKE_ALERT = 38;
-    public static final int FEATURE_STEP_DATE = 39;
+    public static final int FEATURE_STEP_DATE = 39; // This represents a DATE. "STEP" is a typo.
     public static final int FEATURE_BLAZE_BUILD_PROGRESS = 40;
     public static final int FEATURE_EARTHQUAKE_OCCURRED = 41;
 
diff --git a/core/java/android/app/usage/OWNERS b/core/java/android/app/usage/OWNERS
index 9668f80..a4bf985 100644
--- a/core/java/android/app/usage/OWNERS
+++ b/core/java/android/app/usage/OWNERS
@@ -5,3 +5,4 @@
 varunshah@google.com
 
 per-file *StorageStats* = file:/core/java/android/os/storage/OWNERS
+per-file *Broadcast* = sudheersai@google.com
diff --git a/core/java/android/companion/virtual/OWNERS b/core/java/android/companion/virtual/OWNERS
new file mode 100644
index 0000000..2968104
--- /dev/null
+++ b/core/java/android/companion/virtual/OWNERS
@@ -0,0 +1 @@
+include /services/companion/java/com/android/server/companion/virtual/OWNERS
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 907db7d..836bff5 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -2269,6 +2269,19 @@
      */
     public void sendBroadcastMultiplePermissions(@NonNull Intent intent,
             @NonNull String[] receiverPermissions, @Nullable String[] excludedPermissions) {
+        sendBroadcastMultiplePermissions(intent, receiverPermissions, excludedPermissions, null);
+    }
+
+
+    /**
+     * Like {@link #sendBroadcastMultiplePermissions(Intent, String[], String[])}, but also allows
+     * specification of a list of excluded packages.
+     *
+     * @hide
+     */
+    public void sendBroadcastMultiplePermissions(@NonNull Intent intent,
+            @NonNull String[] receiverPermissions, @Nullable String[] excludedPermissions,
+            @Nullable String[] excludedPackages) {
         throw new RuntimeException("Not implemented. Must override in a subclass.");
     }
 
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 4ecd776..e654918 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -515,8 +515,10 @@
     /** @hide */
     @Override
     public void sendBroadcastMultiplePermissions(@NonNull Intent intent,
-            @NonNull String[] receiverPermissions, @Nullable String[] excludedPermissions) {
-        mBase.sendBroadcastMultiplePermissions(intent, receiverPermissions, excludedPermissions);
+            @NonNull String[] receiverPermissions, @Nullable String[] excludedPermissions,
+            @Nullable String[] excludedPackages) {
+        mBase.sendBroadcastMultiplePermissions(intent, receiverPermissions, excludedPermissions,
+                excludedPackages);
     }
 
     /** @hide */
diff --git a/core/java/android/content/pm/AppSearchShortcutInfo.java b/core/java/android/content/pm/AppSearchShortcutInfo.java
index 1b84686b..fb41b89 100644
--- a/core/java/android/content/pm/AppSearchShortcutInfo.java
+++ b/core/java/android/content/pm/AppSearchShortcutInfo.java
@@ -413,7 +413,10 @@
         final int iconResId = (int) getPropertyLong(KEY_ICON_RES_ID);
         final String iconResName = getPropertyString(KEY_ICON_RES_NAME);
         final String iconUri = getPropertyString(KEY_ICON_URI);
-        final int disabledReason = Integer.parseInt(getPropertyString(KEY_DISABLED_REASON));
+        final String disabledReasonString = getPropertyString(KEY_DISABLED_REASON);
+        final int disabledReason = !TextUtils.isEmpty(disabledReasonString)
+                ? Integer.parseInt(getPropertyString(KEY_DISABLED_REASON))
+                : ShortcutInfo.DISABLED_REASON_NOT_DISABLED;
         final Map<String, Map<String, List<String>>> capabilityBindings =
                 parseCapabilityBindings(getPropertyStringArray(KEY_CAPABILITY_BINDINGS));
         return new ShortcutInfo(
diff --git a/core/java/android/content/res/AssetFileDescriptor.java b/core/java/android/content/res/AssetFileDescriptor.java
index dd00c3a..ac65933 100644
--- a/core/java/android/content/res/AssetFileDescriptor.java
+++ b/core/java/android/content/res/AssetFileDescriptor.java
@@ -21,8 +21,6 @@
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
-import android.system.ErrnoException;
-import android.system.Os;
 
 import java.io.Closeable;
 import java.io.FileDescriptor;
@@ -54,11 +52,11 @@
     /**
      * Create a new AssetFileDescriptor from the given values.
      *
-     * @param fd          The underlying file descriptor.
+     * @param fd The underlying file descriptor.
      * @param startOffset The location within the file that the asset starts.
-     *                    This must be 0 if length is UNKNOWN_LENGTH.
-     * @param length      The number of bytes of the asset, or
-     *                    {@link #UNKNOWN_LENGTH} if it extends to the end of the file.
+     *            This must be 0 if length is UNKNOWN_LENGTH.
+     * @param length The number of bytes of the asset, or
+     *            {@link #UNKNOWN_LENGTH} if it extends to the end of the file.
      */
     public AssetFileDescriptor(ParcelFileDescriptor fd, long startOffset,
             long length) {
@@ -68,13 +66,13 @@
     /**
      * Create a new AssetFileDescriptor from the given values.
      *
-     * @param fd          The underlying file descriptor.
+     * @param fd The underlying file descriptor.
      * @param startOffset The location within the file that the asset starts.
-     *                    This must be 0 if length is UNKNOWN_LENGTH.
-     * @param length      The number of bytes of the asset, or
-     *                    {@link #UNKNOWN_LENGTH} if it extends to the end of the file.
-     * @param extras      additional details that can be used to interpret the
-     *                    underlying file descriptor. May be null.
+     *            This must be 0 if length is UNKNOWN_LENGTH.
+     * @param length The number of bytes of the asset, or
+     *            {@link #UNKNOWN_LENGTH} if it extends to the end of the file.
+     * @param extras additional details that can be used to interpret the
+     *            underlying file descriptor. May be null.
      */
     public AssetFileDescriptor(ParcelFileDescriptor fd, long startOffset,
             long length, Bundle extras) {
@@ -205,24 +203,19 @@
      */
     public static class AutoCloseInputStream
             extends ParcelFileDescriptor.AutoCloseInputStream {
-        private final long mSizeFromStartOffset;
-        private final long mStartOffset;
-        private long mPosFromStartOffset;
+        private long mRemaining;
 
         public AutoCloseInputStream(AssetFileDescriptor fd) throws IOException {
             super(fd.getParcelFileDescriptor());
-            // this skip is necessary if getChannel() is called
             super.skip(fd.getStartOffset());
-            mSizeFromStartOffset = fd.getLength();
-            mStartOffset = fd.getStartOffset();
+            mRemaining = (int) fd.getLength();
         }
 
         @Override
         public int available() throws IOException {
-            long available = mSizeFromStartOffset - mPosFromStartOffset;
-            return available >= 0
-                    ? (available < 0x7fffffff ? (int) available : 0x7fffffff)
-                    : 0;
+            return mRemaining >= 0
+                    ? (mRemaining < 0x7fffffff ? (int) mRemaining : 0x7fffffff)
+                    : super.available();
         }
 
         @Override
@@ -234,24 +227,15 @@
 
         @Override
         public int read(byte[] buffer, int offset, int count) throws IOException {
-            int available = available();
-
-            if (available <= 0) {
-                return -1;
-            } else {
-                if (count > available) count = available;
-                try {
-                    int res = Os.pread(getFD(), buffer, offset, count,
-                            mStartOffset + mPosFromStartOffset);
-                    // pread returns 0 at end of file, while java's InputStream interface
-                    // requires -1
-                    if (res == 0) res = -1;
-                    if (res >= 0) mPosFromStartOffset += res;
-                    return res;
-                } catch (ErrnoException e) {
-                    throw new IOException(e);
-                }
+            if (mRemaining >= 0) {
+                if (mRemaining == 0) return -1;
+                if (count > mRemaining) count = (int) mRemaining;
+                int res = super.read(buffer, offset, count);
+                if (res >= 0) mRemaining -= res;
+                return res;
             }
+
+            return super.read(buffer, offset, count);
         }
 
         @Override
@@ -261,31 +245,41 @@
 
         @Override
         public long skip(long count) throws IOException {
-            int available = available();
-            if (available <= 0) {
-                return -1;
-            } else {
-                if (count > available) count = available;
-                mPosFromStartOffset += count;
-                return count;
+            if (mRemaining >= 0) {
+                if (mRemaining == 0) return -1;
+                if (count > mRemaining) count = mRemaining;
+                long res = super.skip(count);
+                if (res >= 0) mRemaining -= res;
+                return res;
             }
+
+            return super.skip(count);
         }
 
         @Override
         public void mark(int readlimit) {
-            // Not supported.
-            return;
+            if (mRemaining >= 0) {
+                // Not supported.
+                return;
+            }
+            super.mark(readlimit);
         }
 
         @Override
         public boolean markSupported() {
-            return false;
+            if (mRemaining >= 0) {
+                return false;
+            }
+            return super.markSupported();
         }
 
         @Override
         public synchronized void reset() throws IOException {
-            // Not supported.
-            return;
+            if (mRemaining >= 0) {
+                // Not supported.
+                return;
+            }
+            super.reset();
         }
     }
 
@@ -381,7 +375,6 @@
         public AssetFileDescriptor createFromParcel(Parcel in) {
             return new AssetFileDescriptor(in);
         }
-
         public AssetFileDescriptor[] newArray(int size) {
             return new AssetFileDescriptor[size];
         }
diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java
index 0d3aaf5..10c3730 100644
--- a/core/java/android/hardware/Sensor.java
+++ b/core/java/android/hardware/Sensor.java
@@ -712,14 +712,16 @@
     public static final String STRING_TYPE_HINGE_ANGLE = "android.sensor.hinge_angle";
 
     /**
-     * A constant describing a head tracker sensor.
+     * A constant describing a head tracker sensor. Note that this sensor type is typically not
+     * available for apps to use.
      *
      * See {@link android.hardware.SensorEvent#values SensorEvent.values} for more details.
      */
     public static final int TYPE_HEAD_TRACKER = 37;
 
     /**
-     * A constant string describing a head tracker sensor.
+     * A constant string describing a head tracker sensor. Note that this sensor type is typically
+     * not available for apps to use.
      *
      * See {@link android.hardware.SensorEvent#values SensorEvent.values} for more details.
      */
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index bb0caa7..4a25177 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -166,7 +166,16 @@
      * {@link #TIMESTAMP_BASE_MONOTONIC}, which is roughly the same time base as
      * {@link android.os.SystemClock#uptimeMillis}.</li>
      * <li> For all other cases, the timestamp base is {@link #TIMESTAMP_BASE_SENSOR}, the same
-     * as what's specified by {@link CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE}.</li>
+     * as what's specified by {@link CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE}.
+     * <ul><li> For a SurfaceTexture output surface, the camera system re-spaces the delivery
+     * of output frames based on image readout intervals, reducing viewfinder jitter. The timestamps
+     * of images remain to be {@link #TIMESTAMP_BASE_SENSOR}.</li></ul></li>
+     *
+     * <p>Note that the reduction of frame jitter for SurfaceView and SurfaceTexture comes with
+     * slight increase in photon-to-photon latency, which is the time from when photons hit the
+     * scene to when the corresponding pixels show up on the screen. If the photon-to-photon latency
+     * is more important than the smoothness of viewfinder, {@link #TIMESTAMP_BASE_SENSOR} should be
+     * used instead.</p>
      *
      * @see #TIMESTAMP_BASE_CHOREOGRAPHER_SYNCED
      * @see #TIMESTAMP_BASE_MONOTONIC
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index e1ffd4a..2da12e6c 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -57,11 +57,16 @@
     // Temporarily changes the pointer speed.
     void tryPointerSpeed(int speed);
 
-    // Injects an input event into the system.  To inject into windows owned by other
-    // applications, the caller must have the INJECT_EVENTS permission.
+    // Injects an input event into the system. The caller must have the INJECT_EVENTS permssion.
+    // This method exists only for compatibility purposes and may be removed in a future release.
     @UnsupportedAppUsage
     boolean injectInputEvent(in InputEvent ev, int mode);
 
+    // Injects an input event into the system. The caller must have the INJECT_EVENTS permission.
+    // The caller can target windows owned by a certain UID by providing a valid UID, or by
+    // providing {@link android.os.Process#INVALID_UID} to target all windows.
+    boolean injectInputEventToTarget(in InputEvent ev, int mode, int targetUid);
+
     VerifiedInputEvent verifyInputEvent(in InputEvent ev);
 
     // Calibrate input device position
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index cc5b275..d17a952 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -45,6 +45,7 @@
 import android.os.InputEventInjectionSync;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
@@ -1107,14 +1108,58 @@
         }
     }
 
+    /**
+     * Injects an input event into the event system, targeting windows owned by the provided uid.
+     *
+     * If a valid targetUid is provided, the system will only consider injecting the input event
+     * into windows owned by the provided uid. If the input event is targeted at a window that is
+     * not owned by the provided uid, input injection will fail and a RemoteException will be
+     * thrown.
+     *
+     * The synchronization mode determines whether the method blocks while waiting for
+     * input injection to proceed.
+     * <p>
+     * Requires the {@link android.Manifest.permission.INJECT_EVENTS} permission.
+     * </p><p>
+     * Make sure you correctly set the event time and input source of the event
+     * before calling this method.
+     * </p>
+     *
+     * @param event The event to inject.
+     * @param mode The synchronization mode.  One of:
+     * {@link android.os.InputEventInjectionSync.NONE},
+     * {@link android.os.InputEventInjectionSync.WAIT_FOR_RESULT}, or
+     * {@link android.os.InputEventInjectionSync.WAIT_FOR_FINISHED}.
+     * @param targetUid The uid to target, or {@link android.os.Process#INVALID_UID} to target all
+     *                 windows.
+     * @return True if input event injection succeeded.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.INJECT_EVENTS)
+    public boolean injectInputEvent(InputEvent event, int mode, int targetUid) {
+        if (event == null) {
+            throw new IllegalArgumentException("event must not be null");
+        }
+        if (mode != InputEventInjectionSync.NONE
+                && mode != InputEventInjectionSync.WAIT_FOR_FINISHED
+                && mode != InputEventInjectionSync.WAIT_FOR_RESULT) {
+            throw new IllegalArgumentException("mode is invalid");
+        }
+
+        try {
+            return mIm.injectInputEventToTarget(event, mode, targetUid);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
 
     /**
      * Injects an input event into the event system on behalf of an application.
      * The synchronization mode determines whether the method blocks while waiting for
      * input injection to proceed.
      * <p>
-     * Requires {@link android.Manifest.permission.INJECT_EVENTS} to inject into
-     * windows that are owned by other applications.
+     * Requires the {@link android.Manifest.permission.INJECT_EVENTS} permission.
      * </p><p>
      * Make sure you correctly set the event time and input source of the event
      * before calling this method.
@@ -1129,22 +1174,10 @@
      *
      * @hide
      */
+    @RequiresPermission(Manifest.permission.INJECT_EVENTS)
     @UnsupportedAppUsage
     public boolean injectInputEvent(InputEvent event, int mode) {
-        if (event == null) {
-            throw new IllegalArgumentException("event must not be null");
-        }
-        if (mode != InputEventInjectionSync.NONE
-                && mode != InputEventInjectionSync.WAIT_FOR_FINISHED
-                && mode != InputEventInjectionSync.WAIT_FOR_RESULT) {
-            throw new IllegalArgumentException("mode is invalid");
-        }
-
-        try {
-            return mIm.injectInputEvent(event, mode);
-        } catch (RemoteException ex) {
-            throw ex.rethrowFromSystemServer();
-        }
+        return injectInputEvent(event, mode, Process.INVALID_UID);
     }
 
     /**
diff --git a/core/java/android/hardware/input/TEST_MAPPING b/core/java/android/hardware/input/TEST_MAPPING
new file mode 100644
index 0000000..9626d8d
--- /dev/null
+++ b/core/java/android/hardware/input/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "imports": [
+    {
+      "path": "frameworks/native/services/inputflinger"
+    }
+  ]
+}
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 25296bc..21ecf8b 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -592,7 +592,7 @@
 
     private InlineSuggestionSessionController mInlineSuggestionSessionController;
 
-    private boolean mAutomotiveHideNavBarForKeyboard;
+    private boolean mHideNavBarForKeyboard;
     private boolean mIsAutomotive;
     private @NonNull OptionalInt mHandwritingRequestId = OptionalInt.empty();
     private InputEventReceiver mHandwritingEventReceiver;
@@ -1498,9 +1498,8 @@
         // shown the first time (cold start).
         mSettingsObserver.shouldShowImeWithHardKeyboard();
 
-        mIsAutomotive = isAutomotive();
-        mAutomotiveHideNavBarForKeyboard = getApplicationContext().getResources().getBoolean(
-                com.android.internal.R.bool.config_automotiveHideNavBarForKeyboard);
+        mHideNavBarForKeyboard = getApplicationContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_hideNavBarForKeyboard);
 
         // TODO(b/111364446) Need to address context lifecycle issue if need to re-create
         // for update resources & configuration correctly when show soft input
@@ -1539,11 +1538,11 @@
             window.setFlags(windowFlags, windowFlagsMask);
 
             // Automotive devices may request the navigation bar to be hidden when the IME shows up
-            // (controlled via config_automotiveHideNavBarForKeyboard) in order to maximize the
-            // visible screen real estate. When this happens, the IME window should animate from the
+            // (controlled via config_hideNavBarForKeyboard) in order to maximize the visible
+            // screen real estate. When this happens, the IME window should animate from the
             // bottom of the screen to reduce the jank that happens from the lack of synchronization
             // between the bottom system window and the IME window.
-            if (mIsAutomotive && mAutomotiveHideNavBarForKeyboard) {
+            if (mHideNavBarForKeyboard) {
                 window.setDecorFitsSystemWindows(false);
             }
         }
@@ -1625,7 +1624,6 @@
             // when IME developers are doing something unsupported.
             InputMethodPrivilegedOperationsRegistry.remove(mToken);
         }
-        unregisterCompatOnBackInvokedCallback();
         mImeDispatcher = null;
     }
 
@@ -2788,6 +2786,11 @@
         if (mInkWindow != null) {
             finishStylusHandwriting();
         }
+        // Back callback is typically unregistered in {@link #hideWindow()}, but it's possible
+        // for {@link #doFinishInput()} to be called without {@link #hideWindow()} so we also
+        // unregister here.
+        // TODO(b/232341407): Add CTS to verify back behavior after screen on / off.
+        unregisterCompatOnBackInvokedCallback();
     }
 
     void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) {
diff --git a/core/java/android/net/IVpnManager.aidl b/core/java/android/net/IVpnManager.aidl
index b4647ca..f302378 100644
--- a/core/java/android/net/IVpnManager.aidl
+++ b/core/java/android/net/IVpnManager.aidl
@@ -42,6 +42,8 @@
     String startVpnProfile(String packageName);
     void stopVpnProfile(String packageName);
     VpnProfileState getProvisionedVpnProfileState(String packageName);
+    boolean setAppExclusionList(int userId, String vpnPackage, in List<String> excludedApps);
+    List<String> getAppExclusionList(int userId, String vpnPackage);
 
     /** Always-on VPN APIs */
     boolean isAlwaysOnVpnPackageSupported(int userId, String packageName);
diff --git a/core/java/android/net/VpnManager.java b/core/java/android/net/VpnManager.java
index ae7d91f..f62d7c4 100644
--- a/core/java/android/net/VpnManager.java
+++ b/core/java/android/net/VpnManager.java
@@ -187,14 +187,24 @@
     /**
      * The network that was underlying the VPN when the event occurred, as a {@link Network}.
      *
-     * This extra will be null if there was no underlying network at the time of the event.
+     * <p>This extra will be null if there was no underlying network at the time of the event, or
+     *    the underlying network has no bearing on the event, as in the case of:
+     * <ul>
+     *   <li>CATEGORY_EVENT_DEACTIVATED_BY_USER
+     *   <li>CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED
+     * </ul>
      */
     public static final String EXTRA_UNDERLYING_NETWORK = "android.net.extra.UNDERLYING_NETWORK";
 
     /**
      * The {@link NetworkCapabilities} of the underlying network when the event occurred.
      *
-     * This extra will be null if there was no underlying network at the time of the event.
+     * <p>This extra will be null if there was no underlying network at the time of the event, or
+     *    the underlying network has no bearing on the event, as in the case of:
+     * <ul>
+     *   <li>CATEGORY_EVENT_DEACTIVATED_BY_USER
+     *   <li>CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED
+     * </ul>
      */
     public static final String EXTRA_UNDERLYING_NETWORK_CAPABILITIES =
             "android.net.extra.UNDERLYING_NETWORK_CAPABILITIES";
@@ -202,7 +212,12 @@
     /**
      * The {@link LinkProperties} of the underlying network when the event occurred.
      *
-     * This extra will be null if there was no underlying network at the time of the event.
+     * <p>This extra will be null if there was no underlying network at the time of the event, or
+     *    the underlying network has no bearing on the event, as in the case of:
+     * <ul>
+     *   <li>CATEGORY_EVENT_DEACTIVATED_BY_USER
+     *   <li>CATEGORY_EVENT_ALWAYS_ON_STATE_CHANGED
+     * </ul>
      */
     public static final String EXTRA_UNDERLYING_LINK_PROPERTIES =
             "android.net.extra.UNDERLYING_LINK_PROPERTIES";
@@ -580,6 +595,63 @@
     }
 
     /**
+     * Sets the application exclusion list for the specified VPN profile.
+     *
+     * <p>If an app in the set of excluded apps is not installed for the given user, it will be
+     * skipped in the list of app exclusions. If apps are installed or removed, any active VPN will
+     * have its UID set updated automatically. If the caller is not {@code userId},
+     * {@link android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission is required.
+     *
+     * <p>This will ONLY affect VpnManager profiles. As such, the NETWORK_SETTINGS provider MUST NOT
+     * allow configuration of these options if the application has not provided a VPN profile.
+     *
+     * @param userId the identifier of the user to set app exclusion list
+     * @param vpnPackage The package name for an installed VPN app on the device
+     * @param excludedApps the app exclusion list
+     * @throws IllegalStateException exception if vpn for the @code userId} is not ready yet.
+     *
+     * @return whether setting the list is successful or not
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK})
+    public boolean setAppExclusionList(int userId, @NonNull String vpnPackage,
+            @NonNull List<String> excludedApps) {
+        try {
+            return mService.setAppExclusionList(userId, vpnPackage, excludedApps);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Gets the application exclusion list for the specified VPN profile. If the caller is not
+     * {@code userId}, {@link android.Manifest.permission.INTERACT_ACROSS_USERS_FULL} permission
+     * is required.
+     *
+     * @param userId the identifier of the user to set app exclusion list
+     * @param vpnPackage The package name for an installed VPN app on the device
+     * @return the list of packages for the specified VPN profile or null if no corresponding VPN
+     *         profile configured.
+     *
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK})
+    @Nullable
+    public List<String> getAppExclusionList(int userId, @NonNull String vpnPackage) {
+        try {
+            return mService.getAppExclusionList(userId, vpnPackage);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * @return the list of packages that are allowed to access network when always-on VPN is in
      * lockdown mode but not connected. Returns {@code null} when VPN lockdown is not active.
      *
diff --git a/core/java/android/net/VpnProfileState.java b/core/java/android/net/VpnProfileState.java
index c69ea1a..552a2c1 100644
--- a/core/java/android/net/VpnProfileState.java
+++ b/core/java/android/net/VpnProfileState.java
@@ -24,6 +24,8 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+import java.util.StringJoiner;
 
 /**
  * Describe the state of VPN.
@@ -150,4 +152,44 @@
         mAlwaysOn = in.readBoolean();
         mLockdown = in.readBoolean();
     }
+
+    private String convertStateToString(@State int state) {
+        switch (state) {
+            case STATE_CONNECTED:
+                return "CONNECTED";
+            case STATE_CONNECTING:
+                return "CONNECTING";
+            case STATE_DISCONNECTED:
+                return "DISCONNECTED";
+            case STATE_FAILED:
+                return "FAILED";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    @Override
+    public String toString() {
+        final StringJoiner resultJoiner = new StringJoiner(", ", "{", "}");
+        resultJoiner.add("State: " + convertStateToString(getState()));
+        resultJoiner.add("SessionId: " + getSessionId());
+        resultJoiner.add("Always-on: " + isAlwaysOn());
+        resultJoiner.add("Lockdown: " + isLockdownEnabled());
+        return resultJoiner.toString();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof VpnProfileState)) return false;
+        final VpnProfileState that = (VpnProfileState) obj;
+        return (getState() == that.getState()
+                && Objects.equals(getSessionId(), that.getSessionId())
+                && isAlwaysOn() == that.isAlwaysOn()
+                && isLockdownEnabled() == that.isLockdownEnabled());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getState(), getSessionId(), isAlwaysOn(), isLockdownEnabled());
+    }
 }
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
index 022d213..5177cb4 100644
--- a/core/java/android/os/Environment.java
+++ b/core/java/android/os/Environment.java
@@ -478,8 +478,15 @@
     }
 
     /** {@hide} */
-    public static File getDataMiscCeSharedSdkSandboxDirectory(int userId, String packageName) {
-        return buildPath(getDataMiscCeDirectory(userId), "sdksandbox", packageName, "shared");
+    private static File getDataMiscCeDirectory(String volumeUuid, int userId) {
+        return buildPath(getDataDirectory(volumeUuid), "misc_ce", String.valueOf(userId));
+    }
+
+    /** {@hide} */
+    public static File getDataMiscCeSharedSdkSandboxDirectory(String volumeUuid, int userId,
+            String packageName) {
+        return buildPath(getDataMiscCeDirectory(volumeUuid, userId), "sdksandbox",
+                packageName, "shared");
     }
 
     /** {@hide} */
@@ -488,8 +495,15 @@
     }
 
     /** {@hide} */
-    public static File getDataMiscDeSharedSdkSandboxDirectory(int userId, String packageName) {
-        return buildPath(getDataMiscDeDirectory(userId), "sdksandbox", packageName, "shared");
+    private static File getDataMiscDeDirectory(String volumeUuid, int userId) {
+        return buildPath(getDataDirectory(volumeUuid), "misc_de", String.valueOf(userId));
+    }
+
+    /** {@hide} */
+    public static File getDataMiscDeSharedSdkSandboxDirectory(String volumeUuid, int userId,
+            String packageName) {
+        return buildPath(getDataMiscDeDirectory(volumeUuid, userId), "sdksandbox",
+                packageName, "shared");
     }
 
     private static File getDataProfilesDeDirectory(int userId) {
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 8f2d218..13ca2c3 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -463,21 +463,22 @@
     /**
      * @hide
      */
-    public static String sleepReasonToString(int sleepReason) {
+    public static String sleepReasonToString(@GoToSleepReason int sleepReason) {
         switch (sleepReason) {
+            case GO_TO_SLEEP_REASON_ACCESSIBILITY: return "accessibility";
             case GO_TO_SLEEP_REASON_APPLICATION: return "application";
             case GO_TO_SLEEP_REASON_DEVICE_ADMIN: return "device_admin";
-            case GO_TO_SLEEP_REASON_TIMEOUT: return "timeout";
-            case GO_TO_SLEEP_REASON_LID_SWITCH: return "lid_switch";
-            case GO_TO_SLEEP_REASON_POWER_BUTTON: return "power_button";
-            case GO_TO_SLEEP_REASON_HDMI: return "hdmi";
-            case GO_TO_SLEEP_REASON_SLEEP_BUTTON: return "sleep_button";
-            case GO_TO_SLEEP_REASON_ACCESSIBILITY: return "accessibility";
-            case GO_TO_SLEEP_REASON_FORCE_SUSPEND: return "force_suspend";
-            case GO_TO_SLEEP_REASON_INATTENTIVE: return "inattentive";
+            case GO_TO_SLEEP_REASON_DEVICE_FOLD: return "device_folded";
             case GO_TO_SLEEP_REASON_DISPLAY_GROUP_REMOVED: return "display_group_removed";
             case GO_TO_SLEEP_REASON_DISPLAY_GROUPS_TURNED_OFF: return "display_groups_turned_off";
-            case GO_TO_SLEEP_REASON_DEVICE_FOLD: return "device_folded";
+            case GO_TO_SLEEP_REASON_FORCE_SUSPEND: return "force_suspend";
+            case GO_TO_SLEEP_REASON_HDMI: return "hdmi";
+            case GO_TO_SLEEP_REASON_INATTENTIVE: return "inattentive";
+            case GO_TO_SLEEP_REASON_LID_SWITCH: return "lid_switch";
+            case GO_TO_SLEEP_REASON_POWER_BUTTON: return "power_button";
+            case GO_TO_SLEEP_REASON_QUIESCENT: return "quiescent";
+            case GO_TO_SLEEP_REASON_SLEEP_BUTTON: return "sleep_button";
+            case GO_TO_SLEEP_REASON_TIMEOUT: return "timeout";
             default: return Integer.toString(sleepReason);
         }
     }
@@ -576,18 +577,20 @@
      * @hide
      */
     @IntDef(prefix = { "GO_TO_SLEEP_REASON_" }, value = {
+            GO_TO_SLEEP_REASON_ACCESSIBILITY,
             GO_TO_SLEEP_REASON_APPLICATION,
             GO_TO_SLEEP_REASON_DEVICE_ADMIN,
-            GO_TO_SLEEP_REASON_TIMEOUT,
+            GO_TO_SLEEP_REASON_DEVICE_FOLD,
+            GO_TO_SLEEP_REASON_DISPLAY_GROUP_REMOVED,
+            GO_TO_SLEEP_REASON_DISPLAY_GROUPS_TURNED_OFF,
+            GO_TO_SLEEP_REASON_FORCE_SUSPEND,
+            GO_TO_SLEEP_REASON_HDMI,
+            GO_TO_SLEEP_REASON_INATTENTIVE,
             GO_TO_SLEEP_REASON_LID_SWITCH,
             GO_TO_SLEEP_REASON_POWER_BUTTON,
-            GO_TO_SLEEP_REASON_HDMI,
-            GO_TO_SLEEP_REASON_SLEEP_BUTTON,
-            GO_TO_SLEEP_REASON_ACCESSIBILITY,
-            GO_TO_SLEEP_REASON_FORCE_SUSPEND,
-            GO_TO_SLEEP_REASON_INATTENTIVE,
             GO_TO_SLEEP_REASON_QUIESCENT,
-            GO_TO_SLEEP_REASON_DEVICE_FOLD
+            GO_TO_SLEEP_REASON_SLEEP_BUTTON,
+            GO_TO_SLEEP_REASON_TIMEOUT,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface GoToSleepReason{}
@@ -704,6 +707,8 @@
     }
 
     /**
+     * Information related to the device waking up, triggered by {@link #wakeUp}.
+     *
      * @hide
      */
     public static class WakeData {
@@ -712,9 +717,9 @@
             this.wakeReason = wakeReason;
             this.sleepDuration = sleepDuration;
         }
-        public long wakeTime;
-        public @WakeReason int wakeReason;
-        public long sleepDuration;
+        public final long wakeTime;
+        public final @WakeReason int wakeReason;
+        public final long sleepDuration;
 
         @Override
         public boolean equals(@Nullable Object o) {
@@ -733,6 +738,35 @@
     }
 
     /**
+     * Information related to the device going to sleep, triggered by {@link #goToSleep}.
+     *
+     * @hide
+     */
+    public static class SleepData {
+        public SleepData(long goToSleepUptimeMillis, @GoToSleepReason int goToSleepReason) {
+            this.goToSleepUptimeMillis = goToSleepUptimeMillis;
+            this.goToSleepReason = goToSleepReason;
+        }
+        public final long goToSleepUptimeMillis;
+        public final @GoToSleepReason int goToSleepReason;
+
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (o instanceof SleepData) {
+                final SleepData other = (SleepData) o;
+                return goToSleepUptimeMillis == other.goToSleepUptimeMillis
+                        && goToSleepReason == other.goToSleepReason;
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(goToSleepUptimeMillis, goToSleepReason);
+        }
+    }
+
+    /**
      * The value to pass as the 'reason' argument to reboot() to reboot into
      * recovery mode for tasks other than applying system updates, such as
      * doing factory resets.
@@ -2644,6 +2678,7 @@
      *
      * @hide
      */
+    @GoToSleepReason
     public int getLastSleepReason() {
         try {
             return mService.getLastSleepReason();
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index ec4d3b6..5ca0da2 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -330,6 +330,9 @@
     /** Returns information about the last wakeup event. */
     public abstract PowerManager.WakeData getLastWakeup();
 
+    /** Returns information about the last event to go to sleep. */
+    public abstract PowerManager.SleepData getLastGoToSleep();
+
     /** Allows power button to intercept a power key button press. */
     public abstract boolean interceptPowerKeyDown(KeyEvent event);
 }
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 196f2f9..0ffdfc6 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -257,7 +257,8 @@
     public static final String DISALLOW_MODIFY_ACCOUNTS = "no_modify_accounts";
 
     /**
-     * Specifies if a user is disallowed from changing Wi-Fi access points via Settings.
+     * Specifies if a user is disallowed from changing Wi-Fi access points via Settings. This
+     * restriction does not affect Wi-Fi tethering settings.
      *
      * <p>A device owner and a profile owner can set this restriction, although the restriction has
      * no effect in a managed profile. When it is set by a device owner, a profile owner on the
@@ -295,6 +296,9 @@
     /**
      * Specifies if a user is disallowed from using Wi-Fi tethering.
      *
+     * <p>This restriction does not limit the user's ability to modify or connect to regular
+     * Wi-Fi networks, which is separately controlled by {@link #DISALLOW_CONFIG_WIFI}.
+     *
      * <p>This restriction can only be set by a device owner,
      * a profile owner of an organization-owned managed profile on the parent profile.
      * When it is set by any of these owners, it prevents all users from using
diff --git a/core/java/android/permission/PermissionControllerManager.java b/core/java/android/permission/PermissionControllerManager.java
index 3c2c7f0..b494c7f 100644
--- a/core/java/android/permission/PermissionControllerManager.java
+++ b/core/java/android/permission/PermissionControllerManager.java
@@ -860,7 +860,7 @@
                     Binder.restoreCallingIdentity(token);
                 }
             }
-        });
+        }, executor);
     }
 
     /**
diff --git a/core/java/android/permission/PermissionUsageHelper.java b/core/java/android/permission/PermissionUsageHelper.java
index 4ed939c..f5f1c37 100644
--- a/core/java/android/permission/PermissionUsageHelper.java
+++ b/core/java/android/permission/PermissionUsageHelper.java
@@ -30,6 +30,7 @@
 import static android.app.AppOpsManager.OPSTR_FINE_LOCATION;
 import static android.app.AppOpsManager.OPSTR_PHONE_CALL_CAMERA;
 import static android.app.AppOpsManager.OPSTR_PHONE_CALL_MICROPHONE;
+import static android.app.AppOpsManager.OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO;
 import static android.app.AppOpsManager.OPSTR_RECORD_AUDIO;
 import static android.app.AppOpsManager.OP_CAMERA;
 import static android.app.AppOpsManager.OP_FLAGS_ALL_TRUSTED;
@@ -137,6 +138,7 @@
 
     private static final List<String> MIC_OPS = List.of(
             OPSTR_PHONE_CALL_MICROPHONE,
+            OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO,
             OPSTR_RECORD_AUDIO
     );
 
@@ -147,6 +149,7 @@
 
     private static @NonNull String getGroupForOp(String op) {
         switch (op) {
+            case OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO:
             case OPSTR_RECORD_AUDIO:
                 return MICROPHONE;
             case OPSTR_CAMERA:
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index 9a2f7ba..13a3ec8 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -607,6 +607,14 @@
     public static final String NAMESPACE_VOICE_INTERACTION = "voice_interaction";
 
     /**
+     * Namespace for DevicePolicyManager related features.
+     *
+     * @hide
+     */
+    public static final String NAMESPACE_DEVICE_POLICY_MANAGER =
+            "device_policy_manager";
+
+    /**
      * List of namespaces which can be read without READ_DEVICE_CONFIG permission
      *
      * @hide
@@ -614,7 +622,8 @@
     @NonNull
     private static final List<String> PUBLIC_NAMESPACES =
             Arrays.asList(NAMESPACE_TEXTCLASSIFIER, NAMESPACE_RUNTIME, NAMESPACE_STATSD_JAVA,
-                    NAMESPACE_STATSD_JAVA_BOOT, NAMESPACE_SELECTION_TOOLBAR, NAMESPACE_AUTOFILL);
+                    NAMESPACE_STATSD_JAVA_BOOT, NAMESPACE_SELECTION_TOOLBAR, NAMESPACE_AUTOFILL,
+                    NAMESPACE_DEVICE_POLICY_MANAGER);
     /**
      * Privacy related properties definitions.
      *
@@ -738,14 +747,6 @@
      */
     public static final String NAMESPACE_VENDOR_SYSTEM_NATIVE = "vendor_system_native";
 
-    /**
-     * Namespace for DevicePolicyManager related features.
-     *
-     * @hide
-     */
-    public static final String NAMESPACE_DEVICE_POLICY_MANAGER =
-            "device_policy_manager";
-
     private static final Object sLock = new Object();
     @GuardedBy("sLock")
     private static ArrayMap<OnPropertiesChangedListener, Pair<String, Executor>> sListeners =
diff --git a/core/java/android/service/voice/AbstractHotwordDetector.java b/core/java/android/service/voice/AbstractHotwordDetector.java
index 01d5638..b2bf9bc 100644
--- a/core/java/android/service/voice/AbstractHotwordDetector.java
+++ b/core/java/android/service/voice/AbstractHotwordDetector.java
@@ -189,5 +189,14 @@
                             .setHotwordDetectedResult(hotwordDetectedResult)
                             .build()));
         }
+
+        /** Called when the detection fails due to an error. */
+        @Override
+        public void onError() {
+            Slog.v(TAG, "BinderCallback#onError");
+            mHandler.sendMessage(obtainMessage(
+                    HotwordDetector.Callback::onError,
+                    mCallback));
+        }
     }
 }
diff --git a/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl b/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl
index 80f20fe..e865089 100644
--- a/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl
+++ b/core/java/android/service/voice/IMicrophoneHotwordDetectionVoiceInteractionCallback.aidl
@@ -33,4 +33,9 @@
         in HotwordDetectedResult hotwordDetectedResult,
         in AudioFormat audioFormat,
         in ParcelFileDescriptor audioStream);
+
+    /**
+     * Called when the detection fails due to an error.
+     */
+    void onError();
 }
diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java
index 2d662ea..f5a0c66 100644
--- a/core/java/android/service/voice/SoftwareHotwordDetector.java
+++ b/core/java/android/service/voice/SoftwareHotwordDetector.java
@@ -155,6 +155,15 @@
                             .setHotwordDetectedResult(hotwordDetectedResult)
                             .build()));
         }
+
+        /** Called when the detection fails due to an error. */
+        @Override
+        public void onError() {
+            Slog.v(TAG, "BinderCallback#onError");
+            mHandler.sendMessage(obtainMessage(
+                    HotwordDetector.Callback::onError,
+                    mCallback));
+        }
     }
 
     private static class InitializationStateListener
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 0ec95c6..d598017 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -888,7 +888,6 @@
             if (mShouldDimByDefault != mShouldDim && mWallpaperDimAmount == 0f) {
                 mShouldDim = mShouldDimByDefault;
                 updateSurfaceDimming();
-                updateSurface(false, false, true);
             }
         }
 
@@ -898,6 +897,10 @@
          * @param dimAmount Float amount between [0.0, 1.0] to dim the wallpaper.
          */
         private void updateWallpaperDimming(float dimAmount) {
+            if (dimAmount == mWallpaperDimAmount) {
+                return;
+            }
+
             // Custom dim amount cannot be less than the default dim amount.
             mWallpaperDimAmount = Math.max(mDefaultDimAmount, dimAmount);
             // If dim amount is 0f (additional dimming is removed), then the wallpaper should dim
@@ -1195,7 +1198,6 @@
                                     .setParent(mSurfaceControl)
                                     .setCallsite("Wallpaper#relayout")
                                     .build();
-                            updateSurfaceDimming();
                         }
                         // Propagate transform hint from WM, so we can use the right hint for the
                         // first frame.
@@ -1366,7 +1368,6 @@
                             mSession.finishDrawing(mWindow, null /* postDrawTransaction */,
                                                    Integer.MAX_VALUE);
                             processLocalColors(mPendingXOffset, mPendingXOffsetStep);
-                            notifyColorsChanged();
                         }
                         reposition();
                         reportEngineShown(shouldWaitForEngineShown());
diff --git a/core/java/android/speech/RecognitionService.java b/core/java/android/speech/RecognitionService.java
index 6a65efb..6b59f54 100644
--- a/core/java/android/speech/RecognitionService.java
+++ b/core/java/android/speech/RecognitionService.java
@@ -129,8 +129,9 @@
             @NonNull AttributionSource attributionSource) {
         try {
             if (mCurrentCallback == null) {
-                boolean preflightPermissionCheckPassed = checkPermissionForPreflightNotHardDenied(
-                        attributionSource);
+                boolean preflightPermissionCheckPassed =
+                        intent.hasExtra(RecognizerIntent.EXTRA_AUDIO_SOURCE)
+                        || checkPermissionForPreflightNotHardDenied(attributionSource);
                 if (preflightPermissionCheckPassed) {
                     if (DBG) {
                         Log.d(TAG, "created new mCurrentCallback, listener = "
diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java
index e5ec260..691452f 100644
--- a/core/java/android/view/Surface.java
+++ b/core/java/android/view/Surface.java
@@ -341,10 +341,12 @@
      */
     @UnsupportedAppUsage
     public void destroy() {
-        if (mNativeObject != 0) {
-            nativeDestroy(mNativeObject);
+        synchronized (mLock) {
+            if (mNativeObject != 0) {
+                nativeDestroy(mNativeObject);
+            }
+            release();
         }
-        release();
     }
 
     /**
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index d04b07c..38ca248 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -11941,7 +11941,7 @@
     @NonNull
     public final List<Rect> getUnrestrictedPreferKeepClearRects() {
         final ListenerInfo info = mListenerInfo;
-        if (info != null && info.mKeepClearRects != null) {
+        if (info != null && info.mUnrestrictedKeepClearRects != null) {
             return new ArrayList(info.mUnrestrictedKeepClearRects);
         }
 
@@ -21170,6 +21170,11 @@
         }
 
         AccessibilityNodeIdManager.getInstance().unregisterViewWithId(getAccessibilityViewId());
+
+        if (mBackgroundRenderNode != null) {
+            mBackgroundRenderNode.forceEndAnimators();
+        }
+        mRenderNode.forceEndAnimators();
     }
 
     private void cleanupDraw() {
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index e2e9a85..a0a3b4f 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -782,6 +782,7 @@
         @Override
         public void setCurrentRootView(ViewRootImpl rootView) {
             synchronized (mH) {
+                mImeDispatcher.switchRootView(mCurRootView, rootView);
                 mCurRootView = rootView;
             }
         }
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index d5763aa..5924844 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -25,8 +25,9 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.util.Log;
+import android.view.ViewRootImpl;
 
-import java.util.HashMap;
+import java.util.ArrayList;
 
 /**
  * A {@link OnBackInvokedDispatcher} for IME that forwards {@link OnBackInvokedCallback}
@@ -117,7 +118,7 @@
                 }
             };
 
-    private final HashMap<Integer, OnBackInvokedCallback> mImeCallbackMap = new HashMap<>();
+    private final ArrayList<ImeOnBackInvokedCallback> mImeCallbacks = new ArrayList<>();
 
     private void receive(
             int resultCode, Bundle resultData,
@@ -140,33 +141,55 @@
             int callbackId,
             @NonNull OnBackInvokedDispatcher receivingDispatcher) {
         final ImeOnBackInvokedCallback imeCallback =
-                new ImeOnBackInvokedCallback(iCallback);
-        mImeCallbackMap.put(callbackId, imeCallback);
+                new ImeOnBackInvokedCallback(iCallback, callbackId, priority);
+        mImeCallbacks.add(imeCallback);
         receivingDispatcher.registerOnBackInvokedCallback(priority, imeCallback);
     }
 
     private void unregisterReceivedCallback(
             int callbackId, OnBackInvokedDispatcher receivingDispatcher) {
-        final OnBackInvokedCallback callback = mImeCallbackMap.get(callbackId);
+        ImeOnBackInvokedCallback callback = null;
+        for (ImeOnBackInvokedCallback imeCallback : mImeCallbacks) {
+            if (imeCallback.getId() == callbackId) {
+                callback = imeCallback;
+                break;
+            }
+        }
         if (callback == null) {
             Log.e(TAG, "Ime callback not found. Ignoring unregisterReceivedCallback. "
                     + "callbackId: " + callbackId);
             return;
         }
         receivingDispatcher.unregisterOnBackInvokedCallback(callback);
+        mImeCallbacks.remove(callback);
     }
 
     /** Clears all registered callbacks on the instance. */
     public void clear() {
-        mImeCallbackMap.clear();
+        // Unregister previously registered callbacks if there's any.
+        if (getReceivingDispatcher() != null) {
+            for (ImeOnBackInvokedCallback callback : mImeCallbacks) {
+                getReceivingDispatcher().unregisterOnBackInvokedCallback(callback);
+            }
+        }
+        mImeCallbacks.clear();
     }
 
     static class ImeOnBackInvokedCallback implements OnBackInvokedCallback {
         @NonNull
         private final IOnBackInvokedCallback mIOnBackInvokedCallback;
+        /**
+         * The hashcode of the callback instance in the IME process, used as a unique id to
+         * identify the callback when it's passed between processes.
+         */
+        private final int mId;
+        private final int mPriority;
 
-        ImeOnBackInvokedCallback(@NonNull IOnBackInvokedCallback iCallback) {
+        ImeOnBackInvokedCallback(@NonNull IOnBackInvokedCallback iCallback, int id,
+                @Priority int priority) {
             mIOnBackInvokedCallback = iCallback;
+            mId = id;
+            mPriority = priority;
         }
 
         @Override
@@ -180,8 +203,33 @@
             }
         }
 
+        private int getId() {
+            return mId;
+        }
+
         IOnBackInvokedCallback getIOnBackInvokedCallback() {
             return mIOnBackInvokedCallback;
         }
     }
+
+    /**
+     * Transfers {@link ImeOnBackInvokedCallback}s registered on one {@link ViewRootImpl} to
+     * another {@link ViewRootImpl} on focus change.
+     *
+     * @param previous the previously focused {@link ViewRootImpl}.
+     * @param current the currently focused {@link ViewRootImpl}.
+     */
+    // TODO(b/232845902): Add CTS to test IME back behavior when there's root view change while
+    // IME is up.
+    public void switchRootView(ViewRootImpl previous, ViewRootImpl current) {
+        for (ImeOnBackInvokedCallback imeCallback : mImeCallbacks) {
+            if (previous != null) {
+                previous.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(imeCallback);
+            }
+            if (current != null) {
+                current.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                        imeCallback.mPriority, imeCallback);
+            }
+        }
+    }
 }
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index edfdbcc..985e2b9 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -157,15 +157,16 @@
 
     /** Clears all registered callbacks on the instance. */
     public void clear() {
+        if (mImeDispatcher != null) {
+            mImeDispatcher.clear();
+            mImeDispatcher = null;
+        }
         if (!mAllCallbacks.isEmpty()) {
             // Clear binder references in WM.
             setTopOnBackInvokedCallback(null);
         }
         mAllCallbacks.clear();
         mOnBackInvokedCallbacks.clear();
-        if (mImeDispatcher != null) {
-            mImeDispatcher = null;
-        }
     }
 
     private void setTopOnBackInvokedCallback(@Nullable OnBackInvokedCallback callback) {
diff --git a/core/java/com/android/internal/app/AppLocaleStore.java b/core/java/com/android/internal/app/AppLocaleStore.java
index 599e6d2..f3a322c 100644
--- a/core/java/com/android/internal/app/AppLocaleStore.java
+++ b/core/java/com/android/internal/app/AppLocaleStore.java
@@ -20,12 +20,15 @@
 
 import android.app.LocaleConfig;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageManager;
 import android.os.LocaleList;
 import android.util.Log;
 
-import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Locale;
+import java.util.stream.Collectors;
 
 class AppLocaleStore {
     private static final String TAG = AppLocaleStore.class.getSimpleName();
@@ -34,7 +37,8 @@
             Context context, String packageName) {
         LocaleConfig localeConfig = null;
         AppLocaleResult.LocaleStatus localeStatus = LocaleStatus.UNKNOWN_FAILURE;
-        ArrayList<Locale> appSupportedLocales = new ArrayList<>();
+        HashSet<Locale> appSupportedLocales = new HashSet<>();
+        HashSet<Locale> assetLocale = getAssetLocales(context, packageName);
 
         try {
             localeConfig = new LocaleConfig(context.createPackageContext(packageName, 0));
@@ -45,32 +49,43 @@
         if (localeConfig != null) {
             if (localeConfig.getStatus() == LocaleConfig.STATUS_SUCCESS) {
                 LocaleList packageLocaleList = localeConfig.getSupportedLocales();
-                if (packageLocaleList.size() > 0) {
+                boolean shouldFilterNotMatchingLocale = !hasInstallerInfo(context, packageName) &&
+                        isSystemApp(context, packageName);
+
+                Log.d(TAG, "filterNonMatchingLocale. " +
+                        ", shouldFilterNotMatchingLocale: " + shouldFilterNotMatchingLocale +
+                        ", assetLocale size: " + assetLocale.size() +
+                        ", packageLocaleList size: " + packageLocaleList.size());
+
+                for (int i = 0; i < packageLocaleList.size(); i++) {
+                    appSupportedLocales.add(packageLocaleList.get(i));
+                }
+                if (shouldFilterNotMatchingLocale) {
+                    appSupportedLocales = filterNotMatchingLocale(appSupportedLocales, assetLocale);
+                }
+
+                if (appSupportedLocales.size() > 0) {
                     localeStatus = LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG;
-                    for (int i = 0; i < packageLocaleList.size(); i++) {
-                        appSupportedLocales.add(packageLocaleList.get(i));
-                    }
                 } else {
                     localeStatus = LocaleStatus.NO_SUPPORTED_LANGUAGE_IN_APP;
                 }
             } else if (localeConfig.getStatus() == LocaleConfig.STATUS_NOT_SPECIFIED) {
-                String[] languages = getAssetLocales(context, packageName);
-                if (languages.length > 0) {
+                if (assetLocale.size() > 0) {
                     localeStatus = LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET;
-                    for (String language : languages) {
-                        appSupportedLocales.add(Locale.forLanguageTag(language));
-                    }
+                    appSupportedLocales = assetLocale;
                 } else {
                     localeStatus = LocaleStatus.ASSET_LOCALE_IS_EMPTY;
                 }
             }
         }
-        Log.d(TAG, "getAppSupportedLocales(). status: " + localeStatus
+        Log.d(TAG, "getAppSupportedLocales(). package: " + packageName
+                + ", status: " + localeStatus
                 + ", appSupportedLocales:" + appSupportedLocales.size());
         return new AppLocaleResult(localeStatus, appSupportedLocales);
     }
 
-    private static String[] getAssetLocales(Context context, String packageName) {
+    private static HashSet<Locale> getAssetLocales(Context context, String packageName) {
+        HashSet<Locale> result = new HashSet<>();
         try {
             PackageManager packageManager = context.getPackageManager();
             String[] locales = packageManager.getResourcesForApplication(
@@ -78,16 +93,59 @@
                             .applicationInfo).getAssets().getNonSystemLocales();
             if (locales == null) {
                 Log.i(TAG, "[" + packageName + "] locales are null.");
-                return new String[0];
             } else if (locales.length <= 0) {
                 Log.i(TAG, "[" + packageName + "] locales length is 0.");
-                return new String[0];
+            } else {
+                for (String language : locales) {
+                    result.add(Locale.forLanguageTag(language));
+                }
             }
-            return locales;
         } catch (PackageManager.NameNotFoundException e) {
             Log.w(TAG, "Can not found the package name : " + packageName + " / " + e);
         }
-        return new String[0];
+        return result;
+    }
+
+    private static HashSet<Locale> filterNotMatchingLocale(
+            HashSet<Locale> appSupportedLocales, HashSet<Locale> assetLocale) {
+        return appSupportedLocales.stream()
+                .filter(locale -> matchLanguageInSet(locale, assetLocale))
+                .collect(Collectors.toCollection(HashSet::new));
+    }
+
+    private static boolean matchLanguageInSet(Locale locale, HashSet<Locale> localesSet) {
+        if (localesSet.contains(locale)) {
+            return true;
+        }
+        for (Locale l: localesSet) {
+            if(LocaleList.matchesLanguageAndScript(l, locale)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean hasInstallerInfo(Context context, String packageName) {
+        InstallSourceInfo installSourceInfo;
+        try {
+            installSourceInfo = context.getPackageManager().getInstallSourceInfo(packageName);
+            return installSourceInfo != null;
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Installer info not found for: " + packageName);
+        }
+        return false;
+    }
+
+    private static boolean isSystemApp(Context context, String packageName) {
+        ApplicationInfo applicationInfo;
+        try {
+            applicationInfo = context.getPackageManager()
+                    .getApplicationInfoAsUser(packageName, /* flags= */ 0, context.getUserId());
+            return applicationInfo.isSystemApp();
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Application info not found for: " + packageName);
+        }
+        return false;
     }
 
     static class AppLocaleResult {
@@ -100,9 +158,9 @@
         }
 
         LocaleStatus mLocaleStatus;
-        ArrayList<Locale> mAppSupportedLocales;
+        HashSet<Locale> mAppSupportedLocales;
 
-        public AppLocaleResult(LocaleStatus localeStatus, ArrayList<Locale> appSupportedLocales) {
+        public AppLocaleResult(LocaleStatus localeStatus, HashSet<Locale> appSupportedLocales) {
             this.mLocaleStatus = localeStatus;
             this.mAppSupportedLocales = appSupportedLocales;
         }
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 2ad1b38..7cc37f7 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -190,6 +190,7 @@
     private static final String SHORTCUT_TARGET = "shortcut_target";
     private static final int APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20;
     public static final String APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter";
+    private static final String SHARED_TEXT_KEY = "shared_text";
 
     private static final String PLURALS_COUNT = "count";
     private static final String PLURALS_FILE_NAME = "file_name";
@@ -2201,6 +2202,7 @@
         final IntentFilter filter = getTargetIntentFilter();
         Bundle extras = new Bundle();
         extras.putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, filter);
+        populateTextContent(extras);
         AppPredictionContext appPredictionContext = new AppPredictionContext.Builder(contextAsUser)
             .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE)
             .setPredictedTargetCount(APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT)
@@ -2219,6 +2221,12 @@
         return appPredictionSession;
     }
 
+    private void populateTextContent(Bundle extras) {
+        final Intent intent = getTargetIntent();
+        String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
+        extras.putString(SHARED_TEXT_KEY, sharedText);
+    }
+
     /**
      * This will return an app predictor if it is enabled for direct share sorting
      * and if one exists. Otherwise, it returns null.
diff --git a/core/java/com/android/internal/app/ChooserTargetActionsDialogFragment.java b/core/java/com/android/internal/app/ChooserTargetActionsDialogFragment.java
index 4f1f380..3e1b5f0 100644
--- a/core/java/com/android/internal/app/ChooserTargetActionsDialogFragment.java
+++ b/core/java/com/android/internal/app/ChooserTargetActionsDialogFragment.java
@@ -269,7 +269,7 @@
     protected CharSequence getItemLabel(DisplayResolveInfo dri) {
         final PackageManager pm = getContext().getPackageManager();
         return getPinLabel(isPinned(dri),
-                isShortcutTarget() ? "" : dri.getResolveInfo().loadLabel(pm));
+                isShortcutTarget() ? mShortcutTitle : dri.getResolveInfo().loadLabel(pm));
     }
 
     @Nullable
diff --git a/core/java/com/android/internal/app/LocalePickerWithRegion.java b/core/java/com/android/internal/app/LocalePickerWithRegion.java
index a06ba9b..965895f 100644
--- a/core/java/com/android/internal/app/LocalePickerWithRegion.java
+++ b/core/java/com/android/internal/app/LocalePickerWithRegion.java
@@ -29,13 +29,13 @@
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
+import android.view.MenuItem.OnActionExpandListener;
 import android.view.View;
 import android.widget.ListView;
 import android.widget.SearchView;
 
 import com.android.internal.R;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Locale;
@@ -64,6 +64,7 @@
     private int mTopDistance = 0;
     private String mAppPackageName;
     private CharSequence mTitle = null;
+    private OnActionExpandListener mOnActionExpandListener;
 
     /**
      * Other classes can register to be notified when a locale was selected.
@@ -80,8 +81,10 @@
 
     private static LocalePickerWithRegion createCountryPicker(Context context,
             LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
-            boolean translatedOnly, String appPackageName) {
+            boolean translatedOnly, String appPackageName,
+            OnActionExpandListener onActionExpandListener) {
         LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
+        localePicker.setOnActionExpandListener(onActionExpandListener);
         boolean shouldShowTheList = localePicker.setListener(context, listener, parent,
                 translatedOnly, appPackageName);
         return shouldShowTheList ? localePicker : null;
@@ -95,8 +98,10 @@
     }
 
     public static LocalePickerWithRegion createLanguagePicker(Context context,
-            LocaleSelectedListener listener, boolean translatedOnly, String appPackageName) {
+            LocaleSelectedListener listener, boolean translatedOnly, String appPackageName,
+            OnActionExpandListener onActionExpandListener) {
         LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
+        localePicker.setOnActionExpandListener(onActionExpandListener);
         localePicker.setListener(
                 context, listener, /* parent */ null, translatedOnly, appPackageName);
         return localePicker;
@@ -198,13 +203,20 @@
     }
 
     private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotSupportedInApp(
-            boolean shouldShowList, ArrayList<Locale> supportedLocales) {
+            boolean shouldShowList, HashSet<Locale> supportedLocales) {
         Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>();
-        if (shouldShowList) {
-            for(LocaleStore.LocaleInfo li: mLocaleList) {
+        if (!shouldShowList) {
+            return filteredList;
+        }
+
+        for(LocaleStore.LocaleInfo li: mLocaleList) {
+            if (supportedLocales.contains(li.getLocale())) {
+                filteredList.add(li);
+            } else {
                 for(Locale l: supportedLocales) {
                     if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) {
                         filteredList.add(li);
+                        break;
                     }
                 }
             }
@@ -310,7 +322,7 @@
         } else {
             LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker(
                     getContext(), mListener, locale, mTranslatedOnly /* translate only */,
-                    mAppPackageName);
+                    mAppPackageName, mOnActionExpandListener);
             if (selector != null) {
                 getFragmentManager().beginTransaction()
                         .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
@@ -328,8 +340,11 @@
             inflater.inflate(R.menu.language_selection_list, menu);
 
             final MenuItem searchMenuItem = menu.findItem(R.id.locale_search_menu);
-            mSearchView = (SearchView) searchMenuItem.getActionView();
+            if (!TextUtils.isEmpty(mAppPackageName) && mOnActionExpandListener != null) {
+                searchMenuItem.setOnActionExpandListener(mOnActionExpandListener);
+            }
 
+            mSearchView = (SearchView) searchMenuItem.getActionView();
             mSearchView.setQueryHint(getText(R.string.search_language_hint));
             mSearchView.setOnQueryTextListener(this);
 
@@ -363,4 +378,11 @@
         }
         return false;
     }
+
+    /**
+     * Sets OnActionExpandListener to LocalePickerWithRegion to dectect action of search bar.
+     */
+    public void setOnActionExpandListener(OnActionExpandListener onActionExpandListener) {
+        mOnActionExpandListener = onActionExpandListener;
+    }
 }
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index bd5a73d..7e53a5a 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -119,6 +119,11 @@
 
     @UnsupportedAppUsage
     public ResolverActivity() {
+        mIsIntentPicker = getClass().equals(ResolverActivity.class);
+    }
+
+    protected ResolverActivity(boolean isIntentPicker) {
+        mIsIntentPicker = isIntentPicker;
     }
 
     private boolean mSafeForwardingMode;
@@ -135,6 +140,8 @@
     private String mReferrerPackage;
     private CharSequence mTitle;
     private int mDefaultTitleResId;
+    // Expected to be true if this object is ResolverActivity or is ResolverWrapperActivity.
+    private final boolean mIsIntentPicker;
 
     // Whether or not this activity supports choosing a default handler for the intent.
     @VisibleForTesting
@@ -445,10 +452,6 @@
                         + (categories != null ? Arrays.toString(categories.toArray()) : ""));
     }
 
-    private boolean isIntentPicker() {
-        return getClass().equals(ResolverActivity.class);
-    }
-
     protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter(
             Intent[] initialIntents,
             List<ResolveInfo> rList,
@@ -637,6 +640,11 @@
 
         resetButtonBar();
 
+        if (shouldUseMiniResolver()) {
+            View buttonContainer = findViewById(R.id.button_bar_container);
+            buttonContainer.setPadding(0, 0, 0, mSystemWindowInsets.bottom);
+        }
+
         // Need extra padding so the list can fully scroll up
         if (shouldAddFooterView()) {
             applyFooterView(mSystemWindowInsets.bottom);
@@ -649,7 +657,8 @@
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
-        if (isIntentPicker() && shouldShowTabs() && !useLayoutWithDefault()) {
+        if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault()
+                && !shouldUseMiniResolver()) {
             updateIntentPickerPaddings();
         }
 
@@ -1084,7 +1093,7 @@
         if (isAutolaunching()) {
             return;
         }
-        if (isIntentPicker()) {
+        if (mIsIntentPicker) {
             ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
                     .setUseLayoutWithDefault(useLayoutWithDefault());
         }
@@ -1108,7 +1117,7 @@
     protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildCompleted) {
         final ItemClickListener listener = new ItemClickListener();
         setupAdapterListView((ListView) mMultiProfilePagerAdapter.getActiveAdapterView(), listener);
-        if (shouldShowTabs() && isIntentPicker()) {
+        if (shouldShowTabs() && mIsIntentPicker) {
             final ResolverDrawerLayout rdl = findViewById(R.id.contentPanel);
             if (rdl != null) {
                 rdl.setMaxCollapsedHeight(getResources()
@@ -1448,6 +1457,12 @@
         return postRebuildList(rebuildCompleted);
     }
 
+    /**
+     * Mini resolver is shown when the user is choosing between browser[s] in this profile and a
+     * single app in the other profile (see shouldUseMiniResolver()). It shows the single app icon
+     * and asks the user if they'd like to open that cross-profile app or use the in-profile
+     * browser.
+     */
     private void configureMiniResolverContent() {
         mLayoutId = R.layout.miniresolver;
         setContentView(mLayoutId);
@@ -1456,10 +1471,14 @@
                 mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList.get(0);
         boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
 
-        DisplayResolveInfo otherProfileResolveInfo =
-                mMultiProfilePagerAdapter.getInactiveListAdapter().mDisplayList.get(0);
+        ResolverListAdapter inactiveAdapter = mMultiProfilePagerAdapter.getInactiveListAdapter();
+        DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
+
+        // Load the icon asynchronously
         ImageView icon = findViewById(R.id.icon);
-        // TODO: Set icon drawable to app icon.
+        ResolverListAdapter.LoadIconTask iconTask = inactiveAdapter.new LoadIconTask(
+                        otherProfileResolveInfo, new ResolverListAdapter.ViewHolder(icon));
+        iconTask.execute();
 
         ((TextView) findViewById(R.id.open_cross_profile)).setText(
                 getResources().getString(
@@ -1479,12 +1498,20 @@
                 prepareIntentForCrossProfileLaunch(intent);
             }
             safelyStartActivityAsUser(otherProfileResolveInfo,
-                    mMultiProfilePagerAdapter.getInactiveListAdapter().mResolverListController
-                            .getUserHandle());
+                    inactiveAdapter.mResolverListController.getUserHandle());
         });
     }
 
+    /**
+     * Mini resolver should be used when all of the following are true:
+     * 1. This is the intent picker (ResolverActivity).
+     * 2. This profile only has web browser matches.
+     * 3. The other profile has a single non-browser match.
+     */
     private boolean shouldUseMiniResolver() {
+        if (!mIsIntentPicker) {
+            return false;
+        }
         if (mMultiProfilePagerAdapter.getActiveListAdapter() == null
                 || mMultiProfilePagerAdapter.getInactiveListAdapter() == null) {
             return false;
@@ -1790,7 +1817,7 @@
     void onHorizontalSwipeStateChanged(int state) {}
 
     private void maybeHideDivider() {
-        if (!isIntentPicker()) {
+        if (!mIsIntentPicker) {
             return;
         }
         final View divider = findViewById(R.id.divider);
@@ -1807,7 +1834,7 @@
     protected void onProfileTabSelected() { }
 
     private void resetCheckedItem() {
-        if (!isIntentPicker()) {
+        if (!mIsIntentPicker) {
             return;
         }
         mLastSelected = ListView.INVALID_POSITION;
diff --git a/core/java/com/android/internal/app/procstats/ProcessStats.java b/core/java/com/android/internal/app/procstats/ProcessStats.java
index b7abe73..9fea86e 100644
--- a/core/java/com/android/internal/app/procstats/ProcessStats.java
+++ b/core/java/com/android/internal/app/procstats/ProcessStats.java
@@ -374,14 +374,12 @@
                     }
                 }
                 thisProc.add(otherProc);
-                if (thisProc.isActive()) {
-                    UidState uidState = mUidStates.get(uid);
-                    if (uidState == null) {
-                        uidState = new UidState(this, uid);
-                        mUidStates.put(uid, uidState);
-                    }
-                    uidState.addProcess(thisProc);
+                UidState uidState = mUidStates.get(uid);
+                if (uidState == null) {
+                    uidState = new UidState(this, uid);
+                    mUidStates.put(uid, uidState);
                 }
+                uidState.addProcess(thisProc);
             }
         }
 
@@ -1185,9 +1183,7 @@
                         + " " + proc);
                 mProcesses.put(procName, uid, proc);
 
-                if (proc.isActive()) {
-                    mUidStates.get(uid).addProcess(proc);
-                }
+                mUidStates.get(uid).addProcess(proc);
             }
         }
 
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index e466c88..2dcc585 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -71,8 +71,10 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SUW_LOADING_TO_NEXT_FLOW;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SUW_LOADING_TO_SHOW_INFO_WITH_ACTIONS;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SUW_SHOW_FUNCTION_SCREEN_WITH_ACTIONS;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TAKE_SCREENSHOT;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__UNFOLD_ANIM;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__VOLUME_CONTROL;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION;
 
 import android.annotation.IntDef;
@@ -194,6 +196,8 @@
     public static final int CUJ_LOCKSCREEN_LAUNCH_CAMERA = 51; // reserved.
     public static final int CUJ_SPLIT_SCREEN_RESIZE = 52;
     public static final int CUJ_SETTINGS_SLIDER = 53;
+    public static final int CUJ_TAKE_SCREENSHOT = 54;
+    public static final int CUJ_VOLUME_CONTROL = 55;
 
     private static final int NO_STATSD_LOGGING = -1;
 
@@ -256,6 +260,8 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_LAUNCH_CAMERA,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SPLIT_SCREEN_RESIZE,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_SLIDER,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TAKE_SCREENSHOT,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__VOLUME_CONTROL,
     };
 
     private static volatile InteractionJankMonitor sInstance;
@@ -330,6 +336,8 @@
             CUJ_LOCKSCREEN_LAUNCH_CAMERA,
             CUJ_SPLIT_SCREEN_RESIZE,
             CUJ_SETTINGS_SLIDER,
+            CUJ_TAKE_SCREENSHOT,
+            CUJ_VOLUME_CONTROL,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -756,6 +764,10 @@
                 return "CUJ_SPLIT_SCREEN_RESIZE";
             case CUJ_SETTINGS_SLIDER:
                 return "SETTINGS_SLIDER";
+            case CUJ_TAKE_SCREENSHOT:
+                return "TAKE_SCREENSHOT";
+            case CUJ_VOLUME_CONTROL:
+                return "VOLUME_CONTROL";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index b03a8cb..6829f3d 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -644,7 +644,7 @@
         /** Schedule removal of UIDs corresponding to a removed user */
         Future<?> scheduleCleanupDueToRemovedUser(int userId);
         /** Schedule a sync because of a process state change */
-        Future<?> scheduleSyncDueToProcessStateChange(long delayMillis);
+        void scheduleSyncDueToProcessStateChange(int flags, long delayMillis);
     }
 
     public Handler mHandler;
@@ -6215,9 +6215,7 @@
             long elapsedRealtimeMs, long uptimeMs) {
         if (mMobileRadioPowerState != powerState) {
             long realElapsedRealtimeMs;
-            final boolean active =
-                    powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_MEDIUM
-                            || powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
+            final boolean active = isActiveRadioPowerState(powerState);
             if (active) {
                 if (uid > 0) {
                     noteMobileRadioApWakeupLocked(elapsedRealtimeMs, uptimeMs, uid);
@@ -6259,6 +6257,11 @@
         return false;
     }
 
+    private static boolean isActiveRadioPowerState(int powerState) {
+        return powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_MEDIUM
+                || powerState == DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH;
+    }
+
     @GuardedBy("this")
     public void notePowerSaveModeLocked(boolean enabled) {
         notePowerSaveModeLocked(enabled, mClock.elapsedRealtime(), mClock.uptimeMillis());
@@ -12042,7 +12045,13 @@
                 return;
             }
 
-            mBsi.mExternalSync.scheduleSyncDueToProcessStateChange(
+            int flags = ExternalStatsSync.UPDATE_ON_PROC_STATE_CHANGE;
+            // Skip querying for inactive radio, where power usage is probably negligible.
+            if (!BatteryStatsImpl.isActiveRadioPowerState(mBsi.mMobileRadioPowerState)) {
+                flags &= ~ExternalStatsSync.UPDATE_RADIO;
+            }
+
+            mBsi.mExternalSync.scheduleSyncDueToProcessStateChange(flags,
                     mBsi.mConstants.PROC_STATE_CHANGE_COLLECTION_DELAY_MS);
         }
 
diff --git a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java
index f2c27a4..d842eb6 100644
--- a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java
+++ b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java
@@ -395,7 +395,7 @@
         @SyncAndDrawResult
         public int renderView(View view, Rect sourceRect) {
             HardwareRenderer.FrameRenderRequest request = mRenderer.createRenderRequest();
-            request.setVsyncTime(SystemClock.elapsedRealtimeNanos());
+            request.setVsyncTime(System.nanoTime());
             if (updateForView(view)) {
                 setupLighting(view);
             }
diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java
index 06d12b5..3436b9e 100644
--- a/core/java/com/android/server/SystemConfig.java
+++ b/core/java/com/android/server/SystemConfig.java
@@ -682,7 +682,6 @@
             readPermissions(parser, Environment.buildPath(f, "etc", "permissions"),
                     apexPermissionFlag);
         }
-        pruneVendorApexPrivappAllowlists();
     }
 
     @VisibleForTesting
@@ -1598,21 +1597,6 @@
         }
     }
 
-    /**
-     * Prunes out any privileged permission allowlists bundled in vendor apexes.
-     */
-    @VisibleForTesting
-    public void pruneVendorApexPrivappAllowlists() {
-        for (String moduleName: mAllowedVendorApexes.keySet()) {
-            if (mApexPrivAppPermissions.containsKey(moduleName)
-                    || mApexPrivAppDenyPermissions.containsKey(moduleName)) {
-                Slog.w(TAG, moduleName + " is a vendor apex, ignore its priv-app allowlist");
-                mApexPrivAppPermissions.remove(moduleName);
-                mApexPrivAppDenyPermissions.remove(moduleName);
-            }
-        }
-    }
-
     private void readInstallInUserType(XmlPullParser parser,
             Map<String, Set<String>> doInstallMap,
             Map<String, Set<String>> nonInstallMap)
diff --git a/core/jni/android_net_LocalSocketImpl.cpp b/core/jni/android_net_LocalSocketImpl.cpp
index 42cf1f4..9bd0700 100644
--- a/core/jni/android_net_LocalSocketImpl.cpp
+++ b/core/jni/android_net_LocalSocketImpl.cpp
@@ -257,7 +257,7 @@
     err = socket_read_all(env, object, fd, &buf, 1);
 
     if (err < 0) {
-        jniThrowIOException(env, errno);
+        // socket_read_all has already thrown
         return (jint)0;
     }
 
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index c769da5..4044c57 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -1921,6 +1921,7 @@
     FocusRequest request;
     request.timestamp = systemTime(SYSTEM_TIME_MONOTONIC);
     request.displayId = displayId;
+    request.windowName = "<null>";
     transaction->setFocusedWindow(request);
 }
 
diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto
index 2f2158d..2a625b0 100644
--- a/core/proto/android/server/vibrator/vibratormanagerservice.proto
+++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto
@@ -86,7 +86,7 @@
     optional int32 flags = 3;
 }
 
-// Next id: 7
+// Next id: 8
 message VibrationProto {
     option (.android.msg_privacy).dest = DEST_AUTOMATIC;
     optional int64 start_time = 1;
@@ -95,6 +95,7 @@
     optional CombinedVibrationEffectProto original_effect = 4;
     optional VibrationAttributesProto attributes = 5;
     optional int32 status = 6;
+    optional int64 duration_ms = 7;
 }
 
 // Next id: 25
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 0f328b0..8db5f9a 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6441,11 +6441,11 @@
 
     <!-- @SystemApi Must be required by a safety source to send an update using the
              {@link android.safetycenter.SafetyCenterManager}.
-             <p>Protection level: signature|privileged
+             <p>Protection level: internal|privileged
              @hide
         -->
     <permission android:name="android.permission.SEND_SAFETY_CENTER_UPDATE"
-                android:protectionLevel="signature|privileged" />
+                android:protectionLevel="internal|privileged" />
 
     <!-- @SystemApi Allows an application to launch device manager setup screens.
          <p>Not for use by third-party applications.
@@ -7044,7 +7044,7 @@
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
-        <service android:name="com.android.server.companion.AssociationCleanUpService"
+        <service android:name="com.android.server.companion.InactiveAssociationsRemovalService"
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
 
diff --git a/core/res/res/anim-ldrtl/task_fragment_close_enter.xml b/core/res/res/anim-ldrtl/task_fragment_close_enter.xml
new file mode 100644
index 0000000..e5f5707
--- /dev/null
+++ b/core/res/res/anim-ldrtl/task_fragment_close_enter.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+     android:shareInterpolator="false">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:fillEnabled="true"
+        android:fillBefore="true"
+        android:fillAfter="true"
+        android:interpolator="@interpolator/linear"
+        android:startOffset="0"
+        android:duration="200" />
+
+    <translate
+        android:fillEnabled="true"
+        android:fillBefore="true"
+        android:fillAfter="true"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:startOffset="0"
+        android:duration="200" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim-ldrtl/task_fragment_close_exit.xml b/core/res/res/anim-ldrtl/task_fragment_close_exit.xml
new file mode 100644
index 0000000..c5a3654
--- /dev/null
+++ b/core/res/res/anim-ldrtl/task_fragment_close_exit.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+     android:shareInterpolator="false">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="0.0"
+        android:fillEnabled="true"
+        android:fillBefore="true"
+        android:fillAfter="true"
+        android:interpolator="@interpolator/linear"
+        android:startOffset="35"
+        android:duration="83" />
+
+    <translate
+        android:fromXDelta="0"
+        android:toXDelta="-10%"
+        android:fillEnabled="true"
+        android:fillBefore="true"
+        android:fillAfter="true"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:startOffset="0"
+        android:duration="200" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim-ldrtl/task_fragment_open_enter.xml b/core/res/res/anim-ldrtl/task_fragment_open_enter.xml
new file mode 100644
index 0000000..b6f1af3
--- /dev/null
+++ b/core/res/res/anim-ldrtl/task_fragment_open_enter.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+/*
+** Copyright 2022, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+     android:shareInterpolator="false">
+
+    <alpha
+        android:fromAlpha="0"
+        android:toAlpha="1.0"
+        android:fillEnabled="true"
+        android:fillBefore="true"
+        android:fillAfter="true"
+        android:interpolator="@interpolator/linear"
+        android:startOffset="50"
+        android:duration="83" />
+
+    <translate
+        android:fromXDelta="-10%"
+        android:toXDelta="0"
+        android:fillEnabled="true"
+        android:fillBefore="true"
+        android:fillAfter="true"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:duration="450" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim-ldrtl/task_fragment_open_exit.xml b/core/res/res/anim-ldrtl/task_fragment_open_exit.xml
new file mode 100644
index 0000000..6cea53c
--- /dev/null
+++ b/core/res/res/anim-ldrtl/task_fragment_open_exit.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+/*
+** Copyright 2022, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+     android:shareInterpolator="false">
+
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:fillEnabled="true"
+        android:fillBefore="true"
+        android:fillAfter="true"
+        android:interpolator="@interpolator/standard_accelerate"
+        android:startOffset="0"
+        android:duration="450" />
+
+    <translate
+        android:fillEnabled="true"
+        android:fillBefore="true"
+        android:fillAfter="true"
+        android:interpolator="@interpolator/fast_out_extra_slow_in"
+        android:duration="450" />
+</set>
\ No newline at end of file
diff --git a/core/res/res/anim/task_fragment_close_enter.xml b/core/res/res/anim/task_fragment_close_enter.xml
index c940552..cb6cdbe 100644
--- a/core/res/res/anim/task_fragment_close_enter.xml
+++ b/core/res/res/anim/task_fragment_close_enter.xml
@@ -17,16 +17,21 @@
 
 <set xmlns:android="http://schemas.android.com/apk/res/android"
     android:shareInterpolator="false">
-    <scale
-        android:fromXScale="1.1"
-        android:toXScale="1"
-        android:fromYScale="1.1"
-        android:toYScale="1"
-        android:pivotX="50%"
-        android:pivotY="50%"
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:fillEnabled="true"
+        android:fillBefore="true"
+        android:fillAfter="true"
+        android:interpolator="@interpolator/linear"
+        android:startOffset="0"
+        android:duration="200" />
+
+    <translate
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
         android:interpolator="@interpolator/fast_out_extra_slow_in"
-        android:duration="400"/>
+        android:startOffset="0"
+        android:duration="200" />
 </set>
\ No newline at end of file
diff --git a/core/res/res/anim/task_fragment_close_exit.xml b/core/res/res/anim/task_fragment_close_exit.xml
index 8998f76..84d8b7e 100644
--- a/core/res/res/anim/task_fragment_close_exit.xml
+++ b/core/res/res/anim/task_fragment_close_exit.xml
@@ -19,24 +19,21 @@
     android:shareInterpolator="false"
     android:zAdjustment="top">
     <alpha
-        android:fromAlpha="1"
+        android:fromAlpha="1.0"
         android:toAlpha="0.0"
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
         android:interpolator="@interpolator/linear"
-        android:startOffset="33"
-        android:duration="50"/>
-    <scale
-        android:fromXScale="1"
-        android:toXScale="0.9"
-        android:fromYScale="1"
-        android:toYScale="0.9"
-        android:pivotX="50%"
-        android:pivotY="50%"
+        android:startOffset="0"
+        android:duration="83" />
+
+    <translate
+        android:fromXDelta="0"
+        android:toXDelta="10%"
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
         android:interpolator="@interpolator/fast_out_extra_slow_in"
-        android:duration="400"/>
+        android:duration="200" />
 </set>
diff --git a/core/res/res/anim/task_fragment_open_enter.xml b/core/res/res/anim/task_fragment_open_enter.xml
index 6bc47de..aa61e6f 100644
--- a/core/res/res/anim/task_fragment_open_enter.xml
+++ b/core/res/res/anim/task_fragment_open_enter.xml
@@ -25,17 +25,13 @@
         android:fillAfter="true"
         android:interpolator="@interpolator/linear"
         android:startOffset="50"
-        android:duration="50"/>
-    <scale
-        android:fromXScale="0.85"
-        android:toXScale="1"
-        android:fromYScale="0.85"
-        android:toYScale="1"
-        android:pivotX="50%"
-        android:pivotY="50%"
+        android:duration="83" />
+    <translate
+        android:fromXDelta="10%"
+        android:toXDelta="0"
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
         android:interpolator="@interpolator/fast_out_extra_slow_in"
-        android:duration="400"/>
+        android:duration="400" />
 </set>
diff --git a/core/res/res/anim/task_fragment_open_exit.xml b/core/res/res/anim/task_fragment_open_exit.xml
index 160eb84..b4914d2 100644
--- a/core/res/res/anim/task_fragment_open_exit.xml
+++ b/core/res/res/anim/task_fragment_open_exit.xml
@@ -17,16 +17,20 @@
 
 <set xmlns:android="http://schemas.android.com/apk/res/android"
     android:shareInterpolator="false">
-    <scale
-        android:fromXScale="1"
-        android:toXScale="1.05"
-        android:fromYScale="1"
-        android:toYScale="1.05"
-        android:pivotX="50%"
-        android:pivotY="50%"
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="1.0"
+        android:fillEnabled="true"
+        android:fillBefore="true"
+        android:fillAfter="true"
+        android:interpolator="@interpolator/standard_accelerate"
+        android:startOffset="0"
+        android:duration="400" />
+
+    <translate
         android:fillEnabled="true"
         android:fillBefore="true"
         android:fillAfter="true"
         android:interpolator="@interpolator/fast_out_extra_slow_in"
-        android:duration="400"/>
+        android:duration="400" />
 </set>
\ No newline at end of file
diff --git a/core/res/res/drawable/bottomsheet_background.xml b/core/res/res/drawable/bottomsheet_background.xml
index 00d53004..9469e0a 100644
--- a/core/res/res/drawable/bottomsheet_background.xml
+++ b/core/res/res/drawable/bottomsheet_background.xml
@@ -16,7 +16,7 @@
 
 <shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
     <corners
-        android:topLeftRadius="@dimen/config_bottomDialogCornerRadius"
-        android:topRightRadius="@dimen/config_bottomDialogCornerRadius"/>
-    <solid android:color="?attr/colorBackground" />
+        android:topLeftRadius="@dimen/autofill_dialog_corner_radius"
+        android:topRightRadius="@dimen/autofill_dialog_corner_radius"/>
+    <solid android:color="?attr/colorSurface" />
 </shape>
diff --git a/core/res/res/drawable/btn_outlined.xml b/core/res/res/drawable/btn_outlined.xml
new file mode 100644
index 0000000..8fbcc25
--- /dev/null
+++ b/core/res/res/drawable/btn_outlined.xml
@@ -0,0 +1,20 @@
+<!-- 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+  <solid android:color="?attr/selectableItemBackground"/>
+  <stroke android:width="1dp" android:color="?attr/colorAccentPrimaryVariant"/>
+  <corners android:radius="28dp"/>
+</shape>
diff --git a/core/res/res/drawable/btn_tonal.xml b/core/res/res/drawable/btn_tonal.xml
new file mode 100644
index 0000000..b891ca8
--- /dev/null
+++ b/core/res/res/drawable/btn_tonal.xml
@@ -0,0 +1,19 @@
+<!-- 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+  <solid android:color="?attr/colorAccentPrimary"/>
+  <corners android:radius="28dp"/>
+</shape>
diff --git a/core/res/res/layout/autofill_fill_dialog.xml b/core/res/res/layout/autofill_fill_dialog.xml
index 0b6a201..e638764 100644
--- a/core/res/res/layout/autofill_fill_dialog.xml
+++ b/core/res/res/layout/autofill_fill_dialog.xml
@@ -20,8 +20,6 @@
     android:id="@+id/autofill_dialog_picker"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
-    android:layout_marginTop="@dimen/autofill_save_outer_top_margin"
-    android:padding="@dimen/autofill_save_outer_top_padding"
     android:background="@drawable/bottomsheet_background"
     android:orientation="vertical">
 
@@ -29,8 +27,9 @@
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:gravity="center_horizontal"
-        android:paddingStart="@dimen/autofill_save_inner_padding"
-        android:paddingEnd="@dimen/autofill_save_inner_padding"
+        android:paddingTop="24dp"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="24dp"
         android:orientation="vertical">
 
         <ImageView
@@ -59,6 +58,8 @@
         android:paddingStart="@dimen/autofill_save_inner_padding"
         android:paddingEnd="@dimen/autofill_save_inner_padding"
         android:visibility="gone"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="24dp"
         android:background="@drawable/autofill_dataset_picker_background"/>
 
     <ListView
@@ -69,22 +70,30 @@
         android:drawSelectorOnTop="true"
         android:clickable="true"
         android:divider="?android:attr/listDivider"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="24dp"
         android:background="@drawable/autofill_dataset_picker_background"
         android:visibility="gone"/>
 
     <com.android.internal.widget.ButtonBarLayout
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="48dp"
         android:layout_gravity="end"
-        android:padding="@dimen/autofill_save_button_bar_padding"
         android:clipToPadding="false"
+        android:layout_marginTop="32dp"
+        android:layout_marginBottom="18dp"
+        android:layout_marginStart="24dp"
+        android:layout_marginEnd="24dp"
+        android:theme="@style/Theme.DeviceDefault.AutofillHalfScreenDialogButton"
         android:orientation="horizontal">
 
         <Button
             android:id="@+id/autofill_dialog_no"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            style="?android:attr/buttonBarButtonStyle"
+            android:layout_height="36dp"
+            android:layout_marginTop="6dp"
+            android:layout_marginBottom="6dp"
+            style="@style/AutofillHalfSheetOutlinedButton"
             android:text="@string/autofill_save_no">
         </Button>
 
@@ -98,8 +107,10 @@
         <Button
             android:id="@+id/autofill_dialog_yes"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            style="@style/Widget.DeviceDefault.Button.Colored"
+            android:layout_height="36dp"
+            android:layout_marginTop="6dp"
+            android:layout_marginBottom="6dp"
+            style="@style/AutofillHalfSheetTonalButton"
             android:text="@string/autofill_save_yes"
             android:visibility="gone" >
         </Button>
diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml
index eb5fd56..8a1a9c7 100644
--- a/core/res/res/layout/autofill_save.xml
+++ b/core/res/res/layout/autofill_save.xml
@@ -19,6 +19,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
+    android:background="@drawable/bottomsheet_background"
     android:orientation="vertical">
 
     <LinearLayout
@@ -29,7 +30,7 @@
         android:layout_marginTop="@dimen/autofill_save_outer_top_margin"
         android:paddingTop="@dimen/autofill_save_outer_top_padding"
         android:elevation="@dimen/autofill_elevation"
-        android:background="?android:attr/colorBackground"
+        android:background="?android:attr/colorSurface"
         android:orientation="vertical">
 
         <LinearLayout
@@ -73,13 +74,26 @@
 
         <com.android.internal.widget.ButtonBarLayout
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
+            android:layout_height="48dp"
             android:layout_gravity="end"
-            android:padding="@dimen/autofill_save_button_bar_padding"
             android:clipToPadding="false"
-            android:layout_weight="1"
+            android:layout_marginTop="32dp"
+            android:layout_marginBottom="18dp"
+            android:layout_marginStart="24dp"
+            android:layout_marginEnd="24dp"
+            android:theme="@style/Theme.DeviceDefault.AutofillHalfScreenDialogButton"
             android:orientation="horizontal">
 
+            <Button
+                android:id="@+id/autofill_save_no"
+                android:layout_width="wrap_content"
+                android:layout_height="36dp"
+                android:layout_marginTop="6dp"
+                android:layout_marginBottom="6dp"
+                style="@style/AutofillHalfSheetOutlinedButton"
+                android:text="@string/autofill_save_no">
+            </Button>
+
             <Space
                 android:layout_width="0dp"
                 android:layout_height="0dp"
@@ -88,18 +102,12 @@
             </Space>
 
             <Button
-                android:id="@+id/autofill_save_no"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                style="?android:attr/buttonBarButtonStyle"
-                android:text="@string/autofill_save_no">
-            </Button>
-
-            <Button
                 android:id="@+id/autofill_save_yes"
                 android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                style="@style/Widget.DeviceDefault.Button.Colored"
+                android:layout_height="36dp"
+                android:layout_marginTop="6dp"
+                android:layout_marginBottom="6dp"
+                style="@style/AutofillHalfSheetTonalButton"
                 android:text="@string/autofill_save_yes">
             </Button>
 
diff --git a/core/res/res/layout/miniresolver.xml b/core/res/res/layout/miniresolver.xml
index 44ed6f2..85fe283 100644
--- a/core/res/res/layout/miniresolver.xml
+++ b/core/res/res/layout/miniresolver.xml
@@ -24,12 +24,11 @@
     android:id="@id/contentPanel">
 
     <RelativeLayout
-        android:id="@+id/title_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_alwaysShow="true"
         android:elevation="@dimen/resolver_elevation"
-        android:paddingTop="@dimen/resolver_small_margin"
+        android:paddingTop="24dp"
         android:paddingStart="@dimen/resolver_edge_margin"
         android:paddingEnd="@dimen/resolver_edge_margin"
         android:paddingBottom="@dimen/resolver_title_padding_bottom"
@@ -47,8 +46,12 @@
             android:id="@+id/open_cross_profile"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
+            android:paddingTop="16dp"
             android:layout_below="@id/icon"
             android:layout_centerHorizontal="true"
+            android:textSize="24sp"
+            android:lineHeight="32sp"
+            android:gravity="center"
             android:textColor="?android:textColorPrimary"
         />
     </RelativeLayout>
@@ -58,15 +61,11 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_alwaysShow="true"
+        android:paddingTop="32dp"
+        android:paddingBottom="@dimen/resolver_button_bar_spacing"
         android:orientation="vertical"
         android:background="?attr/colorBackground"
         android:layout_ignoreOffset="true">
-        <View
-            android:id="@+id/resolver_button_bar_divider"
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:background="?attr/colorBackground"
-            android:foreground="?attr/dividerVertical" />
         <RelativeLayout
             style="?attr/buttonBarStyle"
             android:layout_width="match_parent"
@@ -77,7 +76,6 @@
             android:orientation="horizontal"
             android:layoutDirection="locale"
             android:measureWithLargestChild="true"
-            android:paddingTop="@dimen/resolver_button_bar_spacing"
             android:paddingBottom="@dimen/resolver_button_bar_spacing"
             android:paddingStart="@dimen/resolver_edge_margin"
             android:paddingEnd="@dimen/resolver_small_margin"
diff --git a/core/res/res/values-mcc262/config.xml b/core/res/res/values-mcc262/config.xml
index 79eefb7..d234061 100644
--- a/core/res/res/values-mcc262/config.xml
+++ b/core/res/res/values-mcc262/config.xml
@@ -21,5 +21,5 @@
      for different hardware and product builds. -->
 <resources>
   <!-- Set to false to disable emergency alert. -->
-  <bool name="config_cellBroadcastAppLinks">false</bool>
+  <bool name="config_cellBroadcastAppLinks">true</bool>
 </resources>
\ No newline at end of file
diff --git a/core/res/res/values-night/themes_device_defaults.xml b/core/res/res/values-night/themes_device_defaults.xml
index 93975a9..366e6f9 100644
--- a/core/res/res/values-night/themes_device_defaults.xml
+++ b/core/res/res/values-night/themes_device_defaults.xml
@@ -90,4 +90,13 @@
         <item name="colorBackgroundFloating">@android:color/GM2_grey_800</item>
         <item name="textColorSecondary">@android:color/resolver_text_color_secondary_dark</item>
     </style>
+
+    <!-- @hide DeviceDefault theme for the Autofill half screen dialog UI.  -->
+    <style name="Theme.DeviceDefault.AutofillHalfScreenDialogButton" parent="Theme.DeviceDefault.Panel">
+        <item name="selectableItemBackground">@android:color/transparent</item>
+        <item name="colorAccentPrimaryVariant">@color/system_accent1_200</item>
+        <item name="colorAccentPrimary">@color/system_accent1_700</item>
+        <item name="textColorPrimary">@color/system_neutral1_100</item>
+        <item name="textColorSecondary">@color/system_neutral2_100</item>
+    </style>
 </resources>
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index 5df3dde..b515abc 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -448,5 +448,6 @@
     <color name="accessibility_color_inversion_background">#546E7A</color>
 
     <!-- Color of camera light when camera is in use -->
-    <color name="camera_privacy_light">#FFFFFF</color>
+    <color name="camera_privacy_light_day">#FFFFFF</color>
+    <color name="camera_privacy_light_night">#FFFFFF</color>
 </resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 689ff66..b139e47 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1869,6 +1869,11 @@
     -->
     <string name="config_defaultSearchSelectorPackageName" translatable="false"></string>
 
+    <!-- The package name of the default captive portal login app. Must be granted the
+         POST_NOTIFICATIONS permission.
+    -->
+    <string name="config_defaultCaptivePortalLoginPackageName" translatable="false"></string>
+
     <!-- Whether to enable geocoder overlay which allows geocoder to be replaced
          by an app at run-time. When disabled, only the
          config_geocoderProviderPackageName package will be searched for
@@ -2998,12 +3003,6 @@
 
     </string-array>
 
-    <!-- When migrating notification settings into the permission framework, whether all existing
-         apps should be marked as 'user-set' (true) or whether only the apps that have explicitly
-         modified notification settings should be marked as 'user-set' (false). Users will not see
-         system generated permission prompts for 'user-set' apps. -->
-    <bool name="config_notificationForceUserSetOnUpgrade">true</bool>
-
     <!-- Default Gravity setting for the system Toast view. Equivalent to: Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM -->
     <integer name="config_toastDefaultGravity">0x00000051</integer>
 
@@ -4895,7 +4894,7 @@
     <!-- Whether or not to hide the navigation bar when the soft keyboard is visible in order to
          create additional screen real estate outside beyond the keyboard. Note that the user needs
          to have a confirmed way to dismiss the keyboard when desired. -->
-    <bool name="config_automotiveHideNavBarForKeyboard">false</bool>
+    <bool name="config_hideNavBarForKeyboard">false</bool>
 
     <!-- Whether or not to show the built-in charging animation when the device begins charging
          wirelessly. -->
@@ -5662,6 +5661,9 @@
     <!-- Whether or not to enable the lock screen entry point for the QR code scanner. -->
     <bool name="config_enableQrCodeScannerOnLockScreen">false</bool>
 
+    <!-- Default component for QR code scanner -->
+    <string name="config_defaultQrCodeComponent"></string>
+
     <!-- Whether Low Power Standby is supported and can be enabled. -->
     <bool name="config_lowPowerStandbySupported">false</bool>
 
@@ -5752,6 +5754,14 @@
     -->
     <integer name="config_bg_current_drain_location_min_duration">1800</integer>
 
+    <!-- The behavior when the system detects it has abusive current drains, whether or not to
+         move the app to the restricted standby bucket level.
+         True - we'll move the app to restricted standby bucket as long as its bg battery usage
+         goes beyond the threshold, False - we'll not move it.
+         Note: This should be only enabled on devices with high confidence on power measurement.
+    -->
+    <bool name="config_bg_current_drain_auto_restrict_abusive_apps">false</bool>
+
     <!-- The behavior for an app with a FGS and its notification is still showing, when the system
          detects it's abusive and should be put into bg restricted level. True - we'll
          show the prompt to user, False - we'll not show it.
@@ -5787,4 +5797,17 @@
 
     <!-- List of the labels of requestable device state config values -->
     <string-array name="config_deviceStatesAvailableForAppRequests"/>
+
+    <!-- Interval in milliseconds to average light sensor values for camera light brightness -->
+    <integer name="config_cameraPrivacyLightAlsAveragingIntervalMillis">3000</integer>
+    <!-- Light sensor's lux value to use as the threshold between using day or night brightness -->
+    <integer name="config_cameraPrivacyLightAlsNightThreshold">4</integer>
+
+    <!-- List of system components which are allowed to receive ServiceState entries in an
+         un-sanitized form, even if the location toggle is off. This is intended ONLY for system
+         components, such as the telephony stack, which require access to the full ServiceState for
+         tasks such as network registration. -->
+    <string-array name="config_serviceStateLocationAllowedPackages">
+        <item>"com.android.phone"</item>
+    </string-array>
 </resources>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index b73f96e..211bf7f 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -873,6 +873,7 @@
     <dimen name="autofill_save_title_start_padding">8dp</dimen>
     <dimen name="autofill_save_scroll_view_top_margin">4dp</dimen>
     <dimen name="autofill_save_button_bar_padding">16dp</dimen>
+    <dimen name="autofill_dialog_corner_radius">28dp</dimen>
 
     <!-- Max height of the the autofill save custom subtitle as a fraction of the screen width/height -->
     <dimen name="autofill_save_custom_subtitle_max_height">20%</dimen>
@@ -888,7 +889,7 @@
     <integer name="autofill_max_visible_datasets">3</integer>
 
     <!-- Size of an icon in the Autolfill fill dialog -->
-    <dimen name="autofill_dialog_icon_size">24dp</dimen>
+    <dimen name="autofill_dialog_icon_size">32dp</dimen>
 
     <!-- Size of a slice shortcut view -->
     <dimen name="slice_shortcut_size">56dp</dimen>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 824dd8b..e5d90f0 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -876,9 +876,9 @@
     <string name="permgroupdesc_sms">send and view SMS messages</string>
 
     <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. -->
-    <string name="permgrouplab_storage">Files and documents</string>
+    <string name="permgrouplab_storage">Files</string>
     <!-- Description of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. -->
-    <string name="permgroupdesc_storage">access files and documents on your device</string>
+    <string name="permgroupdesc_storage">access files on your device</string>
 
     <!-- Title of a category of application permissions, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=40]-->
     <string name="permgrouplab_readMediaAural">Music and audio</string>
@@ -6313,5 +6313,5 @@
     <string name="vdm_camera_access_denied" product="tablet">Can’t access the tablet’s camera from your <xliff:g id="device" example="Chromebook">%1$s</xliff:g></string>
 
     <!-- Title for preference of the system default locale. [CHAR LIMIT=50]-->
-    <string name="system_locale_title">System language</string>
+    <string name="system_locale_title">System default</string>
 </resources>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index c07404b..0c35f93 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1464,6 +1464,31 @@
         <item name="successColor">@color/lock_pattern_view_success_color</item>
     </style>
 
+    <!-- @hide The style for Autofill half screen dialog button -->
+    <style name="AutofillHalfSheetButton" parent="Widget.Material.Button">
+        <item name="textSize">14sp</item>
+        <item name="fontFamily">google-sans-text-medium</item>
+        <item name="textStyle">normal</item>
+        <item name="textAllCaps">false</item>
+        <item name="layout_marginTop">6dp</item>
+        <item name="layout_marginBottom">6dp</item>
+        <item name="paddingStart">16dp</item>
+        <item name="paddingEnd">16dp</item>
+        <item name="paddingTop">8dp</item>
+        <item name="paddingBottom">8dp</item>
+    </style>
+    <!-- @hide Tonal button for Autofill half screen dialog -->
+    <style name="AutofillHalfSheetTonalButton" parent="AutofillHalfSheetButton">
+        <item name="textColor">?attr/textColorPrimary</item>
+        <item name="background">@drawable/btn_tonal</item>
+        <item name="stateListAnimator">@null</item>
+    </style>
+    <!-- @hide Outline button for Autofill half screen dialog -->
+    <style name="AutofillHalfSheetOutlinedButton" parent="AutofillHalfSheetButton">
+        <item name="textColor">?attr/textColorPrimary</item>
+        <item name="background">@drawable/btn_outlined</item>
+    </style>
+
     <!-- @hide Autofill background for popup window (not for fullscreen) -->
     <style name="AutofillDatasetPicker">
         <item name="elevation">4dp</item>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 443f9a6..77007afc 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3419,6 +3419,9 @@
   <!-- Search Selector -->
   <java-symbol type="string" name="config_defaultSearchSelectorPackageName" />
 
+  <!-- Captive Portal Login -->
+  <java-symbol type="string" name="config_defaultCaptivePortalLoginPackageName" />
+
   <!-- Optional IPsec algorithms -->
   <java-symbol type="array" name="config_optionalIpSecAlgorithms" />
 
@@ -4151,7 +4154,7 @@
   <java-symbol type="bool" name="config_disable_all_cb_messages" />
   <java-symbol type="drawable" name="ic_close" />
 
-  <java-symbol type="bool" name="config_automotiveHideNavBarForKeyboard" />
+  <java-symbol type="bool" name="config_hideNavBarForKeyboard" />
 
   <java-symbol type="bool" name="config_showBuiltinWirelessChargingAnim" />
 
@@ -4708,6 +4711,7 @@
 
   <java-symbol type="string" name="config_wearSysUiPackage"/>
   <java-symbol type="string" name="config_wearSysUiMainActivity"/>
+  <java-symbol type="string" name="config_defaultQrCodeComponent"/>
 
   <java-symbol type="dimen" name="secondary_rounded_corner_radius" />
   <java-symbol type="dimen" name="secondary_rounded_corner_radius_top" />
@@ -4753,7 +4757,10 @@
   <!-- For VirtualDeviceManager -->
   <java-symbol type="string" name="vdm_camera_access_denied" />
 
-  <java-symbol type="color" name="camera_privacy_light"/>
+  <java-symbol type="color" name="camera_privacy_light_day"/>
+  <java-symbol type="color" name="camera_privacy_light_night"/>
+  <java-symbol type="integer" name="config_cameraPrivacyLightAlsAveragingIntervalMillis"/>
+  <java-symbol type="integer" name="config_cameraPrivacyLightAlsNightThreshold"/>
 
   <java-symbol type="bool" name="config_bg_current_drain_monitor_enabled" />
   <java-symbol type="array" name="config_bg_current_drain_threshold_to_restricted_bucket" />
@@ -4767,13 +4774,14 @@
   <java-symbol type="array" name="config_bg_current_drain_high_threshold_to_bg_restricted" />
   <java-symbol type="integer" name="config_bg_current_drain_media_playback_min_duration" />
   <java-symbol type="integer" name="config_bg_current_drain_location_min_duration" />
+  <java-symbol type="bool" name="config_bg_current_drain_auto_restrict_abusive_apps" />
   <java-symbol type="bool" name="config_bg_prompt_fgs_with_noti_to_bg_restricted" />
   <java-symbol type="bool" name="config_bg_prompt_abusive_apps_to_bg_restricted" />
   <java-symbol type="integer" name="config_bg_current_drain_exempted_types" />
   <java-symbol type="bool" name="config_bg_current_drain_high_threshold_by_bg_location" />
   <java-symbol type="drawable" name="ic_swap_horiz" />
-  <java-symbol type="bool" name="config_notificationForceUserSetOnUpgrade" />
   <java-symbol type="array" name="config_deviceStatesAvailableForAppRequests" />
+  <java-symbol type="array" name="config_serviceStateLocationAllowedPackages" />
 
   <!-- For app language picker -->
   <java-symbol type="string" name="system_locale_title" />
diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml
index 9d0a42d8..5000b58 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -2695,4 +2695,13 @@
     <style name="Theme.DeviceDefault.DocumentsUI" parent="Theme.DeviceDefault.DayNight">
         <item name="actionModeCloseDrawable">@drawable/ic_clear_material</item>
     </style>
+
+    <!-- @hide DeviceDefault theme for the Autofill half screen dialog UI.  -->
+    <style name="Theme.DeviceDefault.AutofillHalfScreenDialogButton" parent="Theme.DeviceDefault.Panel">
+        <item name="selectableItemBackground">@android:color/transparent</item>
+        <item name="colorAccentPrimaryVariant">@color/system_accent1_600</item>
+        <item name="colorAccentPrimary">@color/system_accent1_100</item>
+        <item name="textColorPrimary">@color/system_neutral1_900</item>
+        <item name="textColorSecondary">@color/system_neutral2_700</item>
+    </style>
 </resources>
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index 8276d10..0eca0a8 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -425,7 +425,7 @@
 
         @Override
         public void bindApplication(String s, ApplicationInfo applicationInfo,
-                String sdkSandboxClientAppPackage,
+                String sdkSandboxClientAppVolumeUuid, String sdkSandboxClientAppPackage,
                 ProviderInfoList list, ComponentName componentName, ProfilerInfo profilerInfo,
                 Bundle bundle, IInstrumentationWatcher iInstrumentationWatcher,
                 IUiAutomationConnection iUiAutomationConnection, int i, boolean b, boolean b1,
diff --git a/core/tests/coretests/src/android/content/pm/AppSearchShortcutInfoTest.java b/core/tests/coretests/src/android/content/pm/AppSearchShortcutInfoTest.java
index 969357f..22feecb 100644
--- a/core/tests/coretests/src/android/content/pm/AppSearchShortcutInfoTest.java
+++ b/core/tests/coretests/src/android/content/pm/AppSearchShortcutInfoTest.java
@@ -24,7 +24,6 @@
 import android.platform.test.annotations.Presubmit;
 import android.util.ArraySet;
 
-import org.junit.Ignore;
 import org.junit.Test;
 
 import java.util.Set;
@@ -32,7 +31,6 @@
 @Presubmit
 public class AppSearchShortcutInfoTest {
 
-    @Ignore("b/208375334")
     @Test
     public void testBuildShortcutAndGetValue() {
         final String category =
@@ -51,7 +49,7 @@
         final Intent shortcutIntent = new Intent(Intent.ACTION_VIEW);
         final ShortcutInfo shortcut = new AppSearchShortcutInfo.Builder(/*packageName=*/"", id)
                 .setActivity(activity)
-                .setLongLabel(id)
+                .setShortLabel(id)
                 .setIconResName(shortcutIconResName)
                 .setIntent(shortcutIntent)
                 .setPerson(person)
@@ -64,11 +62,13 @@
         assertThat(shortcut.getId()).isEqualTo(id);
         assertThat(shortcut.getShortLabel()).isEqualTo(id);
         assertThat(shortcut.getIconResName()).isEqualTo(shortcutIconResName);
-        assertThat(shortcut.getIntent().toString()).isEqualTo(shortcut.toString());
+        assertThat(shortcut.getIntent().toString()).isEqualTo(shortcutIntent.toString());
         assertThat(shortcut.getPersons().length).isEqualTo(1);
-        assertThat(shortcut.getPersons()[0]).isEqualTo(person);
+        final Person target = shortcut.getPersons()[0];
+        assertThat(target.getName()).isEqualTo(person.getName());
+        assertThat(target.isBot()).isEqualTo(person.isBot());
+        assertThat(target.isImportant()).isEqualTo(person.isImportant());
         assertThat(shortcut.getCategories()).isEqualTo(categorySet);
-        assertThat(shortcut.getFlags()).isEqualTo(ShortcutInfo.FLAG_LONG_LIVED);
         assertThat(shortcut.getActivity()).isEqualTo(activity);
     }
 }
diff --git a/core/tests/coretests/src/android/view/RenderNodeAnimatorTest.java b/core/tests/coretests/src/android/view/RenderNodeAnimatorTest.java
index 786c22b..9b6bcda 100644
--- a/core/tests/coretests/src/android/view/RenderNodeAnimatorTest.java
+++ b/core/tests/coretests/src/android/view/RenderNodeAnimatorTest.java
@@ -19,17 +19,24 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.app.Activity;
 import android.content.Context;
+import android.widget.FrameLayout;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.rule.ActivityTestRule;
 
+import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
 @MediumTest
 public class RenderNodeAnimatorTest  {
     @Rule
@@ -57,4 +64,46 @@
         anim.start(); // should initialize mTransformationInfo
         assertNotNull(view.mTransformationInfo);
     }
+
+    @Test
+    public void testViewDetachCancelsRenderNodeAnimator() {
+        // Start a RenderNodeAnimator with a long duration time, then detach the target view
+        // before the animation completes. Detaching of a View from a window should force cancel all
+        // RenderNodeAnimators
+        CountDownLatch latch = new CountDownLatch(1);
+
+        FrameLayout container = new FrameLayout(getContext());
+        View view = new View(getContext());
+
+        getActivity().runOnUiThread(() -> {
+            container.addView(view, new FrameLayout.LayoutParams(100, 100));
+            getActivity().setContentView(container);
+        });
+        getActivity().runOnUiThread(() -> {
+            RenderNodeAnimator anim = new RenderNodeAnimator(0, 0, 10f, 30f);
+            anim.setDuration(10000);
+            anim.setTarget(view);
+            anim.addListener(new AnimatorListenerAdapter() {
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    super.onAnimationEnd(animation);
+                    latch.countDown();
+                }
+            });
+
+            anim.start();
+        });
+
+        getActivity().runOnUiThread(()-> {
+            container.removeView(view);
+        });
+
+        try {
+            Assert.assertTrue("onAnimationEnd not invoked",
+                    latch.await(3000, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException excep) {
+            Assert.fail("Interrupted waiting for onAnimationEnd callback");
+        }
+    }
 }
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
index 0c009a0..4cf9c3f 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
@@ -39,6 +39,10 @@
     static final OverrideData sOverrides = new OverrideData();
     private UsageStatsManager mUsm;
 
+    public ResolverWrapperActivity() {
+        super(/* isIntentPicker= */ true);
+    }
+
     @Override
     public ResolverListAdapter createResolverListAdapter(Context context,
             List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
index 87c45dc..d19f9f5 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
@@ -47,6 +47,7 @@
 import android.telephony.ModemActivityInfo;
 import android.telephony.ServiceState;
 import android.telephony.TelephonyManager;
+import android.util.MutableInt;
 import android.util.SparseIntArray;
 import android.util.SparseLongArray;
 import android.view.Display;
@@ -1982,6 +1983,54 @@
                 expectedTxDurationsMs, bi, state.currentTimeMs);
     }
 
+    @SmallTest
+    @SuppressWarnings("GuardedBy")
+    public void testProcStateSyncScheduling_mobileRadioActiveState() {
+        final MockClock clock = new MockClock(); // holds realtime and uptime in ms
+        final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock);
+        final MutableInt lastProcStateChangeFlags = new MutableInt(0);
+
+        MockBatteryStatsImpl.DummyExternalStatsSync externalStatsSync =
+                new MockBatteryStatsImpl.DummyExternalStatsSync() {
+                    @Override
+                    public void scheduleSyncDueToProcessStateChange(int flags,
+                            long delayMillis) {
+                        lastProcStateChangeFlags.value = flags;
+                    }
+                };
+
+        bi.setDummyExternalStatsSync(externalStatsSync);
+
+        bi.updateTimeBasesLocked(true, Display.STATE_OFF, 0, 0);
+
+        // Note mobile radio is on.
+        long curr = 1000L * (clock.realtime = clock.uptime = 1001);
+        bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr,
+                UID);
+
+        lastProcStateChangeFlags.value = 0;
+        clock.realtime = clock.uptime = 2002;
+        bi.noteUidProcessStateLocked(UID, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND);
+
+        final int allProcFlags = BatteryStatsImpl.ExternalStatsSync.UPDATE_ON_PROC_STATE_CHANGE;
+        assertEquals(allProcFlags, lastProcStateChangeFlags.value);
+
+        // Note mobile radio is off.
+        curr = 1000L * (clock.realtime = clock.uptime = 3003);
+        bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, curr,
+                UID);
+
+        lastProcStateChangeFlags.value = 0;
+        clock.realtime = clock.uptime = 4004;
+        bi.noteUidProcessStateLocked(UID, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
+
+        final int noRadioProcFlags = BatteryStatsImpl.ExternalStatsSync.UPDATE_ON_PROC_STATE_CHANGE
+                & ~BatteryStatsImpl.ExternalStatsSync.UPDATE_RADIO;
+        assertEquals(
+                "An inactive radio should not be queried on proc state change",
+                noRadioProcFlags, lastProcStateChangeFlags.value);
+    }
+
     private void setFgState(int uid, boolean fgOn, MockBatteryStatsImpl bi) {
         // Note that noteUidProcessStateLocked uses ActivityManager process states.
         if (fgOn) {
diff --git a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
index 00154a3..edeb5e9 100644
--- a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
+++ b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
@@ -212,7 +212,12 @@
         return flags;
     }
 
-    private class DummyExternalStatsSync implements ExternalStatsSync {
+    public void setDummyExternalStatsSync(DummyExternalStatsSync externalStatsSync) {
+        mExternalStatsSync = externalStatsSync;
+        setExternalStatsSyncLocked(mExternalStatsSync);
+    }
+
+    public static class DummyExternalStatsSync implements ExternalStatsSync {
         public int flags = 0;
 
         @Override
@@ -257,8 +262,7 @@
         }
 
         @Override
-        public Future<?> scheduleSyncDueToProcessStateChange(long delayMillis) {
-            return null;
+        public void scheduleSyncDueToProcessStateChange(int flags, long delayMillis) {
         }
     }
 }
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index ebf5832..f030d80 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -80,5 +80,6 @@
         <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
         <permission name="android.permission.READ_DEVICE_CONFIG" />
         <permission name="android.permission.READ_SAFETY_CENTER_STATUS" />
+        <permission name="android.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS" />
     </privapp-permissions>
 </permissions>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 04ead1b..58a2073 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -33,30 +33,6 @@
         <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
     </privapp-permissions>
 
-    <privapp-permissions package="com.android.bluetooth.services">
-        <permission name="android.permission.DUMP"/>
-        <permission name="android.permission.MODIFY_AUDIO_ROUTING"/>
-        <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
-        <permission name="android.permission.TETHER_PRIVILEGED"/>
-        <permission name="android.permission.CALL_PRIVILEGED"/>
-        <permission name="android.permission.MODIFY_PHONE_STATE"/>
-        <permission name="android.permission.INTERACT_ACROSS_USERS"/>
-        <permission name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
-        <permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
-        <permission name="android.permission.UPDATE_DEVICE_STATS"/>
-        <permission name="android.permission.PACKAGE_USAGE_STATS"/>
-        <permission name="android.permission.NFC_HANDOVER_STATUS"/>
-        <permission name="android.permission.CONNECTIVITY_INTERNAL"/>
-        <permission name="android.permission.BLUETOOTH_PRIVILEGED"/>
-        <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
-        <permission name="android.permission.MEDIA_CONTENT_CONTROL"/>
-        <permission name="android.permission.REAL_GET_TASKS"/>
-        <permission name="android.permission.MANAGE_USERS"/>
-        <permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"/>
-        <permission name="android.permission.WRITE_APN_SETTINGS"/>
-        <permission name="android.permission.UPDATE_APP_OPS_STATS"/>
-    </privapp-permissions>
-
     <privapp-permissions package="com.android.backupconfirm">
         <permission name="android.permission.BACKUP"/>
         <permission name="android.permission.CRYPT_KEEPER"/>
diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java
index 5fd53ad..dadbd8d 100644
--- a/graphics/java/android/graphics/RenderNode.java
+++ b/graphics/java/android/graphics/RenderNode.java
@@ -1611,6 +1611,11 @@
         nEndAllAnimators(mNativeRenderNode);
     }
 
+    /** @hide */
+    public void forceEndAnimators() {
+        nForceEndAnimators(mNativeRenderNode);
+    }
+
     ///////////////////////////////////////////////////////////////////////////
     // Regular JNI methods
     ///////////////////////////////////////////////////////////////////////////
@@ -1633,6 +1638,8 @@
 
     private static native void nEndAllAnimators(long renderNode);
 
+    private static native void nForceEndAnimators(long renderNode);
+
     ///////////////////////////////////////////////////////////////////////////
     // @CriticalNative methods
     ///////////////////////////////////////////////////////////////////////////
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java
index 9ad6f3a..6fff52a 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreBCWorkaroundProvider.java
@@ -206,6 +206,8 @@
 
         putSignatureImpl("NONEwithECDSA",
                 PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$NONE");
+        putSignatureImpl("Ed25519",
+                PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$Ed25519");
 
         putSignatureImpl("SHA1withECDSA", PACKAGE_NAME + ".AndroidKeyStoreECDSASignatureSpi$SHA1");
         put("Alg.Alias.Signature.ECDSA", "SHA1withECDSA");
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java
index 8289671..5216a90 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECDSASignatureSpi.java
@@ -29,7 +29,10 @@
 import java.io.ByteArrayOutputStream;
 import java.security.InvalidKeyException;
 import java.security.SignatureSpi;
+import java.security.spec.NamedParameterSpec;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Base class for {@link SignatureSpi} providing Android KeyStore backed ECDSA signatures.
@@ -37,6 +40,10 @@
  * @hide
  */
 abstract class AndroidKeyStoreECDSASignatureSpi extends AndroidKeyStoreSignatureSpiBase {
+    private static final Set<String> ACCEPTED_SIGNING_SCHEMES = Set.of(
+            KeyProperties.KEY_ALGORITHM_EC.toLowerCase(),
+            NamedParameterSpec.ED25519.getName().toLowerCase(),
+            "eddsa");
 
     public final static class NONE extends AndroidKeyStoreECDSASignatureSpi {
         public NONE() {
@@ -114,6 +121,18 @@
         }
     }
 
+    public static final class Ed25519 extends AndroidKeyStoreECDSASignatureSpi {
+        public Ed25519() {
+            // Ed25519 uses an internal digest system.
+            super(KeymasterDefs.KM_DIGEST_NONE);
+        }
+
+        @Override
+        protected String getAlgorithm() {
+            return NamedParameterSpec.ED25519.getName();
+        }
+    }
+
     public final static class SHA1 extends AndroidKeyStoreECDSASignatureSpi {
         public SHA1() {
             super(KeymasterDefs.KM_DIGEST_SHA1);
@@ -174,9 +193,10 @@
 
     @Override
     protected final void initKey(AndroidKeyStoreKey key) throws InvalidKeyException {
-        if (!KeyProperties.KEY_ALGORITHM_EC.equalsIgnoreCase(key.getAlgorithm())) {
+        if (!ACCEPTED_SIGNING_SCHEMES.contains(key.getAlgorithm().toLowerCase())) {
             throw new InvalidKeyException("Unsupported key algorithm: " + key.getAlgorithm()
-                    + ". Only" + KeyProperties.KEY_ALGORITHM_EC + " supported");
+                    + ". Only" + Arrays.toString(ACCEPTED_SIGNING_SCHEMES.stream().toArray())
+                    + " supported");
         }
 
         long keySizeBits = -1;
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
index fc963a8..b1338d1 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java
@@ -61,6 +61,17 @@
         }
     }
 
+    /**
+     * X25519 key agreement support.
+     *
+     * @hide
+     */
+    public static class XDH extends AndroidKeyStoreKeyAgreementSpi {
+        public XDH() {
+            super(Algorithm.EC);
+        }
+    }
+
     private final int mKeymintAlgorithm;
 
     // Fields below are populated by engineInit and should be preserved after engineDoFinal.
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
index 40659f5..cdc1085 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
@@ -712,7 +712,7 @@
                 case KeymasterDefs.KM_ERROR_HARDWARE_TYPE_UNAVAILABLE:
                     throw new StrongBoxUnavailableException("Failed to generated key pair.", e);
                 case ResponseCode.OUT_OF_KEYS:
-                    throw makeOutOfKeysException(e, securityLevel);
+                    return checkIfRetryableOrThrow(e, securityLevel);
                 default:
                     ProviderException p = new ProviderException("Failed to generate key pair.", e);
                     if ((mSpec.getPurposes() & KeyProperties.PURPOSE_WRAP_KEY) != 0) {
@@ -740,7 +740,7 @@
 
     // In case keystore reports OUT_OF_KEYS, call this handler in an attempt to remotely provision
     // some keys.
-    private ProviderException makeOutOfKeysException(KeyStoreException e, int securityLevel) {
+    GenerateKeyPairHelperResult checkIfRetryableOrThrow(KeyStoreException e, int securityLevel) {
         GenerateRkpKey keyGen = new GenerateRkpKey(ActivityThread
                 .currentApplication());
         KeyStoreException ksException;
@@ -757,8 +757,11 @@
                     rkpStatus = KeyStoreException.RKP_SERVER_REFUSED_ISSUANCE;
                     break;
                 case IGenerateRkpKeyService.Status.OK:
-                    // This will actually retry once immediately, so on "OK" go ahead and return
-                    // "temporarily unavailable". @see generateKeyPair
+                    // Explicitly return not-OK here so we retry in generateKeyPair. All other cases
+                    // should throw because a retry doesn't make sense if we didn't actually
+                    // provision fresh keys.
+                    return new GenerateKeyPairHelperResult(
+                            KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE, null);
                 case IGenerateRkpKeyService.Status.NETWORK_COMMUNICATION_ERROR:
                 case IGenerateRkpKeyService.Status.HTTP_CLIENT_ERROR:
                 case IGenerateRkpKeyService.Status.HTTP_SERVER_ERROR:
@@ -781,7 +784,7 @@
                     KeyStoreException.RKP_TEMPORARILY_UNAVAILABLE);
         }
         ksException.initCause(e);
-        return new ProviderException("Failed to talk to RemoteProvisioner", ksException);
+        throw new ProviderException("Failed to provision new attestation keys.", ksException);
     }
 
     private void addAttestationParameters(@NonNull List<KeyParameter> params)
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
index 0355628..9947d34 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreProvider.java
@@ -104,6 +104,7 @@
 
         // javax.crypto.KeyAgreement
         put("KeyAgreement.ECDH", PACKAGE_NAME + ".AndroidKeyStoreKeyAgreementSpi$ECDH");
+        put("KeyAgreement.XDH", PACKAGE_NAME + ".AndroidKeyStoreKeyAgreementSpi$XDH");
 
         // java.security.SecretKeyFactory
         putSecretKeyFactoryImpl("AES");
@@ -235,8 +236,8 @@
             return new AndroidKeyStoreEdECPublicKey(descriptor, metadata, ED25519_OID,
                     iSecurityLevel, publicKeyEncoded);
         } else if (X25519_ALIAS.equalsIgnoreCase(jcaKeyAlgorithm)) {
-            //TODO(b/214203951) missing classes in conscrypt
-            throw new ProviderException("Curve " + X25519_ALIAS + " not supported yet");
+            return new AndroidKeyStoreXDHPublicKey(descriptor, metadata, X25519_ALIAS,
+                    iSecurityLevel, publicKey.getEncoded());
         } else {
             throw new ProviderException("Unsupported Android Keystore public key algorithm: "
                     + jcaKeyAlgorithm);
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java
new file mode 100644
index 0000000..4258964
--- /dev/null
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java
@@ -0,0 +1,47 @@
+/*
+ * 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 android.security.keystore2;
+
+import android.annotation.NonNull;
+import android.security.KeyStoreSecurityLevel;
+import android.system.keystore2.Authorization;
+import android.system.keystore2.KeyDescriptor;
+
+import java.security.PrivateKey;
+import java.security.interfaces.EdECKey;
+import java.security.spec.NamedParameterSpec;
+
+/**
+ * X25519 Private Key backed by Keystore.
+ * instance of {@link PrivateKey} and {@link EdECKey}
+ *
+ * @hide
+ */
+public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey implements EdECKey {
+    public AndroidKeyStoreXDHPrivateKey(
+            @NonNull KeyDescriptor descriptor, long keyId,
+            @NonNull Authorization[] authorizations,
+            @NonNull String algorithm,
+            @NonNull KeyStoreSecurityLevel securityLevel) {
+        super(descriptor, keyId, authorizations, algorithm, securityLevel);
+    }
+
+    @Override
+    public NamedParameterSpec getParams() {
+        return NamedParameterSpec.X25519;
+    }
+}
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPublicKey.java
new file mode 100644
index 0000000..9f3df3d
--- /dev/null
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPublicKey.java
@@ -0,0 +1,120 @@
+/*
+ * 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 android.security.keystore2;
+
+import android.annotation.NonNull;
+import android.security.KeyStoreSecurityLevel;
+import android.system.keystore2.KeyDescriptor;
+import android.system.keystore2.KeyMetadata;
+
+import java.math.BigInteger;
+import java.security.interfaces.XECPublicKey;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.NamedParameterSpec;
+import java.util.Arrays;
+
+/**
+ * {@link XECPublicKey} backed by keystore.
+ * This class re-implements Conscrypt's OpenSSLX25519PublicKey. The reason is that
+ * OpenSSLX25519PublicKey does not implement XECPublicKey and is not a part of Conscrypt's public
+ * interface so it cannot be referred to.
+ *
+ * So the functionality is duplicated here until (likely Android U) one of the things mentioned
+ * above is fixed.
+ *
+ * @hide
+ */
+public class AndroidKeyStoreXDHPublicKey extends AndroidKeyStorePublicKey implements XECPublicKey {
+    private static final byte[] X509_PREAMBLE = new byte[] {
+            0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x03, 0x21, 0x00,
+    };
+
+    private static final byte[] X509_PREAMBLE_WITH_NULL = new byte[] {
+            0x30, 0x2C, 0x30, 0x07, 0x06, 0x03, 0x2B, 0x65, 0x6E, 0x05, 0x00, 0x03, 0x21, 0x00,
+    };
+
+    private static final int X25519_KEY_SIZE_BYTES = 32;
+
+    private final byte[] mEncodedKey;
+    private final int mPreambleLength;
+
+    public AndroidKeyStoreXDHPublicKey(
+            @NonNull KeyDescriptor descriptor,
+            @NonNull KeyMetadata metadata,
+            @NonNull String algorithm,
+            @NonNull KeyStoreSecurityLevel iSecurityLevel,
+            @NonNull byte[] encodedKey) {
+        super(descriptor, metadata, encodedKey, algorithm, iSecurityLevel);
+        mEncodedKey = encodedKey;
+        if (mEncodedKey == null) {
+            throw new IllegalArgumentException("empty encoded key.");
+        }
+
+        mPreambleLength = matchesPreamble(X509_PREAMBLE, mEncodedKey) | matchesPreamble(
+                X509_PREAMBLE_WITH_NULL, mEncodedKey);
+        if (mPreambleLength == 0) {
+            throw new IllegalArgumentException("Key size is not correct size");
+        }
+    }
+
+    private static int matchesPreamble(byte[] preamble, byte[] encoded) {
+        if (encoded.length != (preamble.length + X25519_KEY_SIZE_BYTES)) {
+            return 0;
+        }
+
+        if (Arrays.compare(preamble, 0, preamble.length, encoded, 0, preamble.length) != 0) {
+            return 0;
+        }
+        return preamble.length;
+    }
+
+    @Override
+    AndroidKeyStorePrivateKey getPrivateKey() {
+        return new AndroidKeyStoreXDHPrivateKey(
+                getUserKeyDescriptor(),
+                getKeyIdDescriptor().nspace,
+                getAuthorizations(),
+                "x25519",
+                getSecurityLevel());
+    }
+
+    @Override
+    public BigInteger getU() {
+        return new BigInteger(Arrays.copyOfRange(mEncodedKey, mPreambleLength, mEncodedKey.length));
+    }
+
+    @Override
+    public byte[] getEncoded() {
+        return mEncodedKey.clone();
+    }
+
+    @Override
+    public String getAlgorithm() {
+        return "XDH";
+    }
+
+    @Override
+    public String getFormat() {
+        return "x.509";
+    }
+
+    @Override
+    public AlgorithmParameterSpec getParams() {
+        return NamedParameterSpec.X25519;
+    }
+}
+
diff --git a/libs/WindowManager/Jetpack/src/TEST_MAPPING b/libs/WindowManager/Jetpack/src/TEST_MAPPING
index eacfe25..f8f6400 100644
--- a/libs/WindowManager/Jetpack/src/TEST_MAPPING
+++ b/libs/WindowManager/Jetpack/src/TEST_MAPPING
@@ -28,5 +28,10 @@
         }
       ]
     }
+  ],
+  "imports": [
+    {
+      "path": "vendor/google_testing/integration/tests/scenarios/src/android/platform/test/scenario/sysui"
+    }
   ]
-}
\ No newline at end of file
+}
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 9713c273..015205c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -32,6 +32,7 @@
 import android.app.ActivityOptions;
 import android.app.ActivityThread;
 import android.app.Instrumentation;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Configuration;
@@ -234,13 +235,45 @@
     public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
             @NonNull IBinder activityToken) {
         // If the activity belongs to the current app process, we treat it as a new activity launch.
-        final Activity activity = ActivityThread.currentActivityThread().getActivity(activityToken);
+        final Activity activity = getActivity(activityToken);
         if (activity != null) {
-            onActivityCreated(activity);
+            // We don't allow split as primary for new launch because we currently only support
+            // launching to top. We allow split as primary for activity reparent because the
+            // activity may be split as primary before it is reparented out. In that case, we want
+            // to show it as primary again when it is reparented back.
+            if (!resolveActivityToContainer(activity, true /* canSplitAsPrimary */)) {
+                // When there is no embedding rule matched, try to place it in the top container
+                // like a normal launch.
+                placeActivityInTopContainer(activity);
+            }
             updateCallbackIfNecessary();
             return;
         }
-        // TODO: handle for activity in other process.
+
+        final TaskContainer taskContainer = getTaskContainer(taskId);
+        if (taskContainer == null || taskContainer.isInPictureInPicture()) {
+            // We don't embed activity when it is in PIP.
+            return;
+        }
+
+        // If the activity belongs to a different app process, we treat it as starting new intent,
+        // since both actions might result in a new activity that should appear in an organized
+        // TaskFragment.
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId,
+                activityIntent, null /* launchingActivity */);
+        if (targetContainer == null) {
+            // When there is no embedding rule matched, try to place it in the top container like a
+            // normal launch.
+            targetContainer = taskContainer.getTopTaskFragmentContainer();
+        }
+        if (targetContainer == null) {
+            return;
+        }
+        wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), activityToken);
+        mPresenter.applyTransaction(wct);
+        // Because the activity does not belong to the organizer process, we wait until
+        // onTaskFragmentAppeared to trigger updateCallbackIfNecessary().
     }
 
     /** Called on receiving {@link #onTaskFragmentVanished(TaskFragmentInfo)} for cleanup. */
@@ -316,92 +349,244 @@
         return false;
     }
 
+    @VisibleForTesting
     void onActivityCreated(@NonNull Activity launchedActivity) {
-        handleActivityCreated(launchedActivity);
+        // TODO(b/229680885): we don't support launching into primary yet because we want to always
+        // launch the new activity on top.
+        resolveActivityToContainer(launchedActivity, false /* canSplitAsPrimary */);
         updateCallbackIfNecessary();
     }
 
     /**
-     * Checks if the activity start should be routed to a particular container. It can create a new
-     * container for the activity and a new split container if necessary.
+     * Checks if the new added activity should be routed to a particular container. It can create a
+     * new container for the activity and a new split container if necessary.
+     * @param launchedActivity  the new launched activity.
+     * @param canSplitAsPrimary whether we can put the new launched activity into primary split.
+     * @return {@code true} if the activity was placed in TaskFragment container.
      */
-    // TODO(b/190433398): Break down into smaller functions.
-    void handleActivityCreated(@NonNull Activity launchedActivity) {
-        if (isInPictureInPicture(launchedActivity)) {
-            // We don't embed activity when it is in PIP.
-            return;
-        }
-        final List<EmbeddingRule> splitRules = getSplitRules();
-        final TaskFragmentContainer currentContainer = getContainerWithActivity(
-                launchedActivity.getActivityToken());
-
-        // Check if the activity is configured to always be expanded.
-        if (shouldExpand(launchedActivity, null, splitRules)) {
-            if (shouldContainerBeExpanded(currentContainer)) {
-                // Make sure that the existing container is expanded
-                mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken());
-            } else {
-                // Put activity into a new expanded container
-                final TaskFragmentContainer newContainer = newContainer(launchedActivity,
-                        launchedActivity.getTaskId());
-                mPresenter.expandActivity(newContainer.getTaskFragmentToken(),
-                        launchedActivity);
-            }
-            return;
+    @VisibleForTesting
+    boolean resolveActivityToContainer(@NonNull Activity launchedActivity,
+            boolean canSplitAsPrimary) {
+        if (isInPictureInPicture(launchedActivity) || launchedActivity.isFinishing()) {
+            // We don't embed activity when it is in PIP, or finishing. Return true since we don't
+            // want any extra handling.
+            return true;
         }
 
-        // Check if activity requires a placeholder
+        /*
+         * We will check the following to see if there is any embedding rule matched:
+         * 1. Whether the new launched activity should always expand.
+         * 2. Whether the new launched activity should launch a placeholder.
+         * 3. Whether the new launched activity has already been in a split with a rule matched
+         *    (likely done in #onStartActivity).
+         * 4. Whether the activity below (if any) should be split with the new launched activity.
+         * 5. Whether the activity split with the activity below (if any) should be split with the
+         *    new launched activity.
+         */
+
+        // 1. Whether the new launched activity should always expand.
+        if (shouldExpand(launchedActivity, null /* intent */)) {
+            expandActivity(launchedActivity);
+            return true;
+        }
+
+        // 2. Whether the new launched activity should launch a placeholder.
         if (launchPlaceholderIfNecessary(launchedActivity)) {
-            return;
+            return true;
         }
 
-        // TODO(b/190433398): Check if it is a placeholder and there is already another split
-        // created by the primary activity. This is necessary for the case when the primary activity
-        // launched another secondary in the split, but the placeholder was still launched by the
-        // logic above. We didn't prevent the placeholder launcher because we didn't know that
-        // another secondary activity is coming up.
+        // 3. Whether the new launched activity has already been in a split with a rule matched.
+        if (isNewActivityInSplitWithRuleMatched(launchedActivity)) {
+            return true;
+        }
 
-        // Check if the activity should form a split with the activity below in the same task
-        // fragment.
+        // 4. Whether the activity below (if any) should be split with the new launched activity.
+        final Activity activityBelow = findActivityBelow(launchedActivity);
+        if (activityBelow == null) {
+            // Can't find any activity below.
+            return false;
+        }
+        if (putActivitiesIntoSplitIfNecessary(activityBelow, launchedActivity)) {
+            // Have split rule of [ activityBelow | launchedActivity ].
+            return true;
+        }
+        if (canSplitAsPrimary
+                && putActivitiesIntoSplitIfNecessary(launchedActivity, activityBelow)) {
+            // Have split rule of [ launchedActivity | activityBelow].
+            return true;
+        }
+
+        // 5. Whether the activity split with the activity below (if any) should be split with the
+        //    new launched activity.
+        final TaskFragmentContainer activityBelowContainer = getContainerWithActivity(
+                activityBelow);
+        final SplitContainer topSplit = getActiveSplitForContainer(activityBelowContainer);
+        if (topSplit == null || !isTopMostSplit(topSplit)) {
+            // Skip if it is not the topmost split.
+            return false;
+        }
+        final TaskFragmentContainer otherTopContainer =
+                topSplit.getPrimaryContainer() == activityBelowContainer
+                        ? topSplit.getSecondaryContainer()
+                        : topSplit.getPrimaryContainer();
+        final Activity otherTopActivity = otherTopContainer.getTopNonFinishingActivity();
+        if (otherTopActivity == null || otherTopActivity == launchedActivity) {
+            // Can't find the top activity on the other split TaskFragment.
+            return false;
+        }
+        if (putActivitiesIntoSplitIfNecessary(otherTopActivity, launchedActivity)) {
+            // Have split rule of [ otherTopActivity | launchedActivity ].
+            return true;
+        }
+        // Have split rule of [ launchedActivity | otherTopActivity].
+        return canSplitAsPrimary
+                && putActivitiesIntoSplitIfNecessary(launchedActivity, otherTopActivity);
+    }
+
+    /**
+     * Places the given activity to the top most TaskFragment in the task if there is any.
+     */
+    @VisibleForTesting
+    void placeActivityInTopContainer(@NonNull Activity activity) {
+        if (getContainerWithActivity(activity) != null) {
+            // The activity has already been put in a TaskFragment. This is likely to be done by
+            // the server when the activity is started.
+            return;
+        }
+        final int taskId = getTaskId(activity);
+        final TaskContainer taskContainer = getTaskContainer(taskId);
+        if (taskContainer == null) {
+            return;
+        }
+        final TaskFragmentContainer targetContainer = taskContainer.getTopTaskFragmentContainer();
+        if (targetContainer == null) {
+            return;
+        }
+        targetContainer.addPendingAppearedActivity(activity);
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(),
+                activity.getActivityToken());
+        mPresenter.applyTransaction(wct);
+    }
+
+    /**
+     * Expands the given activity by either expanding the TaskFragment it is currently in or putting
+     * it into a new expanded TaskFragment.
+     */
+    private void expandActivity(@NonNull Activity activity) {
+        final TaskFragmentContainer container = getContainerWithActivity(activity);
+        if (shouldContainerBeExpanded(container)) {
+            // Make sure that the existing container is expanded.
+            mPresenter.expandTaskFragment(container.getTaskFragmentToken());
+        } else {
+            // Put activity into a new expanded container.
+            final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity));
+            mPresenter.expandActivity(newContainer.getTaskFragmentToken(), activity);
+        }
+    }
+
+    /** Whether the given new launched activity is in a split with a rule matched. */
+    private boolean isNewActivityInSplitWithRuleMatched(@NonNull Activity launchedActivity) {
+        final TaskFragmentContainer container = getContainerWithActivity(launchedActivity);
+        final SplitContainer splitContainer = getActiveSplitForContainer(container);
+        if (splitContainer == null) {
+            return false;
+        }
+
+        if (container == splitContainer.getPrimaryContainer()) {
+            // The new launched can be in the primary container when it is starting a new activity
+            // onCreate, thus the secondary may still be empty.
+            final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
+            final Activity secondaryActivity = secondaryContainer.getTopNonFinishingActivity();
+            return secondaryActivity == null
+                    || getSplitRule(launchedActivity, secondaryActivity) != null;
+        }
+
+        // Check if the new launched activity is a placeholder.
+        if (splitContainer.getSplitRule() instanceof SplitPlaceholderRule) {
+            final SplitPlaceholderRule placeholderRule =
+                    (SplitPlaceholderRule) splitContainer.getSplitRule();
+            final ComponentName placeholderName = placeholderRule.getPlaceholderIntent()
+                    .getComponent();
+            // TODO(b/232330767): Do we have a better way to check this?
+            return placeholderName == null
+                    || placeholderName.equals(launchedActivity.getComponentName())
+                    || placeholderRule.getPlaceholderIntent().equals(launchedActivity.getIntent());
+        }
+
+        // Check if the new launched activity should be split with the primary top activity.
+        final Activity primaryActivity = splitContainer.getPrimaryContainer()
+                .getTopNonFinishingActivity();
+        if (primaryActivity == null) {
+            return false;
+        }
+        /* TODO(b/231845476) we should always respect clearTop.
+        final SplitPairRule curSplitRule = (SplitPairRule) splitContainer.getSplitRule();
+        final SplitPairRule splitRule = getSplitRule(primaryActivity, launchedActivity);
+        return splitRule != null && haveSamePresentation(splitRule, curSplitRule)
+                // If the new launched split rule should clear top and it is not the bottom most,
+                // it means we should create a new split pair and clear the existing secondary.
+                && (!splitRule.shouldClearTop()
+                || container.getBottomMostActivity() == launchedActivity);
+         */
+        return getSplitRule(primaryActivity, launchedActivity) != null;
+    }
+
+    /** Finds the activity below the given activity. */
+    @Nullable
+    private Activity findActivityBelow(@NonNull Activity activity) {
         Activity activityBelow = null;
-        if (currentContainer != null) {
-            final List<Activity> containerActivities = currentContainer.collectActivities();
-            final int index = containerActivities.indexOf(launchedActivity);
+        final TaskFragmentContainer container = getContainerWithActivity(activity);
+        if (container != null) {
+            final List<Activity> containerActivities = container.collectActivities();
+            final int index = containerActivities.indexOf(activity);
             if (index > 0) {
                 activityBelow = containerActivities.get(index - 1);
             }
         }
         if (activityBelow == null) {
-            IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow(
-                    launchedActivity.getActivityToken());
+            final IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow(
+                    activity.getActivityToken());
             if (belowToken != null) {
-                activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken);
+                activityBelow = getActivity(belowToken);
             }
         }
-        if (activityBelow == null) {
-            return;
-        }
+        return activityBelow;
+    }
 
-        // Check if the split is already set.
-        final TaskFragmentContainer activityBelowContainer = getContainerWithActivity(
-                activityBelow.getActivityToken());
-        if (currentContainer != null && activityBelowContainer != null) {
-            final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer,
-                    activityBelowContainer);
-            if (existingSplit != null) {
-                // There is already an active split with the activity below.
-                return;
+    /**
+     * Checks if there is a rule to split the two activities. If there is one, puts them into split
+     * and returns {@code true}. Otherwise, returns {@code false}.
+     */
+    private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity) {
+        final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity);
+        if (splitRule == null) {
+            return false;
+        }
+        final TaskFragmentContainer primaryContainer = getContainerWithActivity(
+                primaryActivity);
+        final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer);
+        if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer()
+                && canReuseContainer(splitRule, splitContainer.getSplitRule())) {
+            // Can launch in the existing secondary container if the rules share the same
+            // presentation.
+            final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
+            if (secondaryContainer == getContainerWithActivity(secondaryActivity)) {
+                // The activity is already in the target TaskFragment.
+                return true;
             }
+            secondaryContainer.addPendingAppearedActivity(secondaryActivity);
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            wct.reparentActivityToTaskFragment(
+                    secondaryContainer.getTaskFragmentToken(),
+                    secondaryActivity.getActivityToken());
+            mPresenter.applyTransaction(wct);
+            return true;
         }
-
-        final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity,
-                splitRules);
-        if (splitPairRule == null) {
-            return;
-        }
-
-        mPresenter.createNewSplitContainer(activityBelow, launchedActivity,
-                splitPairRule);
+        // Create new split pair.
+        mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule);
+        return true;
     }
 
     private void onActivityConfigurationChanged(@NonNull Activity activity) {
@@ -409,8 +594,7 @@
             // We don't embed activity when it is in PIP.
             return;
         }
-        final TaskFragmentContainer currentContainer = getContainerWithActivity(
-                activity.getActivityToken());
+        final TaskFragmentContainer currentContainer = getContainerWithActivity(activity);
 
         if (currentContainer != null) {
             // Changes to activities in controllers are handled in
@@ -443,14 +627,149 @@
     }
 
     /**
+     * When we are trying to handle a new activity Intent, returns the {@link TaskFragmentContainer}
+     * that we should reparent the new activity to if there is any embedding rule matched.
+     *
+     * @param wct               {@link WindowContainerTransaction} including all the window change
+     *                          requests. The caller is responsible to call
+     *                          {@link android.window.TaskFragmentOrganizer#applyTransaction}.
+     * @param taskId            The Task to start the activity in.
+     * @param intent            The {@link Intent} for starting the new launched activity.
+     * @param launchingActivity The {@link Activity} that starts the new activity. We will
+     *                          prioritize to split the new activity with it if it is not
+     *                          {@code null}.
+     * @return the {@link TaskFragmentContainer} to start the new activity in. {@code null} if there
+     *         is no embedding rule matched.
+     */
+    @VisibleForTesting
+    @Nullable
+    TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct,
+            int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) {
+        /*
+         * We will check the following to see if there is any embedding rule matched:
+         * 1. Whether the new activity intent should always expand.
+         * 2. Whether the launching activity (if set) should be split with the new activity intent.
+         * 3. Whether the top activity (if any) should be split with the new activity intent.
+         * 4. Whether the top activity (if any) in other split should be split with the new
+         *    activity intent.
+         */
+
+        // 1. Whether the new activity intent should always expand.
+        if (shouldExpand(null /* activity */, intent)) {
+            return createEmptyExpandedContainer(wct, taskId, launchingActivity);
+        }
+
+        // 2. Whether the launching activity (if set) should be split with the new activity intent.
+        if (launchingActivity != null) {
+            final TaskFragmentContainer container = getSecondaryContainerForSplitIfAny(wct,
+                    launchingActivity, intent, true /* respectClearTop */);
+            if (container != null) {
+                return container;
+            }
+        }
+
+        // 3. Whether the top activity (if any) should be split with the new activity intent.
+        final TaskContainer taskContainer = getTaskContainer(taskId);
+        if (taskContainer == null || taskContainer.getTopTaskFragmentContainer() == null) {
+            // There is no other activity in the Task to check split with.
+            return null;
+        }
+        final TaskFragmentContainer topContainer = taskContainer.getTopTaskFragmentContainer();
+        final Activity topActivity = topContainer.getTopNonFinishingActivity();
+        if (topActivity != null && topActivity != launchingActivity) {
+            final TaskFragmentContainer container = getSecondaryContainerForSplitIfAny(wct,
+                    topActivity, intent, false /* respectClearTop */);
+            if (container != null) {
+                return container;
+            }
+        }
+
+        // 4. Whether the top activity (if any) in other split should be split with the new
+        //    activity intent.
+        final SplitContainer topSplit = getActiveSplitForContainer(topContainer);
+        if (topSplit == null) {
+            return null;
+        }
+        final TaskFragmentContainer otherTopContainer =
+                topSplit.getPrimaryContainer() == topContainer
+                        ? topSplit.getSecondaryContainer()
+                        : topSplit.getPrimaryContainer();
+        final Activity otherTopActivity = otherTopContainer.getTopNonFinishingActivity();
+        if (otherTopActivity != null && otherTopActivity != launchingActivity) {
+            return getSecondaryContainerForSplitIfAny(wct, otherTopActivity, intent,
+                    false /* respectClearTop */);
+        }
+        return null;
+    }
+
+    /**
+     * Returns an empty expanded {@link TaskFragmentContainer} that we can launch an activity into.
+     */
+    @Nullable
+    private TaskFragmentContainer createEmptyExpandedContainer(
+            @NonNull WindowContainerTransaction wct, int taskId,
+            @Nullable Activity launchingActivity) {
+        // We need an activity in the organizer process in the same Task to use as the owner
+        // activity, as well as to get the Task window info.
+        final Activity activityInTask;
+        if (launchingActivity != null) {
+            activityInTask = launchingActivity;
+        } else {
+            final TaskContainer taskContainer = getTaskContainer(taskId);
+            activityInTask = taskContainer != null
+                    ? taskContainer.getTopNonFinishingActivity()
+                    : null;
+        }
+        if (activityInTask == null) {
+            // Can't find any activity in the Task that we can use as the owner activity.
+            return null;
+        }
+        final TaskFragmentContainer expandedContainer = newContainer(null /* activity */,
+                activityInTask, taskId);
+        mPresenter.createTaskFragment(wct, expandedContainer.getTaskFragmentToken(),
+                activityInTask.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED);
+        return expandedContainer;
+    }
+
+    /**
+     * Returns a container for the new activity intent to launch into as splitting with the primary
+     * activity.
+     */
+    @Nullable
+    private TaskFragmentContainer getSecondaryContainerForSplitIfAny(
+            @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity,
+            @NonNull Intent intent, boolean respectClearTop) {
+        final SplitPairRule splitRule = getSplitRule(primaryActivity, intent);
+        if (splitRule == null) {
+            return null;
+        }
+        final TaskFragmentContainer existingContainer = getContainerWithActivity(primaryActivity);
+        final SplitContainer splitContainer = getActiveSplitForContainer(existingContainer);
+        if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer()
+                && (canReuseContainer(splitRule, splitContainer.getSplitRule())
+                // TODO(b/231845476) we should always respect clearTop.
+                || !respectClearTop)) {
+            // Can launch in the existing secondary container if the rules share the same
+            // presentation.
+            return splitContainer.getSecondaryContainer();
+        }
+        // Create a new TaskFragment to split with the primary activity for the new activity.
+        return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, splitRule);
+    }
+
+    /**
      * Returns a container that this activity is registered with. An activity can only belong to one
      * container, or no container at all.
      */
     @Nullable
-    TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) {
+    TaskFragmentContainer getContainerWithActivity(@NonNull Activity activity) {
+        final IBinder activityToken = activity.getActivityToken();
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
             final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
-            for (TaskFragmentContainer container : containers) {
+            // Traverse from top to bottom in case an activity is added to top pending, and hasn't
+            // received update from server yet.
+            for (int j = containers.size() - 1; j >= 0; j--) {
+                final TaskFragmentContainer container = containers.get(j);
                 if (container.hasActivity(activityToken)) {
                     return container;
                 }
@@ -647,8 +966,7 @@
         if (splitContainer == null) {
             return;
         }
-        final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers;
-        if (splitContainer != splitContainers.get(splitContainers.size() - 1)) {
+        if (!isTopMostSplit(splitContainer)) {
             // Skip position update - it isn't the topmost split.
             return;
         }
@@ -664,11 +982,21 @@
         mPresenter.updateSplitContainer(splitContainer, container, wct);
     }
 
+    /** Whether the given split is the topmost split in the Task. */
+    private boolean isTopMostSplit(@NonNull SplitContainer splitContainer) {
+        final List<SplitContainer> splitContainers = splitContainer.getPrimaryContainer()
+                .getTaskContainer().mSplitContainers;
+        return splitContainer == splitContainers.get(splitContainers.size() - 1);
+    }
+
     /**
      * Returns the top active split container that has the provided container, if available.
      */
     @Nullable
-    private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) {
+    private SplitContainer getActiveSplitForContainer(@Nullable TaskFragmentContainer container) {
+        if (container == null) {
+            return null;
+        }
         final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers;
         if (splitContainers.isEmpty()) {
             return null;
@@ -687,8 +1015,9 @@
      * Returns the active split that has the provided containers as primary and secondary or as
      * secondary and primary, if available.
      */
+    @VisibleForTesting
     @Nullable
-    private SplitContainer getActiveSplitForContainers(
+    SplitContainer getActiveSplitForContainers(
             @NonNull TaskFragmentContainer firstContainer,
             @NonNull TaskFragmentContainer secondContainer) {
         final List<SplitContainer> splitContainers = firstContainer.getTaskContainer()
@@ -718,15 +1047,13 @@
     }
 
     boolean launchPlaceholderIfNecessary(@NonNull Activity activity) {
-        final TaskFragmentContainer container = getContainerWithActivity(
-                activity.getActivityToken());
+        final TaskFragmentContainer container = getContainerWithActivity(activity);
         // Don't launch placeholder if the container is occluded.
         if (container != null && container != getTopActiveContainer(container.getTaskId())) {
             return false;
         }
 
-        SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container)
-                : null;
+        final SplitContainer splitContainer = getActiveSplitForContainer(container);
         if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) {
             // Don't launch placeholder in primary split container
             return false;
@@ -861,14 +1188,7 @@
         if (container == null) {
             return false;
         }
-        final List<SplitContainer> splitContainers = container.getTaskContainer().mSplitContainers;
-        for (SplitContainer splitContainer : splitContainers) {
-            if (container.equals(splitContainer.getPrimaryContainer())
-                    || container.equals(splitContainer.getSecondaryContainer())) {
-                return false;
-            }
-        }
-        return true;
+        return getActiveSplitForContainer(container) == null;
     }
 
     /**
@@ -876,9 +1196,9 @@
      * if available.
      */
     @Nullable
-    private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
-            @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules) {
-        for (EmbeddingRule rule : splitRules) {
+    private SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Intent secondaryActivityIntent) {
+        for (EmbeddingRule rule : mSplitRules) {
             if (!(rule instanceof SplitPairRule)) {
                 continue;
             }
@@ -894,9 +1214,9 @@
      * Returns a split rule for the provided pair of primary and secondary activities if available.
      */
     @Nullable
-    private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
-            @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules) {
-        for (EmbeddingRule rule : splitRules) {
+    private SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity) {
+        for (EmbeddingRule rule : mSplitRules) {
             if (!(rule instanceof SplitPairRule)) {
                 continue;
             }
@@ -933,16 +1253,24 @@
         return mHandler;
     }
 
+    int getTaskId(@NonNull Activity activity) {
+        // Prefer to get the taskId from TaskFragmentContainer because Activity.getTaskId() is an
+        // IPC call.
+        final TaskFragmentContainer container = getContainerWithActivity(activity);
+        return container != null ? container.getTaskId() : activity.getTaskId();
+    }
+
+    @Nullable
+    Activity getActivity(@NonNull IBinder activityToken) {
+        return ActivityThread.currentActivityThread().getActivity(activityToken);
+    }
+
     /**
      * Returns {@code true} if an Activity with the provided component name should always be
      * expanded to occupy full task bounds. Such activity must not be put in a split.
      */
-    private static boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent,
-            List<EmbeddingRule> splitRules) {
-        if (splitRules == null) {
-            return false;
-        }
-        for (EmbeddingRule rule : splitRules) {
+    private boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent) {
+        for (EmbeddingRule rule : mSplitRules) {
             if (!(rule instanceof ActivityRule)) {
                 continue;
             }
@@ -996,8 +1324,8 @@
      */
     boolean shouldRetainAssociatedActivity(@NonNull TaskFragmentContainer finishingContainer,
             @NonNull Activity associatedActivity) {
-        TaskFragmentContainer associatedContainer = getContainerWithActivity(
-                associatedActivity.getActivityToken());
+        final TaskFragmentContainer associatedContainer = getContainerWithActivity(
+                associatedActivity);
         if (associatedContainer == null) {
             return false;
         }
@@ -1085,130 +1413,20 @@
                 return super.onStartActivity(who, intent, options);
             }
 
-            if (shouldExpand(null, intent, getSplitRules())) {
-                setLaunchingInExpandedContainer(launchingActivity, options);
-            } else if (!splitWithLaunchingActivity(launchingActivity, intent, options)) {
-                setLaunchingInSameSideContainer(launchingActivity, intent, options);
+            final int taskId = getTaskId(launchingActivity);
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            final TaskFragmentContainer launchedInTaskFragment = resolveStartActivityIntent(wct,
+                    taskId, intent, launchingActivity);
+            if (launchedInTaskFragment != null) {
+                mPresenter.applyTransaction(wct);
+                // Amend the request to let the WM know that the activity should be placed in the
+                // dedicated container.
+                options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
+                        launchedInTaskFragment.getTaskFragmentToken());
             }
 
             return super.onStartActivity(who, intent, options);
         }
-
-        private void setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options) {
-            TaskFragmentContainer newContainer = mPresenter.createNewExpandedContainer(
-                    launchingActivity);
-
-            // Amend the request to let the WM know that the activity should be placed in the
-            // dedicated container.
-            options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
-                    newContainer.getTaskFragmentToken());
-        }
-
-        /**
-         * Returns {@code true} if the activity that is going to be started via the
-         * {@code intent} should be paired with the {@code launchingActivity} and is set to be
-         * launched in the side container.
-         */
-        private boolean splitWithLaunchingActivity(Activity launchingActivity, Intent intent,
-                Bundle options) {
-            final SplitPairRule splitPairRule = getSplitRule(launchingActivity, intent,
-                    getSplitRules());
-            if (splitPairRule == null) {
-                return false;
-            }
-
-            // Check if there is any existing side container to launch into.
-            TaskFragmentContainer secondaryContainer = findSideContainerForNewLaunch(
-                    launchingActivity, splitPairRule);
-            if (secondaryContainer == null) {
-                // Create a new split with an empty side container.
-                secondaryContainer = mPresenter
-                        .createNewSplitWithEmptySideContainer(launchingActivity, splitPairRule);
-            }
-
-            // Amend the request to let the WM know that the activity should be placed in the
-            // dedicated container.
-            options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
-                    secondaryContainer.getTaskFragmentToken());
-            return true;
-        }
-
-        /**
-         * Finds if there is an existing split side {@link TaskFragmentContainer} that can be used
-         * for the new rule.
-         */
-        @Nullable
-        private TaskFragmentContainer findSideContainerForNewLaunch(Activity launchingActivity,
-                SplitPairRule splitPairRule) {
-            final TaskFragmentContainer launchingContainer = getContainerWithActivity(
-                    launchingActivity.getActivityToken());
-            if (launchingContainer == null) {
-                return null;
-            }
-
-            // We only check if the launching activity is the primary of the split. We will check
-            // if the launching activity is the secondary in #setLaunchingInSameSideContainer.
-            final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer);
-            if (splitContainer == null
-                    || splitContainer.getPrimaryContainer() != launchingContainer) {
-                return null;
-            }
-
-            if (canReuseContainer(splitPairRule, splitContainer.getSplitRule())) {
-                return splitContainer.getSecondaryContainer();
-            }
-            return null;
-        }
-
-        /**
-         * Checks if the activity that is going to be started via the {@code intent} should be
-         * paired with the existing top activity which is currently paired with the
-         * {@code launchingActivity}. If so, set the activity to be launched in the same side
-         * container of the {@code launchingActivity}.
-         */
-        private void setLaunchingInSameSideContainer(Activity launchingActivity, Intent intent,
-                Bundle options) {
-            final TaskFragmentContainer launchingContainer = getContainerWithActivity(
-                    launchingActivity.getActivityToken());
-            if (launchingContainer == null) {
-                return;
-            }
-
-            final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer);
-            if (splitContainer == null) {
-                return;
-            }
-
-            if (splitContainer.getSecondaryContainer() != launchingContainer) {
-                return;
-            }
-
-            // The launching activity is on the secondary container. Retrieve the primary
-            // activity from the other container.
-            Activity primaryActivity =
-                    splitContainer.getPrimaryContainer().getTopNonFinishingActivity();
-            if (primaryActivity == null) {
-                return;
-            }
-
-            final SplitPairRule splitPairRule = getSplitRule(primaryActivity, intent,
-                    getSplitRules());
-            if (splitPairRule == null) {
-                return;
-            }
-
-            // Can only launch in the same container if the rules share the same presentation.
-            if (!canReuseContainer(splitPairRule, splitContainer.getSplitRule())) {
-                return;
-            }
-
-            // Amend the request to let the WM know that the activity should be placed in the
-            // dedicated container. This is necessary for the case that the activity is started
-            // into a new Task, or new Task will be escaped from the current host Task and be
-            // displayed in fullscreen.
-            options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
-                    launchingContainer.getTaskFragmentToken());
-        }
     }
 
     /**
@@ -1228,8 +1446,18 @@
         if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) {
             return false;
         }
+        return haveSamePresentation((SplitPairRule) rule1, (SplitPairRule) rule2);
+    }
+
+    /** Whether the two rules have the same presentation. */
+    private static boolean haveSamePresentation(SplitPairRule rule1, SplitPairRule rule2) {
+        // TODO(b/231655482): add util method to do the comparison in SplitPairRule.
         return rule1.getSplitRatio() == rule2.getSplitRatio()
-                && rule1.getLayoutDirection() == rule2.getLayoutDirection();
+                && rule1.getLayoutDirection() == rule2.getLayoutDirection()
+                && rule1.getFinishPrimaryWithSecondary()
+                == rule2.getFinishPrimaryWithSecondary()
+                && rule1.getFinishSecondaryWithPrimary()
+                == rule2.getFinishSecondaryWithPrimary();
     }
 
     /**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index b32f4fa..43d0402 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -16,8 +16,6 @@
 
 package androidx.window.extensions.embedding;
 
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-
 import android.app.Activity;
 import android.app.WindowConfiguration;
 import android.app.WindowConfiguration.WindowingMode;
@@ -100,10 +98,10 @@
      * Creates a new split with the primary activity and an empty secondary container.
      * @return The newly created secondary container.
      */
-    TaskFragmentContainer createNewSplitWithEmptySideContainer(@NonNull Activity primaryActivity,
+    @NonNull
+    TaskFragmentContainer createNewSplitWithEmptySideContainer(
+            @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity,
             @NonNull SplitPairRule rule) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
-
         final Rect parentBounds = getParentContainerBounds(primaryActivity);
         final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule,
                 isLtr(primaryActivity, rule));
@@ -127,8 +125,6 @@
 
         mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
 
-        applyTransaction(wct);
-
         return secondaryContainer;
     }
 
@@ -155,8 +151,15 @@
 
         final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule,
                 isLtr(primaryActivity, rule));
+        final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity(
+                secondaryActivity);
+        TaskFragmentContainer containerToAvoid = primaryContainer;
+        if (rule.shouldClearTop() && curSecondaryContainer != null) {
+            // Do not reuse the current TaskFragment if the rule is to clear top.
+            containerToAvoid = curSecondaryContainer;
+        }
         final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
-                secondaryActivity, secondaryRectBounds, primaryContainer);
+                secondaryActivity, secondaryRectBounds, containerToAvoid);
 
         // Set adjacent to each other so that the containers below will be invisible.
         setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule);
@@ -167,21 +170,6 @@
     }
 
     /**
-     * Creates a new expanded container.
-     */
-    TaskFragmentContainer createNewExpandedContainer(@NonNull Activity launchingActivity) {
-        final TaskFragmentContainer newContainer = mController.newContainer(null /* activity */,
-                launchingActivity, launchingActivity.getTaskId());
-
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
-        createTaskFragment(wct, newContainer.getTaskFragmentToken(),
-                launchingActivity.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED);
-
-        applyTransaction(wct);
-        return newContainer;
-    }
-
-    /**
      * Creates a new container or resizes an existing container for activity to the provided bounds.
      * @param activity The activity to be re-parented to the container if necessary.
      * @param containerToAvoid Re-parent from this container if an activity is already in it.
@@ -189,8 +177,7 @@
     private TaskFragmentContainer prepareContainerForActivity(
             @NonNull WindowContainerTransaction wct, @NonNull Activity activity,
             @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) {
-        TaskFragmentContainer container = mController.getContainerWithActivity(
-                activity.getActivityToken());
+        TaskFragmentContainer container = mController.getContainerWithActivity(activity);
         final int taskId = container != null ? container.getTaskId() : activity.getTaskId();
         if (container == null || container == containerToAvoid) {
             container = mController.newContainer(activity, taskId);
@@ -230,7 +217,7 @@
                 isLtr(launchingActivity, rule));
 
         TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
-                launchingActivity.getActivityToken());
+                launchingActivity);
         if (primaryContainer == null) {
             primaryContainer = mController.newContainer(launchingActivity,
                     launchingActivity.getTaskId());
@@ -460,8 +447,7 @@
 
     @NonNull
     Rect getParentContainerBounds(@NonNull Activity activity) {
-        final TaskFragmentContainer container = mController.getContainerWithActivity(
-                activity.getActivityToken());
+        final TaskFragmentContainer container = mController.getContainerWithActivity(activity);
         if (container != null) {
             return getParentContainerBounds(container);
         }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index dba71ef..0ea5603 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -142,4 +142,23 @@
             container.removePendingAppearedActivity(pendingAppearedActivity);
         }
     }
+
+    @Nullable
+    TaskFragmentContainer getTopTaskFragmentContainer() {
+        if (mContainers.isEmpty()) {
+            return null;
+        }
+        return mContainers.get(mContainers.size() - 1);
+    }
+
+    @Nullable
+    Activity getTopNonFinishingActivity() {
+        for (int i = mContainers.size() - 1; i >= 0; i--) {
+            final Activity activity = mContainers.get(i).getTopNonFinishingActivity();
+            if (activity != null) {
+                return activity;
+            }
+        }
+        return null;
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
index b3becad..cdee9e3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
@@ -17,6 +17,8 @@
 package androidx.window.extensions.embedding;
 
 import static android.graphics.Matrix.MSCALE_X;
+import static android.graphics.Matrix.MTRANS_X;
+import static android.graphics.Matrix.MTRANS_Y;
 
 import android.graphics.Rect;
 import android.view.Choreographer;
@@ -96,22 +98,20 @@
                 mTarget.localBounds.left, mTarget.localBounds.top);
         t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
         t.setAlpha(mLeash, mTransformation.getAlpha());
-
-        // Open/close animation may scale up the surface. Apply an inverse scale to the window crop
-        // so that it will not be covering other windows.
-        mVecs[1] = mVecs[2] = 0;
-        mVecs[0] = mVecs[3] = 1;
-        mTransformation.getMatrix().mapVectors(mVecs);
-        mVecs[0] = 1.f / mVecs[0];
-        mVecs[3] = 1.f / mVecs[3];
-        final Rect clipRect = mTarget.localBounds;
-        mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
-        mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
-        mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
-        mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
-        mRect.offsetTo(Math.round(mTarget.localBounds.width() * (1 - mVecs[0]) / 2.f),
-                Math.round(mTarget.localBounds.height() * (1 - mVecs[3]) / 2.f));
-        t.setWindowCrop(mLeash, mRect);
+        // Get current animation position.
+        final int positionX = Math.round(mMatrix[MTRANS_X]);
+        final int positionY = Math.round(mMatrix[MTRANS_Y]);
+        // The exiting surface starts at position: mTarget.localBounds and moves with
+        // positionX varying. Offset our crop region by the amount we have slided so crop
+        // regions stays exactly on the original container in split.
+        final int cropOffsetX = mTarget.localBounds.left - positionX;
+        final int cropOffsetY = mTarget.localBounds.top - positionY;
+        final Rect cropRect = new Rect();
+        cropRect.set(mTarget.localBounds);
+        // Because window crop uses absolute position.
+        cropRect.offsetTo(0, 0);
+        cropRect.offset(cropOffsetX, cropOffsetY);
+        t.setCrop(mLeash, cropRect);
     }
 
     /** Called after animation finished. */
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 6693755..26ddae4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -21,7 +21,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Activity;
-import android.app.ActivityThread;
 import android.app.WindowConfiguration.WindowingMode;
 import android.graphics.Rect;
 import android.os.Binder;
@@ -122,21 +121,24 @@
     /** List of activities that belong to this container and live in this process. */
     @NonNull
     List<Activity> collectActivities() {
+        final List<Activity> allActivities = new ArrayList<>();
+        if (mInfo != null) {
+            // Add activities reported from the server.
+            for (IBinder token : mInfo.getActivities()) {
+                final Activity activity = mController.getActivity(token);
+                if (activity != null && !activity.isFinishing()) {
+                    allActivities.add(activity);
+                }
+            }
+        }
+
         // Add the re-parenting activity, in case the server has not yet reported the task
         // fragment info update with it placed in this container. We still want to apply rules
         // in this intermediate state.
-        List<Activity> allActivities = new ArrayList<>();
-        if (!mPendingAppearedActivities.isEmpty()) {
-            allActivities.addAll(mPendingAppearedActivities);
-        }
-        // Add activities reported from the server.
-        if (mInfo == null) {
-            return allActivities;
-        }
-        ActivityThread activityThread = ActivityThread.currentActivityThread();
-        for (IBinder token : mInfo.getActivities()) {
-            Activity activity = activityThread.getActivity(token);
-            if (activity != null && !activity.isFinishing() && !allActivities.contains(activity)) {
+        // Place those on top of the list since they will be on the top after reported from the
+        // server.
+        for (Activity activity : mPendingAppearedActivities) {
+            if (!activity.isFinishing()) {
                 allActivities.add(activity);
             }
         }
@@ -243,6 +245,12 @@
         return i >= 0 ? activities.get(i) : null;
     }
 
+    @Nullable
+    Activity getBottomMostActivity() {
+        final List<Activity> activities = collectActivities();
+        return activities.isEmpty() ? null : activities.get(0);
+    }
+
     boolean isEmpty() {
         return mPendingAppearedActivities.isEmpty() && (mInfo == null || mInfo.isEmpty());
     }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 34cde9b..353c7df 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -16,6 +16,9 @@
 
 package androidx.window.extensions.embedding;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
@@ -24,23 +27,35 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 
+import android.annotation.NonNull;
 import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Binder;
 import android.os.Handler;
+import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
+import android.util.Pair;
 import android.window.TaskFragmentInfo;
+import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -53,6 +68,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -67,8 +83,10 @@
 public class SplitControllerTest {
     private static final int TASK_ID = 10;
     private static final Rect TASK_BOUNDS = new Rect(0, 0, 600, 1200);
+    private static final float SPLIT_RATIO = 0.5f;
+    private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent(
+            new ComponentName("test", "placeholder"));
 
-    @Mock
     private Activity mActivity;
     @Mock
     private Resources mActivityResources;
@@ -89,12 +107,13 @@
         mSplitPresenter = mSplitController.mPresenter;
         spyOn(mSplitController);
         spyOn(mSplitPresenter);
+        doNothing().when(mSplitPresenter).applyTransaction(any());
         final Configuration activityConfig = new Configuration();
         activityConfig.windowConfiguration.setBounds(TASK_BOUNDS);
         activityConfig.windowConfiguration.setMaxBounds(TASK_BOUNDS);
-        doReturn(mActivityResources).when(mActivity).getResources();
         doReturn(activityConfig).when(mActivityResources).getConfiguration();
         doReturn(mHandler).when(mSplitController).getHandler();
+        mActivity = createMockActivity();
     }
 
     @Test
@@ -260,6 +279,622 @@
 
         mSplitController.updateContainer(mTransaction, tf);
 
-        verify(mSplitPresenter).updateSplitContainer(eq(splitContainer), eq(tf), eq(mTransaction));
+        verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction);
+    }
+
+    @Test
+    public void testOnActivityCreated() {
+        mSplitController.onActivityCreated(mActivity);
+
+        // Disallow to split as primary because we want the new launch to be always on top.
+        verify(mSplitController).resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+    }
+
+    @Test
+    public void testOnActivityReparentToTask_sameProcess() {
+        mSplitController.onActivityReparentToTask(TASK_ID, new Intent(),
+                mActivity.getActivityToken());
+
+        // Treated as on activity created, but allow to split as primary.
+        verify(mSplitController).resolveActivityToContainer(mActivity,
+                true /* canSplitAsPrimary */);
+        // Try to place the activity to the top TaskFragment when there is no matched rule.
+        verify(mSplitController).placeActivityInTopContainer(mActivity);
+    }
+
+    @Test
+    public void testOnActivityReparentToTask_diffProcess() {
+        // Create an empty TaskFragment to initialize for the Task.
+        mSplitController.newContainer(null, mActivity, TASK_ID);
+        final IBinder activityToken = new Binder();
+        final Intent intent = new Intent();
+
+        mSplitController.onActivityReparentToTask(TASK_ID, intent, activityToken);
+
+        // Treated as starting new intent
+        verify(mSplitController, never()).resolveActivityToContainer(any(), anyBoolean());
+        verify(mSplitController).resolveStartActivityIntent(any(), eq(TASK_ID), eq(intent),
+                isNull());
+    }
+
+    @Test
+    public void testResolveStartActivityIntent_withoutLaunchingActivity() {
+        final Intent intent = new Intent();
+        final ActivityRule expandRule = new ActivityRule.Builder(r -> false, i -> i == intent)
+                .setShouldAlwaysExpand(true)
+                .build();
+        mSplitController.setEmbeddingRules(Collections.singleton(expandRule));
+
+        // No other activity available in the Task.
+        TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(mTransaction,
+                TASK_ID, intent, null /* launchingActivity */);
+        assertNull(container);
+
+        // Task contains another activity that can be used as owner activity.
+        createMockTaskFragmentContainer(mActivity);
+        container = mSplitController.resolveStartActivityIntent(mTransaction,
+                TASK_ID, intent, null /* launchingActivity */);
+        assertNotNull(container);
+    }
+
+    @Test
+    public void testResolveStartActivityIntent_shouldExpand() {
+        final Intent intent = new Intent();
+        setupExpandRule(intent);
+        final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
+                mTransaction, TASK_ID, intent, mActivity);
+
+        assertNotNull(container);
+        assertTrue(container.areLastRequestedBoundsEqual(null));
+        assertTrue(container.isLastRequestedWindowingModeEqual(WINDOWING_MODE_UNDEFINED));
+        assertFalse(container.hasActivity(mActivity.getActivityToken()));
+        verify(mSplitPresenter).createTaskFragment(mTransaction, container.getTaskFragmentToken(),
+                mActivity.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED);
+    }
+
+    @Test
+    public void testResolveStartActivityIntent_shouldSplitWithLaunchingActivity() {
+        final Intent intent = new Intent();
+        setupSplitRule(mActivity, intent);
+
+        final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
+                mTransaction, TASK_ID, intent, mActivity);
+        final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
+                mActivity);
+
+        assertSplitPair(primaryContainer, container);
+    }
+
+    @Test
+    public void testResolveStartActivityIntent_shouldSplitWithTopExpandActivity() {
+        final Intent intent = new Intent();
+        setupSplitRule(mActivity, intent);
+        createMockTaskFragmentContainer(mActivity);
+
+        final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
+                mTransaction, TASK_ID, intent, null /* launchingActivity */);
+        final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
+                mActivity);
+
+        assertSplitPair(primaryContainer, container);
+    }
+
+    @Test
+    public void testResolveStartActivityIntent_shouldSplitWithTopSecondaryActivity() {
+        final Intent intent = new Intent();
+        setupSplitRule(mActivity, intent);
+        final Activity primaryActivity = createMockActivity();
+        addSplitTaskFragments(primaryActivity, mActivity);
+
+        final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
+                mTransaction, TASK_ID, intent, null /* launchingActivity */);
+        final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
+                mActivity);
+
+        assertSplitPair(primaryContainer, container);
+    }
+
+    @Test
+    public void testResolveStartActivityIntent_shouldSplitWithTopPrimaryActivity() {
+        final Intent intent = new Intent();
+        setupSplitRule(mActivity, intent);
+        final Activity secondaryActivity = createMockActivity();
+        addSplitTaskFragments(mActivity, secondaryActivity);
+
+        final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
+                mTransaction, TASK_ID, intent, null /* launchingActivity */);
+        final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
+                mActivity);
+
+        assertSplitPair(primaryContainer, container);
+    }
+
+    @Test
+    public void testPlaceActivityInTopContainer() {
+        mSplitController.placeActivityInTopContainer(mActivity);
+
+        verify(mSplitPresenter, never()).applyTransaction(any());
+
+        mSplitController.newContainer(null /* activity */, mActivity, TASK_ID);
+        mSplitController.placeActivityInTopContainer(mActivity);
+
+        verify(mSplitPresenter).applyTransaction(any());
+
+        // Not reparent if activity is in a TaskFragment.
+        clearInvocations(mSplitPresenter);
+        mSplitController.newContainer(mActivity, TASK_ID);
+        mSplitController.placeActivityInTopContainer(mActivity);
+
+        verify(mSplitPresenter, never()).applyTransaction(any());
+    }
+
+    @Test
+    public void testResolveActivityToContainer_noRuleMatched() {
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertFalse(result);
+        verify(mSplitController, never()).newContainer(any(), any(), anyInt());
+    }
+
+    @Test
+    public void testResolveActivityToContainer_expandRule_notInTaskFragment() {
+        setupExpandRule(mActivity);
+
+        // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it.
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+        final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
+                mActivity);
+
+        assertTrue(result);
+        assertNotNull(container);
+        verify(mSplitController).newContainer(mActivity, TASK_ID);
+        verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity);
+    }
+
+    @Test
+    public void testResolveActivityToContainer_expandRule_inSingleTaskFragment() {
+        setupExpandRule(mActivity);
+
+        // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it.
+        final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertTrue(result);
+        verify(mSplitPresenter).expandTaskFragment(container.getTaskFragmentToken());
+    }
+
+    @Test
+    public void testResolveActivityToContainer_expandRule_inSplitTaskFragment() {
+        setupExpandRule(mActivity);
+
+        // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it.
+        final Activity activity = createMockActivity();
+        addSplitTaskFragments(activity, mActivity);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+        final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
+                mActivity);
+
+        assertTrue(result);
+        assertNotNull(container);
+        verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity);
+    }
+
+    @Test
+    public void testResolveActivityToContainer_placeholderRule_notInTaskFragment() {
+        setupPlaceholderRule(mActivity);
+        final SplitPlaceholderRule placeholderRule =
+                (SplitPlaceholderRule) mSplitController.getSplitRules().get(0);
+
+        // Launch placeholder if the activity is not in any TaskFragment.
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertTrue(result);
+        verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+                null /* activityOptions */, placeholderRule, true /* isPlaceholder */);
+    }
+
+    @Test
+    public void testResolveActivityToContainer_placeholderRule_inOccludedTaskFragment() {
+        setupPlaceholderRule(mActivity);
+
+        // Don't launch placeholder if the activity is not in the topmost active TaskFragment.
+        final Activity activity = createMockActivity();
+        mSplitController.newContainer(mActivity, TASK_ID);
+        mSplitController.newContainer(activity, TASK_ID);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertFalse(result);
+        verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(),
+                anyBoolean());
+    }
+
+    @Test
+    public void testResolveActivityToContainer_placeholderRule_inTopMostTaskFragment() {
+        setupPlaceholderRule(mActivity);
+        final SplitPlaceholderRule placeholderRule =
+                (SplitPlaceholderRule) mSplitController.getSplitRules().get(0);
+
+        // Launch placeholder if the activity is in the topmost expanded TaskFragment.
+        mSplitController.newContainer(mActivity, TASK_ID);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertTrue(result);
+        verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+                null /* activityOptions */, placeholderRule, true /* isPlaceholder */);
+    }
+
+    @Test
+    public void testResolveActivityToContainer_placeholderRule_inPrimarySplit() {
+        setupPlaceholderRule(mActivity);
+
+        // Don't launch placeholder if the activity is in primary split.
+        final Activity secondaryActivity = createMockActivity();
+        addSplitTaskFragments(mActivity, secondaryActivity);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertFalse(result);
+        verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(),
+                anyBoolean());
+    }
+
+    @Test
+    public void testResolveActivityToContainer_placeholderRule_inSecondarySplit() {
+        setupPlaceholderRule(mActivity);
+        final SplitPlaceholderRule placeholderRule =
+                (SplitPlaceholderRule) mSplitController.getSplitRules().get(0);
+
+        // Launch placeholder if the activity is in secondary split.
+        final Activity primaryActivity = createMockActivity();
+        addSplitTaskFragments(primaryActivity, mActivity);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertTrue(result);
+        verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+                null /* activityOptions */, placeholderRule, true /* isPlaceholder */);
+    }
+
+    @Test
+    public void testResolveActivityToContainer_splitRule_inPrimarySplitWithRuleMatched() {
+        final Intent secondaryIntent = new Intent();
+        setupSplitRule(mActivity, secondaryIntent);
+        final SplitPairRule splitRule = (SplitPairRule) mSplitController.getSplitRules().get(0);
+
+        // Activity is already in primary split, no need to create new split.
+        final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity,
+                TASK_ID);
+        final TaskFragmentContainer secondaryContainer = mSplitController.newContainer(
+                null /* activity */, mActivity, TASK_ID);
+        mSplitController.registerSplit(
+                mTransaction,
+                primaryContainer,
+                mActivity,
+                secondaryContainer,
+                splitRule);
+        clearInvocations(mSplitController);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertTrue(result);
+        verify(mSplitController, never()).newContainer(any(), any(), anyInt());
+        verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any());
+    }
+
+    @Test
+    public void testResolveActivityToContainer_splitRule_inSecondarySplitWithRuleMatched() {
+        final Activity primaryActivity = createMockActivity();
+        setupSplitRule(primaryActivity, mActivity);
+
+        // Activity is already in secondary split, no need to create new split.
+        addSplitTaskFragments(primaryActivity, mActivity);
+        clearInvocations(mSplitController);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertTrue(result);
+        verify(mSplitController, never()).newContainer(any(), any(), anyInt());
+        verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any());
+    }
+
+    @Test
+    public void testResolveActivityToContainer_splitRule_inSecondarySplitWithNoRuleMatched() {
+        final Activity primaryActivity = createMockActivity();
+        final Activity secondaryActivity = createMockActivity();
+        setupSplitRule(primaryActivity, secondaryActivity);
+
+        // Activity is in secondary split, but there is no rule to split it with primary.
+        addSplitTaskFragments(primaryActivity, secondaryActivity);
+        mSplitController.getContainerWithActivity(secondaryActivity)
+                .addPendingAppearedActivity(mActivity);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertFalse(result);
+    }
+
+    @Test
+    public void testResolveActivityToContainer_placeholderRule_isPlaceholderWithRuleMatched() {
+        final Activity primaryActivity = createMockActivity();
+        setupPlaceholderRule(primaryActivity);
+        final SplitPlaceholderRule placeholderRule =
+                (SplitPlaceholderRule) mSplitController.getSplitRules().get(0);
+        doReturn(PLACEHOLDER_INTENT).when(mActivity).getIntent();
+
+        // Activity is a placeholder.
+        final TaskFragmentContainer primaryContainer = mSplitController.newContainer(
+                primaryActivity, TASK_ID);
+        final TaskFragmentContainer secondaryContainer = mSplitController.newContainer(mActivity,
+                TASK_ID);
+        mSplitController.registerSplit(
+                mTransaction,
+                primaryContainer,
+                mActivity,
+                secondaryContainer,
+                placeholderRule);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertTrue(result);
+    }
+
+    @Test
+    public void testResolveActivityToContainer_splitRule_splitWithActivityBelowAsSecondary() {
+        final Activity activityBelow = createMockActivity();
+        setupSplitRule(activityBelow, mActivity);
+
+        final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
+                TASK_ID);
+        container.addPendingAppearedActivity(mActivity);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertTrue(result);
+        assertSplitPair(activityBelow, mActivity);
+    }
+
+    @Test
+    public void testResolveActivityToContainer_splitRule_splitWithActivityBelowAsPrimary() {
+        final Activity activityBelow = createMockActivity();
+        setupSplitRule(mActivity, activityBelow);
+
+        // Disallow to split as primary.
+        final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
+                TASK_ID);
+        container.addPendingAppearedActivity(mActivity);
+        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertFalse(result);
+        assertEquals(container, mSplitController.getContainerWithActivity(mActivity));
+
+        // Allow to split as primary.
+        result = mSplitController.resolveActivityToContainer(mActivity,
+                true /* canSplitAsPrimary */);
+
+        assertTrue(result);
+        assertSplitPair(mActivity, activityBelow);
+    }
+
+    @Test
+    public void testResolveActivityToContainer_splitRule_splitWithCurrentPrimaryAsSecondary() {
+        final Activity primaryActivity = createMockActivity();
+        setupSplitRule(primaryActivity, mActivity);
+
+        final Activity activityBelow = createMockActivity();
+        addSplitTaskFragments(primaryActivity, activityBelow);
+        final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
+                primaryActivity);
+        final TaskFragmentContainer secondaryContainer = mSplitController.getContainerWithActivity(
+                activityBelow);
+        secondaryContainer.addPendingAppearedActivity(mActivity);
+        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+        final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
+                mActivity);
+
+        assertTrue(result);
+        // TODO(b/231845476) we should always respect clearTop.
+        // assertNotEquals(secondaryContainer, container);
+        assertSplitPair(primaryContainer, container);
+    }
+
+    @Test
+    public void testResolveActivityToContainer_splitRule_splitWithCurrentPrimaryAsPrimary() {
+        final Activity primaryActivity = createMockActivity();
+        setupSplitRule(mActivity, primaryActivity);
+
+        final Activity activityBelow = createMockActivity();
+        addSplitTaskFragments(primaryActivity, activityBelow);
+        final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
+                primaryActivity);
+        primaryContainer.addPendingAppearedActivity(mActivity);
+        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* canSplitAsPrimary */);
+
+        assertFalse(result);
+        assertEquals(primaryContainer, mSplitController.getContainerWithActivity(mActivity));
+
+
+        result = mSplitController.resolveActivityToContainer(mActivity,
+                true /* canSplitAsPrimary */);
+
+        assertTrue(result);
+        assertSplitPair(mActivity, primaryActivity);
+    }
+
+    /** Creates a mock activity in the organizer process. */
+    private Activity createMockActivity() {
+        final Activity activity = mock(Activity.class);
+        doReturn(mActivityResources).when(activity).getResources();
+        final IBinder activityToken = new Binder();
+        doReturn(activityToken).when(activity).getActivityToken();
+        doReturn(activity).when(mSplitController).getActivity(activityToken);
+        doReturn(TASK_ID).when(activity).getTaskId();
+        return activity;
+    }
+
+    /** Creates a mock TaskFragmentInfo for the given TaskFragment. */
+    private TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container,
+            @NonNull Activity activity) {
+        return new TaskFragmentInfo(container.getTaskFragmentToken(),
+                mock(WindowContainerToken.class),
+                new Configuration(),
+                1,
+                true /* isVisible */,
+                Collections.singletonList(activity.getActivityToken()),
+                new Point(),
+                false /* isTaskClearedForReuse */,
+                false /* isTaskFragmentClearedForPip */);
+    }
+
+    /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
+    private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) {
+        final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID);
+        setupTaskFragmentInfo(container, activity);
+        return container;
+    }
+
+    /** Setups the given TaskFragment as it has appeared in the server. */
+    private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container,
+            @NonNull Activity activity) {
+        final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity);
+        container.setInfo(info);
+        mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info);
+    }
+
+    /** Setups a rule to always expand the given intent. */
+    private void setupExpandRule(@NonNull Intent expandIntent) {
+        final ActivityRule expandRule = new ActivityRule.Builder(r -> false, expandIntent::equals)
+                .setShouldAlwaysExpand(true)
+                .build();
+        mSplitController.setEmbeddingRules(Collections.singleton(expandRule));
+    }
+
+    /** Setups a rule to always expand the given activity. */
+    private void setupExpandRule(@NonNull Activity expandActivity) {
+        final ActivityRule expandRule = new ActivityRule.Builder(expandActivity::equals, i -> false)
+                .setShouldAlwaysExpand(true)
+                .build();
+        mSplitController.setEmbeddingRules(Collections.singleton(expandRule));
+    }
+
+    /** Setups a rule to launch placeholder for the given activity. */
+    private void setupPlaceholderRule(@NonNull Activity primaryActivity) {
+        final SplitRule placeholderRule = new SplitPlaceholderRule.Builder(PLACEHOLDER_INTENT,
+                primaryActivity::equals, i -> false, w -> true)
+                .setSplitRatio(SPLIT_RATIO)
+                .build();
+        mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule));
+    }
+
+    /** Setups a rule to always split the given activities. */
+    private void setupSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Intent secondaryIntent) {
+        final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent);
+        mSplitController.setEmbeddingRules(Collections.singleton(splitRule));
+    }
+
+    /** Setups a rule to always split the given activities. */
+    private void setupSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity) {
+        final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity);
+        mSplitController.setEmbeddingRules(Collections.singleton(splitRule));
+    }
+
+    /** Creates a rule to always split the given activity and the given intent. */
+    private SplitRule createSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Intent secondaryIntent) {
+        final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent);
+        return new SplitPairRule.Builder(
+                activityPair -> false,
+                targetPair::equals,
+                w -> true)
+                .setSplitRatio(SPLIT_RATIO)
+                .setShouldClearTop(true)
+                .build();
+    }
+
+    /** Creates a rule to always split the given activities. */
+    private SplitRule createSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity) {
+        final Pair<Activity, Activity> targetPair = new Pair<>(primaryActivity, secondaryActivity);
+        return new SplitPairRule.Builder(
+                targetPair::equals,
+                activityIntentPair -> false,
+                w -> true)
+                .setSplitRatio(SPLIT_RATIO)
+                .setShouldClearTop(true)
+                .build();
+    }
+
+    /** Adds a pair of TaskFragments as split for the given activities. */
+    private void addSplitTaskFragments(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity) {
+        final TaskFragmentContainer primaryContainer = createMockTaskFragmentContainer(
+                primaryActivity);
+        final TaskFragmentContainer secondaryContainer = createMockTaskFragmentContainer(
+                secondaryActivity);
+        mSplitController.registerSplit(
+                mock(WindowContainerTransaction.class),
+                primaryContainer,
+                primaryActivity,
+                secondaryContainer,
+                createSplitRule(primaryActivity, secondaryActivity));
+
+        // We need to set those in case we are not respecting clear top.
+        // TODO(b/231845476) we should always respect clearTop.
+        final int windowingMode = mSplitController.getTaskContainer(TASK_ID)
+                .getWindowingModeForSplitTaskFragment(TASK_BOUNDS);
+        primaryContainer.setLastRequestedWindowingMode(windowingMode);
+        secondaryContainer.setLastRequestedWindowingMode(windowingMode);
+        primaryContainer.setLastRequestedBounds(getSplitBounds(true /* isPrimary */));
+        secondaryContainer.setLastRequestedBounds(getSplitBounds(false /* isPrimary */));
+    }
+
+    /** Gets the bounds of a TaskFragment that is in split. */
+    private Rect getSplitBounds(boolean isPrimary) {
+        final int width = (int) (TASK_BOUNDS.width() * SPLIT_RATIO);
+        return isPrimary
+                ? new Rect(TASK_BOUNDS.left, TASK_BOUNDS.top, TASK_BOUNDS.left + width,
+                        TASK_BOUNDS.bottom)
+                : new Rect(TASK_BOUNDS.left + width, TASK_BOUNDS.top, TASK_BOUNDS.right,
+                        TASK_BOUNDS.bottom);
+    }
+
+    /** Asserts that the two given activities are in split. */
+    private void assertSplitPair(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity) {
+        assertSplitPair(mSplitController.getContainerWithActivity(primaryActivity),
+                mSplitController.getContainerWithActivity(secondaryActivity));
+    }
+
+    /** Asserts that the two given TaskFragments are in split. */
+    private void assertSplitPair(@NonNull TaskFragmentContainer primaryContainer,
+            @NonNull TaskFragmentContainer secondaryContainer) {
+        assertNotNull(primaryContainer);
+        assertNotNull(secondaryContainer);
+        assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer,
+                secondaryContainer));
+        if (primaryContainer.mInfo != null) {
+            assertTrue(primaryContainer.areLastRequestedBoundsEqual(
+                    getSplitBounds(true /* isPrimary */)));
+            assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(
+                    WINDOWING_MODE_MULTI_WINDOW));
+        }
+        if (secondaryContainer.mInfo != null) {
+            assertTrue(secondaryContainer.areLastRequestedBoundsEqual(
+                    getSplitBounds(false /* isPrimary */)));
+            assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(
+                    WINDOWING_MODE_MULTI_WINDOW));
+        }
     }
 }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
index 0de94b0..f1042ab 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
@@ -24,8 +24,12 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 
+import android.app.Activity;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 
@@ -147,4 +151,38 @@
 
         assertFalse(taskContainer.isEmpty());
     }
+
+    @Test
+    public void testGetTopTaskFragmentContainer() {
+        final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+        assertNull(taskContainer.getTopTaskFragmentContainer());
+
+        final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */,
+                taskContainer, mController);
+        assertEquals(tf0, taskContainer.getTopTaskFragmentContainer());
+
+        final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */,
+                taskContainer, mController);
+        assertEquals(tf1, taskContainer.getTopTaskFragmentContainer());
+    }
+
+    @Test
+    public void testGetTopNonFinishingActivity() {
+        final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+        assertNull(taskContainer.getTopNonFinishingActivity());
+
+        final TaskFragmentContainer tf0 = mock(TaskFragmentContainer.class);
+        taskContainer.mContainers.add(tf0);
+        final Activity activity0 = mock(Activity.class);
+        doReturn(activity0).when(tf0).getTopNonFinishingActivity();
+        assertEquals(activity0, taskContainer.getTopNonFinishingActivity());
+
+        final TaskFragmentContainer tf1 = mock(TaskFragmentContainer.class);
+        taskContainer.mContainers.add(tf1);
+        assertEquals(activity0, taskContainer.getTopNonFinishingActivity());
+
+        final Activity activity1 = mock(Activity.class);
+        doReturn(activity1).when(tf1).getTopNonFinishingActivity();
+        assertEquals(activity1, taskContainer.getTopNonFinishingActivity());
+    }
 }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index ce80cbf3..587878f 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -18,6 +18,7 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -29,7 +30,9 @@
 import static org.mockito.Mockito.never;
 
 import android.app.Activity;
+import android.os.Binder;
 import android.os.Handler;
+import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
 import android.window.TaskFragmentInfo;
 import android.window.WindowContainerTransaction;
@@ -37,6 +40,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.google.android.collect.Lists;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -44,6 +49,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Test class for {@link TaskFragmentContainer}.
@@ -62,16 +68,16 @@
     @Mock
     private SplitController mController;
     @Mock
-    private Activity mActivity;
-    @Mock
     private TaskFragmentInfo mInfo;
     @Mock
     private Handler mHandler;
+    private Activity mActivity;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
         doReturn(mHandler).when(mController).getHandler();
+        mActivity = createMockActivity();
     }
 
     @Test
@@ -165,4 +171,72 @@
         assertNull(container.mAppearEmptyTimeout);
         verify(mController).onTaskFragmentAppearEmptyTimeout(container);
     }
+
+    @Test
+    public void testCollectActivities() {
+        final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+        final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+                taskContainer, mController);
+        List<Activity> activities = container.collectActivities();
+
+        assertTrue(activities.isEmpty());
+
+        container.addPendingAppearedActivity(mActivity);
+        activities = container.collectActivities();
+
+        assertEquals(1, activities.size());
+
+        final Activity activity0 = createMockActivity();
+        final Activity activity1 = createMockActivity();
+        final List<IBinder> runningActivities = Lists.newArrayList(activity0.getActivityToken(),
+                activity1.getActivityToken());
+        doReturn(runningActivities).when(mInfo).getActivities();
+        container.setInfo(mInfo);
+        activities = container.collectActivities();
+
+        assertEquals(3, activities.size());
+        assertEquals(activity0, activities.get(0));
+        assertEquals(activity1, activities.get(1));
+        assertEquals(mActivity, activities.get(2));
+    }
+
+    @Test
+    public void testAddPendingActivity() {
+        final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+        final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+                taskContainer, mController);
+        container.addPendingAppearedActivity(mActivity);
+
+        assertEquals(1, container.collectActivities().size());
+
+        container.addPendingAppearedActivity(mActivity);
+
+        assertEquals(1, container.collectActivities().size());
+    }
+
+    @Test
+    public void testGetBottomMostActivity() {
+        final TaskContainer taskContainer = new TaskContainer(TASK_ID);
+        final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+                taskContainer, mController);
+        container.addPendingAppearedActivity(mActivity);
+
+        assertEquals(mActivity, container.getBottomMostActivity());
+
+        final Activity activity = createMockActivity();
+        final List<IBinder> runningActivities = Lists.newArrayList(activity.getActivityToken());
+        doReturn(runningActivities).when(mInfo).getActivities();
+        container.setInfo(mInfo);
+
+        assertEquals(activity, container.getBottomMostActivity());
+    }
+
+    /** Creates a mock activity in the organizer process. */
+    private Activity createMockActivity() {
+        final Activity activity = mock(Activity.class);
+        final IBinder activityToken = new Binder();
+        doReturn(activityToken).when(activity).getActivityToken();
+        doReturn(activity).when(mController).getActivity(activityToken);
+        return activity;
+    }
 }
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 8ba41ab..f03b7f6 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -100,4 +100,7 @@
     <!-- The default gravity for the picture-in-picture window.
          Currently, this maps to Gravity.BOTTOM | Gravity.RIGHT -->
     <integer name="config_defaultPictureInPictureGravity">0x55</integer>
+
+    <!-- Whether to dim a split-screen task when the other is the IME target -->
+    <bool name="config_dimNonImeAttachedSide">true</bool>
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 8fa9f56..a16841c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -67,8 +67,12 @@
                     SETTING_VALUE_ON) != SETTING_VALUE_OFF;
     private static final int PROGRESS_THRESHOLD = SystemProperties
             .getInt(PREDICTIVE_BACK_PROGRESS_THRESHOLD_PROP, -1);
-
     private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
+    /**
+     * Max duration to wait for a transition to finish before accepting another gesture start
+     * request.
+     */
+    private static final long MAX_TRANSITION_DURATION = 2000;
 
     /**
      * Location of the initial touch event of the back gesture.
@@ -84,6 +88,8 @@
     /** True when a back gesture is ongoing */
     private boolean mBackGestureStarted = false;
 
+    /** Tracks if an uninterruptible transition is in progress */
+    private boolean mTransitionInProgress = false;
     /** @see #setTriggerBack(boolean) */
     private boolean mTriggerBack;
 
@@ -96,6 +102,10 @@
     private IOnBackInvokedCallback mBackToLauncherCallback;
     private float mTriggerThreshold;
     private float mProgressThreshold;
+    private final Runnable mResetTransitionRunnable = () -> {
+        finishAnimation();
+        mTransitionInProgress = false;
+    };
 
     public BackAnimationController(
             @NonNull @ShellMainThread ShellExecutor shellExecutor,
@@ -229,7 +239,8 @@
         mBackToLauncherCallback = null;
     }
 
-    private void onBackToLauncherAnimationFinished() {
+    @VisibleForTesting
+    void onBackToLauncherAnimationFinished() {
         if (mBackNavigationInfo != null) {
             IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
             if (mTriggerBack) {
@@ -246,6 +257,9 @@
      * {@link BackAnimationController}
      */
     public void onMotionEvent(MotionEvent event, int action, @BackEvent.SwipeEdge int swipeEdge) {
+        if (mTransitionInProgress) {
+            return;
+        }
         if (action == MotionEvent.ACTION_MOVE) {
             if (!mBackGestureStarted) {
                 // Let the animation initialized here to make sure the onPointerDownOutsideFocus
@@ -370,6 +384,9 @@
         IOnBackInvokedCallback targetCallback = shouldDispatchToLauncher
                 ? mBackToLauncherCallback
                 : mBackNavigationInfo.getOnBackInvokedCallback();
+        if (shouldDispatchToLauncher) {
+            startTransition();
+        }
         if (mTriggerBack) {
             dispatchOnBackInvoked(targetCallback);
         } else {
@@ -436,6 +453,9 @@
      * Sets to true when the back gesture has passed the triggering threshold, false otherwise.
      */
     public void setTriggerBack(boolean triggerBack) {
+        if (mTransitionInProgress) {
+            return;
+        }
         mTriggerBack = triggerBack;
     }
 
@@ -467,6 +487,23 @@
             mTransaction.remove(screenshotSurface);
         }
         mTransaction.apply();
+        stopTransition();
         backNavigationInfo.onBackNavigationFinished(triggerBack);
     }
+
+    private void startTransition() {
+        if (mTransitionInProgress) {
+            return;
+        }
+        mTransitionInProgress = true;
+        mShellExecutor.executeDelayed(mResetTransitionRunnable, MAX_TRANSITION_DURATION);
+    }
+
+    private void stopTransition() {
+        if (!mTransitionInProgress) {
+            return;
+        }
+        mShellExecutor.removeCallbacks(mResetTransitionRunnable);
+        mTransitionInProgress = false;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
index b04a1fa..f1ee8fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
@@ -183,7 +183,7 @@
 
         getDrawingRect(mTempBounds);
 
-        mDrawParams.color = mDotColor;
+        mDrawParams.dotColor = mDotColor;
         mDrawParams.iconBounds = mTempBounds;
         mDrawParams.leftAlign = mOnLeft;
         mDrawParams.scale = mDotScale;
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 f407bdc..f427a2c 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
@@ -66,6 +66,7 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationListenerService.RankingMap;
 import android.util.ArraySet;
@@ -96,6 +97,8 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.common.annotations.ShellBackgroundThread;
+import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
@@ -145,6 +148,7 @@
     private final FloatingContentCoordinator mFloatingContentCoordinator;
     private final BubbleDataRepository mDataRepository;
     private final WindowManagerShellWrapper mWindowManagerShellWrapper;
+    private final UserManager mUserManager;
     private final LauncherApps mLauncherApps;
     private final IStatusBarService mBarService;
     private final WindowManager mWindowManager;
@@ -158,6 +162,8 @@
     private final ShellExecutor mMainExecutor;
     private final Handler mMainHandler;
 
+    private final ShellExecutor mBackgroundExecutor;
+
     private BubbleLogger mLogger;
     private BubbleData mBubbleData;
     @Nullable private BubbleStackView mStackView;
@@ -227,6 +233,7 @@
             @Nullable IStatusBarService statusBarService,
             WindowManager windowManager,
             WindowManagerShellWrapper windowManagerShellWrapper,
+            UserManager userManager,
             LauncherApps launcherApps,
             TaskStackListenerImpl taskStackListener,
             UiEventLogger uiEventLogger,
@@ -234,8 +241,9 @@
             DisplayController displayController,
             Optional<OneHandedController> oneHandedOptional,
             DragAndDropController dragAndDropController,
-            ShellExecutor mainExecutor,
-            Handler mainHandler,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
         BubbleLogger logger = new BubbleLogger(uiEventLogger);
@@ -243,9 +251,9 @@
         BubbleData data = new BubbleData(context, logger, positioner, mainExecutor);
         return new BubbleController(context, data, synchronizer, floatingContentCoordinator,
                 new BubbleDataRepository(context, launcherApps, mainExecutor),
-                statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
-                logger, taskStackListener, organizer, positioner, displayController,
-                oneHandedOptional, dragAndDropController, mainExecutor, mainHandler,
+                statusBarService, windowManager, windowManagerShellWrapper, userManager,
+                launcherApps, logger, taskStackListener, organizer, positioner, displayController,
+                oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
                 taskViewTransitions, syncQueue);
     }
 
@@ -261,6 +269,7 @@
             @Nullable IStatusBarService statusBarService,
             WindowManager windowManager,
             WindowManagerShellWrapper windowManagerShellWrapper,
+            UserManager userManager,
             LauncherApps launcherApps,
             BubbleLogger bubbleLogger,
             TaskStackListenerImpl taskStackListener,
@@ -269,8 +278,9 @@
             DisplayController displayController,
             Optional<OneHandedController> oneHandedOptional,
             DragAndDropController dragAndDropController,
-            ShellExecutor mainExecutor,
-            Handler mainHandler,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
         mContext = context;
@@ -281,11 +291,13 @@
                 : statusBarService;
         mWindowManager = windowManager;
         mWindowManagerShellWrapper = windowManagerShellWrapper;
+        mUserManager = userManager;
         mFloatingContentCoordinator = floatingContentCoordinator;
         mDataRepository = dataRepository;
         mLogger = bubbleLogger;
         mMainExecutor = mainExecutor;
         mMainHandler = mainHandler;
+        mBackgroundExecutor = bgExecutor;
         mTaskStackListener = taskStackListener;
         mTaskOrganizer = organizer;
         mSurfaceSynchronizer = synchronizer;
@@ -440,6 +452,10 @@
 
         mOneHandedOptional.ifPresent(this::registerOneHandedState);
         mDragAndDropController.addListener(this::collapseStack);
+
+        // Clear out any persisted bubbles on disk that no longer have a valid user.
+        List<UserInfo> users = mUserManager.getAliveUsers();
+        mDataRepository.sanitizeBubbles(users);
     }
 
     @VisibleForTesting
@@ -583,6 +599,17 @@
         mCurrentProfiles = currentProfiles;
     }
 
+    /** Called when a user is removed from the device, including work profiles. */
+    public void onUserRemoved(int removedUserId) {
+        UserInfo parent = mUserManager.getProfileParent(removedUserId);
+        int parentUserId = parent != null ? parent.getUserHandle().getIdentifier() : -1;
+        mBubbleData.removeBubblesForUser(removedUserId);
+        // Typically calls from BubbleData would remove bubbles from the DataRepository as well,
+        // however, this gets complicated when users are removed (mCurrentUserId won't necessarily
+        // be correct for this) so we update the repo directly.
+        mDataRepository.removeBubblesForUser(removedUserId, parentUserId);
+    }
+
     /** Whether this userId belongs to the current user. */
     private boolean isCurrentProfile(int userId) {
         return userId == UserHandle.USER_ALL
@@ -725,7 +752,8 @@
 
         try {
             mAddedToWindowManager = false;
-            mContext.unregisterReceiver(mBroadcastReceiver);
+            // Put on background for this binder call, was causing jank
+            mBackgroundExecutor.execute(() -> mContext.unregisterReceiver(mBroadcastReceiver));
             if (mStackView != null) {
                 mWindowManager.removeView(mStackView);
                 mBubbleData.getOverflow().cleanUpExpandedState();
@@ -1801,6 +1829,13 @@
         }
 
         @Override
+        public void onUserRemoved(int removedUserId) {
+            mMainExecutor.execute(() -> {
+                BubbleController.this.onUserRemoved(removedUserId);
+            });
+        }
+
+        @Override
         public void onConfigChanged(Configuration newConfig) {
             mMainExecutor.execute(() -> {
                 BubbleController.this.onConfigChanged(newConfig);
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 e4a0fd0..fa86c84 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
@@ -465,7 +465,7 @@
                 getOverflowBubbles(), invalidBubblesFromPackage, removeBubble);
     }
 
-    /** Dismisses all bubbles from the given package. */
+    /** Removes all bubbles from the given package. */
     public void removeBubblesWithPackageName(String packageName, int reason) {
         final Predicate<Bubble> bubbleMatchesPackage = bubble ->
                 bubble.getPackageName().equals(packageName);
@@ -477,6 +477,18 @@
         performActionOnBubblesMatching(getOverflowBubbles(), bubbleMatchesPackage, removeBubble);
     }
 
+    /** Removes all bubbles for the given user. */
+    public void removeBubblesForUser(int userId) {
+        List<Bubble> removedBubbles = filterAllBubbles(bubble ->
+                userId == bubble.getUser().getIdentifier());
+        for (Bubble b : removedBubbles) {
+            doRemove(b.getKey(), Bubbles.DISMISS_USER_REMOVED);
+        }
+        if (!removedBubbles.isEmpty()) {
+            dispatchPendingChanges();
+        }
+    }
+
     private void doAdd(Bubble bubble) {
         if (DEBUG_BUBBLE_DATA) {
             Log.d(TAG, "doAdd: " + bubble);
@@ -552,7 +564,8 @@
                 || reason == Bubbles.DISMISS_BLOCKED
                 || reason == Bubbles.DISMISS_SHORTCUT_REMOVED
                 || reason == Bubbles.DISMISS_PACKAGE_REMOVED
-                || reason == Bubbles.DISMISS_USER_CHANGED;
+                || reason == Bubbles.DISMISS_USER_CHANGED
+                || reason == Bubbles.DISMISS_USER_REMOVED;
 
         int indexToRemove = indexForKey(key);
         if (indexToRemove == -1) {
@@ -1073,6 +1086,35 @@
         return null;
     }
 
+    /**
+     * Returns a list of bubbles that match the provided predicate. This checks all types of
+     * bubbles (i.e. pending, suppressed, active, and overflowed).
+     */
+    private List<Bubble> filterAllBubbles(Predicate<Bubble> predicate) {
+        ArrayList<Bubble> matchingBubbles = new ArrayList<>();
+        for (Bubble b : mPendingBubbles.values()) {
+            if (predicate.test(b)) {
+                matchingBubbles.add(b);
+            }
+        }
+        for (Bubble b : mSuppressedBubbles.values()) {
+            if (predicate.test(b)) {
+                matchingBubbles.add(b);
+            }
+        }
+        for (Bubble b : mBubbles) {
+            if (predicate.test(b)) {
+                matchingBubbles.add(b);
+            }
+        }
+        for (Bubble b : mOverflowBubbles) {
+            if (predicate.test(b)) {
+                matchingBubbles.add(b);
+            }
+        }
+        return matchingBubbles;
+    }
+
     @VisibleForTesting(visibility = PRIVATE)
     void setTimeSource(TimeSource timeSource) {
         mTimeSource = timeSource;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
index 9d9e442..97560f4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleDataRepository.kt
@@ -22,6 +22,7 @@
 import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED
 import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC
 import android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
+import android.content.pm.UserInfo
 import android.os.UserHandle
 import android.util.Log
 import com.android.wm.shell.bubbles.storage.BubbleEntity
@@ -73,6 +74,22 @@
         if (entities.isNotEmpty()) persistToDisk()
     }
 
+    /**
+     * Removes all the bubbles associated with the provided user from memory. Then persists the
+     * snapshot to disk asynchronously.
+     */
+    fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentId: Int) {
+        if (volatileRepository.removeBubblesForUser(userId, parentId)) persistToDisk()
+    }
+
+    /**
+     * Remove any bubbles that don't have a user id from the provided list of users.
+     */
+    fun sanitizeBubbles(users: List<UserInfo>) {
+        val userIds = users.map { u -> u.id }
+        if (volatileRepository.sanitizeBubbles(userIds)) persistToDisk()
+    }
+
     private fun transform(bubbles: List<Bubble>): List<BubbleEntity> {
         return bubbles.mapNotNull { b ->
             BubbleEntity(
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 c7db8d8..8a0db0a 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
@@ -57,7 +57,7 @@
             DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE,
             DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT,
             DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED,
-            DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK})
+            DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_REMOVED})
     @Target({FIELD, LOCAL_VARIABLE, PARAMETER})
     @interface DismissReason {}
 
@@ -76,6 +76,7 @@
     int DISMISS_PACKAGE_REMOVED = 13;
     int DISMISS_NO_BUBBLE_UP = 14;
     int DISMISS_RELOAD_FROM_DISK = 15;
+    int DISMISS_USER_REMOVED = 16;
 
     /**
      * @return {@code true} if there is a bubble associated with the provided key and if its
@@ -243,6 +244,13 @@
     void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles);
 
     /**
+     * Called when a user is removed.
+     *
+     * @param removedUserId the id of the removed user.
+     */
+    void onUserRemoved(int removedUserId);
+
+    /**
      * Called when config changed.
      *
      * @param newConfig the new config.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt
index a5267d8..1eee029 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepository.kt
@@ -15,6 +15,7 @@
  */
 package com.android.wm.shell.bubbles.storage
 
+import android.annotation.UserIdInt
 import android.content.pm.LauncherApps
 import android.os.UserHandle
 import android.util.SparseArray
@@ -95,10 +96,68 @@
     }
 
     @Synchronized
-    fun removeBubbles(userId: Int, bubbles: List<BubbleEntity>) =
+    fun removeBubbles(@UserIdInt userId: Int, bubbles: List<BubbleEntity>) =
             uncache(bubbles.filter { b: BubbleEntity ->
                 getEntities(userId).removeIf { e: BubbleEntity -> b.key == e.key } })
 
+    /**
+     * Removes all the bubbles associated with the provided userId.
+     * @return whether bubbles were removed or not.
+     */
+    @Synchronized
+    fun removeBubblesForUser(@UserIdInt userId: Int, @UserIdInt parentUserId: Int): Boolean {
+        if (parentUserId != -1) {
+            return removeBubblesForUserWithParent(userId, parentUserId)
+        } else {
+            val entities = entitiesByUser.get(userId)
+            entitiesByUser.remove(userId)
+            return entities != null
+        }
+    }
+
+    /**
+     * Removes all the bubbles associated with the provided userId when that userId is part of
+     * a profile (e.g. managed account).
+     *
+     * @return whether bubbles were removed or not.
+     */
+    @Synchronized
+    private fun removeBubblesForUserWithParent(
+        @UserIdInt userId: Int,
+        @UserIdInt parentUserId: Int
+    ): Boolean {
+        if (entitiesByUser.get(parentUserId) != null) {
+            return entitiesByUser.get(parentUserId).removeIf {
+                b: BubbleEntity -> b.userId == userId }
+        }
+        return false
+    }
+
+    /**
+     * Goes through all the persisted bubbles and removes them if the user is not in the active
+     * list of users.
+     *
+     * @return whether the list of bubbles changed or not (i.e. was a removal made).
+     */
+    @Synchronized
+    fun sanitizeBubbles(activeUsers: List<Int>): Boolean {
+        for (i in 0 until entitiesByUser.size()) {
+            // First check if the user is a parent / top-level user
+            val parentUserId = entitiesByUser.keyAt(i)
+            if (!activeUsers.contains(parentUserId)) {
+                entitiesByUser.remove(parentUserId)
+                return true
+            } else if (entitiesByUser.get(parentUserId) != null) {
+                // Then check if each of the bubbles in the top-level user, still has a valid user
+                // as it could belong to a profile and have a different id from the parent.
+                return entitiesByUser.get(parentUserId).removeIf { b: BubbleEntity ->
+                    !activeUsers.contains(b.userId)
+                }
+            }
+        }
+        return false
+    }
+
     private fun cache(bubbles: List<BubbleEntity>) {
         bubbles.groupBy { ShortcutKey(it.userId, it.packageName) }.forEach { (key, bubbles) ->
             launcherApps.cacheShortcuts(key.pkg, bubbles.map { it.shortcutId },
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 3b83f15..6a2acf4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -498,6 +498,11 @@
                 dispatchVisibilityChanged(mDisplayId, isShowing);
             }
         }
+
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        public InsetsSourceControl getImeSourceControl() {
+            return mImeSourceControl;
+        }
     }
 
     void removeImeSurface() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java
index 59374a6..b9ddd36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerCallback.java
@@ -38,7 +38,7 @@
 
     default void onTaskStackChanged() { }
 
-    default void onTaskProfileLocked(int taskId, int userId) { }
+    default void onTaskProfileLocked(RunningTaskInfo taskInfo) { }
 
     default void onTaskDisplayChanged(int taskId, int newDisplayId) { }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
index 3b67005..85e2654 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TaskStackListenerImpl.java
@@ -150,8 +150,8 @@
     }
 
     @Override
-    public void onTaskProfileLocked(int taskId, int userId) {
-        mMainHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskId, userId).sendToTarget();
+    public void onTaskProfileLocked(ActivityManager.RunningTaskInfo taskInfo) {
+        mMainHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskInfo).sendToTarget();
     }
 
     @Override
@@ -341,8 +341,10 @@
                     break;
                 }
                 case ON_TASK_PROFILE_LOCKED: {
+                    final ActivityManager.RunningTaskInfo
+                            info = (ActivityManager.RunningTaskInfo) msg.obj;
                     for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
-                        mTaskStackListeners.get(i).onTaskProfileLocked(msg.arg1, msg.arg2);
+                        mTaskStackListeners.get(i).onTaskProfileLocked(info);
                     }
                     break;
                 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 0b8e631..c94455d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -107,6 +107,8 @@
     private int mOrientation;
     private int mRotation;
 
+    private final boolean mDimNonImeSide;
+
     public SplitLayout(String windowName, Context context, Configuration configuration,
             SplitLayoutHandler splitLayoutHandler,
             SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks,
@@ -131,6 +133,8 @@
         mRootBounds.set(configuration.windowConfiguration.getBounds());
         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null);
         resetDividerPosition();
+
+        mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide);
     }
 
     private int getDividerInsets(Resources resources, Display display) {
@@ -860,10 +864,10 @@
             // Update target dim values
             mLastDim1 = mDimValue1;
             mTargetDim1 = imeTargetPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT && mImeShown
-                    ? ADJUSTED_NONFOCUS_DIM : 0.0f;
+                    && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f;
             mLastDim2 = mDimValue2;
             mTargetDim2 = imeTargetPosition == SPLIT_POSITION_TOP_OR_LEFT && mImeShown
-                    ? ADJUSTED_NONFOCUS_DIM : 0.0f;
+                    && mDimNonImeSide ? ADJUSTED_NONFOCUS_DIM : 0.0f;
 
             // Calculate target bounds offset for IME
             mLastYOffset = mYOffsetForIme;
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 dfd4362..1ea5e21 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
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.os.Handler;
+import android.os.SystemClock;
 
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.WindowManagerShellWrapper;
@@ -39,6 +40,7 @@
 import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.pip.PipUiEventLogger;
 import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm;
+import com.android.wm.shell.pip.tv.TvPipBoundsController;
 import com.android.wm.shell.pip.tv.TvPipBoundsState;
 import com.android.wm.shell.pip.tv.TvPipController;
 import com.android.wm.shell.pip.tv.TvPipMenuController;
@@ -64,6 +66,7 @@
             Context context,
             TvPipBoundsState tvPipBoundsState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+            TvPipBoundsController tvPipBoundsController,
             PipAppOpsListener pipAppOpsListener,
             PipTaskOrganizer pipTaskOrganizer,
             TvPipMenuController tvPipMenuController,
@@ -74,13 +77,13 @@
             PipParamsChangedForwarder pipParamsChangedForwarder,
             DisplayController displayController,
             WindowManagerShellWrapper windowManagerShellWrapper,
-            @ShellMainThread ShellExecutor mainExecutor,
-            @ShellMainThread Handler mainHandler) {
+            @ShellMainThread ShellExecutor mainExecutor) {
         return Optional.of(
                 TvPipController.create(
                         context,
                         tvPipBoundsState,
                         tvPipBoundsAlgorithm,
+                        tvPipBoundsController,
                         pipAppOpsListener,
                         pipTaskOrganizer,
                         pipTransitionController,
@@ -91,8 +94,22 @@
                         pipParamsChangedForwarder,
                         displayController,
                         windowManagerShellWrapper,
-                        mainExecutor,
-                        mainHandler));
+                        mainExecutor));
+    }
+
+    @WMSingleton
+    @Provides
+    static TvPipBoundsController provideTvPipBoundsController(
+            Context context,
+            @ShellMainThread Handler mainHandler,
+            TvPipBoundsState tvPipBoundsState,
+            TvPipBoundsAlgorithm tvPipBoundsAlgorithm) {
+        return new TvPipBoundsController(
+                context,
+                SystemClock::uptimeMillis,
+                mainHandler,
+                tvPipBoundsState,
+                tvPipBoundsAlgorithm);
     }
 
     @WMSingleton
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 1bc9e31..b3799e2 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
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.pm.LauncherApps;
 import android.os.Handler;
+import android.os.UserManager;
 import android.view.WindowManager;
 
 import com.android.internal.jank.InteractionJankMonitor;
@@ -43,6 +44,7 @@
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ChoreographerSfVsync;
+import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.freeform.FreeformTaskListener;
@@ -105,6 +107,7 @@
             IStatusBarService statusBarService,
             WindowManager windowManager,
             WindowManagerShellWrapper windowManagerShellWrapper,
+            UserManager userManager,
             LauncherApps launcherApps,
             TaskStackListenerImpl taskStackListener,
             UiEventLogger uiEventLogger,
@@ -114,13 +117,15 @@
             DragAndDropController dragAndDropController,
             @ShellMainThread ShellExecutor mainExecutor,
             @ShellMainThread Handler mainHandler,
+            @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
         return BubbleController.create(context, null /* synchronizer */,
                 floatingContentCoordinator, statusBarService, windowManager,
-                windowManagerShellWrapper, launcherApps, taskStackListener,
+                windowManagerShellWrapper, userManager, launcherApps, taskStackListener,
                 uiEventLogger, organizer, displayController, oneHandedOptional,
-                dragAndDropController, mainExecutor, mainHandler, taskViewTransitions, syncQueue);
+                dragAndDropController, mainExecutor, mainHandler, bgExecutor,
+                taskViewTransitions, syncQueue);
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 45c9a95..07f2e95 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -1292,7 +1292,8 @@
      */
     public void scheduleOffsetPip(Rect originalBounds, int offset, int duration,
             Consumer<Rect> updateBoundsCallback) {
-        if (mPipTransitionState.shouldBlockResizeRequest()) {
+        if (mPipTransitionState.shouldBlockResizeRequest()
+                || mPipTransitionState.getInSwipePipToHomeTransition()) {
             return;
         }
         if (mWaitForFixedRotation) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
index 21d5d40..a2eadcd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
@@ -29,7 +29,6 @@
 import android.content.res.Resources;
 import android.graphics.Insets;
 import android.graphics.Rect;
-import android.os.SystemClock;
 import android.util.ArraySet;
 import android.util.Size;
 import android.view.Gravity;
@@ -66,7 +65,7 @@
             @NonNull PipSnapAlgorithm pipSnapAlgorithm) {
         super(context, tvPipBoundsState, pipSnapAlgorithm);
         this.mTvPipBoundsState = tvPipBoundsState;
-        this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm(SystemClock::uptimeMillis);
+        this.mKeepClearAlgorithm = new TvPipKeepClearAlgorithm();
         reloadResources(context);
     }
 
@@ -80,7 +79,6 @@
                 res.getDimensionPixelSize(R.dimen.pip_keep_clear_area_padding));
         mKeepClearAlgorithm.setMaxRestrictedDistanceFraction(
                 res.getFraction(R.fraction.config_pipMaxRestrictedMoveDistance, 1, 1));
-        mKeepClearAlgorithm.setStashDuration(res.getInteger(R.integer.config_pipStashDuration));
     }
 
     @Override
@@ -104,7 +102,7 @@
             updateGravityOnExpandToggled(Gravity.NO_GRAVITY, true);
         }
         mTvPipBoundsState.setTvPipExpanded(isPipExpanded);
-        return getTvPipBounds().getBounds();
+        return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds());
     }
 
     /** Returns the current bounds adjusted to the new aspect ratio, if valid. */
@@ -114,13 +112,27 @@
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: getAdjustedDestinationBounds: %f", TAG, newAspectRatio);
         }
-        return getTvPipBounds().getBounds();
+        return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds());
+    }
+
+    Rect adjustBoundsForTemporaryDecor(Rect bounds) {
+        Rect boundsWithDecor = new Rect(bounds);
+        Insets decorInset = mTvPipBoundsState.getPipMenuTemporaryDecorInsets();
+        Insets pipDecorReverseInsets = Insets.subtract(Insets.NONE, decorInset);
+        boundsWithDecor.inset(decorInset);
+        Gravity.apply(mTvPipBoundsState.getTvPipGravity(),
+                boundsWithDecor.width(), boundsWithDecor.height(), bounds, boundsWithDecor);
+
+        // remove temporary decoration again
+        boundsWithDecor.inset(pipDecorReverseInsets);
+        return boundsWithDecor;
     }
 
     /**
      * Calculates the PiP bounds.
      */
-    public Placement getTvPipBounds() {
+    @NonNull
+    public Placement getTvPipPlacement() {
         final Size pipSize = getPipSize();
         final Rect displayBounds = mTvPipBoundsState.getDisplayBounds();
         final Size screenSize = new Size(displayBounds.width(), displayBounds.height());
@@ -153,8 +165,6 @@
         mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset());
         mKeepClearAlgorithm.setPipPermanentDecorInsets(
                 mTvPipBoundsState.getPipMenuPermanentDecorInsets());
-        mKeepClearAlgorithm.setPipTemporaryDecorInsets(
-                mTvPipBoundsState.getPipMenuTemporaryDecorInsets());
 
         final Placement placement = mKeepClearAlgorithm.calculatePipPosition(
                 pipSize,
@@ -407,8 +417,4 @@
                     TAG, expandedSize.getWidth(), expandedSize.getHeight());
         }
     }
-
-    void keepUnstashedForCurrentKeepClearAreas() {
-        mKeepClearAlgorithm.keepUnstashedForCurrentKeepClearAreas();
-    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java
new file mode 100644
index 0000000..3a6ce81
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsController.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv;
+
+import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.os.Handler;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * Controller managing the PiP's position.
+ * Manages debouncing of PiP movements and scheduling of unstashing.
+ */
+public class TvPipBoundsController {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "TvPipBoundsController";
+
+    /**
+     * Time the calculated PiP position needs to be stable before PiP is moved there,
+     * to avoid erratic movement.
+     * Some changes will cause the PiP to be repositioned immediately, such as changes to
+     * unrestricted keep clear areas.
+     */
+    @VisibleForTesting
+    static final long POSITION_DEBOUNCE_TIMEOUT_MILLIS = 300L;
+
+    private final Context mContext;
+    private final Supplier<Long> mClock;
+    private final Handler mMainHandler;
+    private final TvPipBoundsState mTvPipBoundsState;
+    private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
+
+    @Nullable
+    private PipBoundsListener mListener;
+
+    private int mResizeAnimationDuration;
+    private int mStashDurationMs;
+    private Rect mCurrentPlacementBounds;
+    private Rect mPipTargetBounds;
+
+    private final Runnable mApplyPendingPlacementRunnable = this::applyPendingPlacement;
+    private boolean mPendingStash;
+    private Placement mPendingPlacement;
+    private int mPendingPlacementAnimationDuration;
+    private Runnable mUnstashRunnable;
+
+    public TvPipBoundsController(
+            Context context,
+            Supplier<Long> clock,
+            Handler mainHandler,
+            TvPipBoundsState tvPipBoundsState,
+            TvPipBoundsAlgorithm tvPipBoundsAlgorithm) {
+        mContext = context;
+        mClock = clock;
+        mMainHandler = mainHandler;
+        mTvPipBoundsState = tvPipBoundsState;
+        mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
+
+        loadConfigurations();
+    }
+
+    private void loadConfigurations() {
+        final Resources res = mContext.getResources();
+        mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
+        mStashDurationMs = res.getInteger(R.integer.config_pipStashDuration);
+    }
+
+    void setListener(PipBoundsListener listener) {
+        mListener = listener;
+    }
+
+    /**
+     * Update the PiP bounds based on the state of the PiP, decors, and keep clear areas.
+     * Unless {@code immediate} is {@code true}, the PiP does not move immediately to avoid
+     * keep clear areas, but waits for a new position to stay uncontested for
+     * {@link #POSITION_DEBOUNCE_TIMEOUT_MILLIS} before moving to it.
+     * Temporary decor changes are applied immediately.
+     *
+     * @param stayAtAnchorPosition If true, PiP will be placed at the anchor position
+     * @param disallowStashing     If true, PiP will not be placed off-screen in a stashed position
+     * @param animationDuration    Duration of the animation to the new position
+     * @param immediate            If true, PiP will move immediately to avoid keep clear areas
+     */
+    @VisibleForTesting
+    void recalculatePipBounds(boolean stayAtAnchorPosition, boolean disallowStashing,
+            int animationDuration, boolean immediate) {
+        final Placement placement = mTvPipBoundsAlgorithm.getTvPipPlacement();
+
+        final int stashType = disallowStashing ? STASH_TYPE_NONE : placement.getStashType();
+        mTvPipBoundsState.setStashed(stashType);
+        if (stayAtAnchorPosition) {
+            cancelScheduledPlacement();
+            applyPlacementBounds(placement.getAnchorBounds(), animationDuration);
+        } else if (disallowStashing) {
+            cancelScheduledPlacement();
+            applyPlacementBounds(placement.getUnstashedBounds(), animationDuration);
+        } else if (immediate) {
+            cancelScheduledPlacement();
+            applyPlacementBounds(placement.getBounds(), animationDuration);
+            scheduleUnstashIfNeeded(placement);
+        } else {
+            applyPlacementBounds(mCurrentPlacementBounds, animationDuration);
+            schedulePinnedStackPlacement(placement, animationDuration);
+        }
+    }
+
+    private void schedulePinnedStackPlacement(@NonNull final Placement placement,
+            int animationDuration) {
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: schedulePinnedStackPlacement() - pip bounds: %s",
+                    TAG, placement.getBounds().toShortString());
+        }
+
+        if (mPendingPlacement != null && Objects.equals(mPendingPlacement.getBounds(),
+                placement.getBounds())) {
+            mPendingStash = mPendingStash || placement.getTriggerStash();
+            return;
+        }
+
+        mPendingStash = placement.getStashType() != STASH_TYPE_NONE
+                && (mPendingStash || placement.getTriggerStash());
+
+        mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable);
+        mPendingPlacement = placement;
+        mPendingPlacementAnimationDuration = animationDuration;
+        mMainHandler.postAtTime(mApplyPendingPlacementRunnable,
+                mClock.get() + POSITION_DEBOUNCE_TIMEOUT_MILLIS);
+    }
+
+    private void scheduleUnstashIfNeeded(final Placement placement) {
+        if (mUnstashRunnable != null) {
+            mMainHandler.removeCallbacks(mUnstashRunnable);
+            mUnstashRunnable = null;
+        }
+        if (placement.getUnstashDestinationBounds() != null) {
+            mUnstashRunnable = () -> {
+                applyPlacementBounds(placement.getUnstashDestinationBounds(),
+                        mResizeAnimationDuration);
+                mUnstashRunnable = null;
+            };
+            mMainHandler.postAtTime(mUnstashRunnable, mClock.get() + mStashDurationMs);
+        }
+    }
+
+    private void applyPendingPlacement() {
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: applyPendingPlacement()", TAG);
+        }
+        if (mPendingPlacement != null) {
+            if (mPendingStash) {
+                mPendingStash = false;
+                scheduleUnstashIfNeeded(mPendingPlacement);
+            }
+
+            if (mUnstashRunnable != null) {
+                // currently stashed, use stashed pos
+                applyPlacementBounds(mPendingPlacement.getBounds(),
+                        mPendingPlacementAnimationDuration);
+            } else {
+                applyPlacementBounds(mPendingPlacement.getUnstashedBounds(),
+                        mPendingPlacementAnimationDuration);
+            }
+        }
+
+        mPendingPlacement = null;
+    }
+
+    void onPipDismissed() {
+        mCurrentPlacementBounds = null;
+        mPipTargetBounds = null;
+        cancelScheduledPlacement();
+    }
+
+    private void cancelScheduledPlacement() {
+        mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable);
+        mPendingPlacement = null;
+
+        if (mUnstashRunnable != null) {
+            mMainHandler.removeCallbacks(mUnstashRunnable);
+            mUnstashRunnable = null;
+        }
+    }
+
+    private void applyPlacementBounds(Rect bounds, int animationDuration) {
+        if (bounds == null) {
+            return;
+        }
+
+        mCurrentPlacementBounds = bounds;
+        Rect adjustedBounds = mTvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(bounds);
+        movePipTo(adjustedBounds, animationDuration);
+    }
+
+    /** Animates the PiP to the given bounds with the given animation duration. */
+    private void movePipTo(Rect bounds, int animationDuration) {
+        if (Objects.equals(mPipTargetBounds, bounds)) {
+            return;
+        }
+
+        mPipTargetBounds = bounds;
+        if (DEBUG) {
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString());
+        }
+
+        if (mListener != null) {
+            mListener.onPipTargetBoundsChange(bounds, animationDuration);
+        }
+    }
+
+    /**
+     * Interface being notified of changes to the PiP bounds as calculated by
+     * @link TvPipBoundsController}.
+     */
+    public interface PipBoundsListener {
+        /**
+         * Called when the calculated PiP bounds are changing.
+         *
+         * @param newTargetBounds The new bounds of the PiP.
+         * @param animationDuration The animation duration for the PiP movement.
+         */
+        void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration);
+    }
+}
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 7667794..fa48def 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
@@ -25,12 +25,10 @@
 import android.app.PendingIntent;
 import android.app.RemoteAction;
 import android.app.TaskInfo;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.os.Handler;
 import android.os.RemoteException;
 import android.view.Gravity;
 
@@ -46,25 +44,24 @@
 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;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
-import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
  * Manages the picture-in-picture (PIP) UI and states.
  */
 public class TvPipController implements PipTransitionController.PipTransitionCallback,
-        TvPipMenuController.Delegate, TvPipNotificationController.Delegate,
-        DisplayController.OnDisplaysChangedListener {
+        TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate,
+        TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener {
     private static final String TAG = "TvPipController";
     static final boolean DEBUG = false;
 
@@ -98,19 +95,18 @@
 
     private final TvPipBoundsState mTvPipBoundsState;
     private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
+    private final TvPipBoundsController mTvPipBoundsController;
     private final PipAppOpsListener mAppOpsListener;
     private final PipTaskOrganizer mPipTaskOrganizer;
     private final PipMediaController mPipMediaController;
     private final TvPipNotificationController mPipNotificationController;
     private final TvPipMenuController mTvPipMenuController;
     private final ShellExecutor mMainExecutor;
-    private final Handler mMainHandler;
     private final TvPipImpl mImpl = new TvPipImpl();
 
     private @State int mState = STATE_NO_PIP;
     private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY;
     private int mPinnedTaskId = NONEXISTENT_TASK_ID;
-    private Runnable mUnstashRunnable;
 
     private RemoteAction mCloseAction;
     // How long the shell will wait for the app to close the PiP if a custom action is set.
@@ -123,6 +119,7 @@
             Context context,
             TvPipBoundsState tvPipBoundsState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+            TvPipBoundsController tvPipBoundsController,
             PipAppOpsListener pipAppOpsListener,
             PipTaskOrganizer pipTaskOrganizer,
             PipTransitionController pipTransitionController,
@@ -133,12 +130,12 @@
             PipParamsChangedForwarder pipParamsChangedForwarder,
             DisplayController displayController,
             WindowManagerShellWrapper wmShell,
-            ShellExecutor mainExecutor,
-            Handler mainHandler) {
+            ShellExecutor mainExecutor) {
         return new TvPipController(
                 context,
                 tvPipBoundsState,
                 tvPipBoundsAlgorithm,
+                tvPipBoundsController,
                 pipAppOpsListener,
                 pipTaskOrganizer,
                 pipTransitionController,
@@ -149,14 +146,14 @@
                 pipParamsChangedForwarder,
                 displayController,
                 wmShell,
-                mainExecutor,
-                mainHandler).mImpl;
+                mainExecutor).mImpl;
     }
 
     private TvPipController(
             Context context,
             TvPipBoundsState tvPipBoundsState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+            TvPipBoundsController tvPipBoundsController,
             PipAppOpsListener pipAppOpsListener,
             PipTaskOrganizer pipTaskOrganizer,
             PipTransitionController pipTransitionController,
@@ -167,16 +164,16 @@
             PipParamsChangedForwarder pipParamsChangedForwarder,
             DisplayController displayController,
             WindowManagerShellWrapper wmShell,
-            ShellExecutor mainExecutor,
-            Handler mainHandler) {
+            ShellExecutor mainExecutor) {
         mContext = context;
         mMainExecutor = mainExecutor;
-        mMainHandler = mainHandler;
 
         mTvPipBoundsState = tvPipBoundsState;
         mTvPipBoundsState.setDisplayId(context.getDisplayId());
         mTvPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay()));
         mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
+        mTvPipBoundsController = tvPipBoundsController;
+        mTvPipBoundsController.setListener(this);
 
         mPipMediaController = pipMediaController;
 
@@ -227,7 +224,7 @@
     /**
      * Starts the process if bringing up the Pip menu if by issuing a command to move Pip
      * task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip
-     * task/window is properly positioned in {@link #onPipTransitionFinished(ComponentName, int)}.
+     * task/window is properly positioned in {@link #onPipTransitionFinished(int)}.
      */
     @Override
     public void showPictureInPictureMenu() {
@@ -256,7 +253,6 @@
                     "%s: closeMenu(), state before=%s", TAG, stateToName(mState));
         }
         setState(STATE_PIP);
-        mTvPipBoundsAlgorithm.keepUnstashedForCurrentKeepClearAreas();
         updatePinnedStackBounds();
     }
 
@@ -331,68 +327,35 @@
     public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted,
             Set<Rect> unrestricted) {
         if (mTvPipBoundsState.getDisplayId() == displayId) {
+            boolean unrestrictedAreasChanged = !Objects.equals(unrestricted,
+                    mTvPipBoundsState.getUnrestrictedKeepClearAreas());
             mTvPipBoundsState.setKeepClearAreas(restricted, unrestricted);
-            updatePinnedStackBounds();
+            updatePinnedStackBounds(mResizeAnimationDuration, unrestrictedAreasChanged);
         }
     }
 
     private void updatePinnedStackBounds() {
-        updatePinnedStackBounds(mResizeAnimationDuration);
+        updatePinnedStackBounds(mResizeAnimationDuration, true);
     }
 
     /**
      * Update the PiP bounds based on the state of the PiP and keep clear areas.
-     * Animates to the current PiP bounds, and schedules unstashing the PiP if necessary.
      */
-    private void updatePinnedStackBounds(int animationDuration) {
+    private void updatePinnedStackBounds(int animationDuration, boolean immediate) {
         if (mState == STATE_NO_PIP) {
             return;
         }
-
         final boolean stayAtAnchorPosition = mTvPipMenuController.isInMoveMode();
         final boolean disallowStashing = mState == STATE_PIP_MENU || stayAtAnchorPosition;
-        final Placement placement = mTvPipBoundsAlgorithm.getTvPipBounds();
-
-        int stashType =
-                disallowStashing ? PipBoundsState.STASH_TYPE_NONE : placement.getStashType();
-        mTvPipBoundsState.setStashed(stashType);
-
-        if (stayAtAnchorPosition) {
-            movePinnedStackTo(placement.getAnchorBounds());
-        } else if (disallowStashing) {
-            movePinnedStackTo(placement.getUnstashedBounds());
-        } else {
-            movePinnedStackTo(placement.getBounds());
-        }
-
-        if (mUnstashRunnable != null) {
-            mMainHandler.removeCallbacks(mUnstashRunnable);
-            mUnstashRunnable = null;
-        }
-        if (!disallowStashing && placement.getUnstashDestinationBounds() != null) {
-            mUnstashRunnable = () -> {
-                movePinnedStackTo(placement.getUnstashDestinationBounds(), animationDuration);
-            };
-            mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime());
-        }
+        mTvPipBoundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing,
+                animationDuration, immediate);
     }
 
-    /** Animates the PiP to the given bounds. */
-    private void movePinnedStackTo(Rect bounds) {
-        movePinnedStackTo(bounds, mResizeAnimationDuration);
-    }
-
-    /** Animates the PiP to the given bounds with the given animation duration. */
-    private void movePinnedStackTo(Rect bounds, int animationDuration) {
-        if (DEBUG) {
-            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: movePinnedStack() - new pip bounds: %s", TAG, bounds.toShortString());
-        }
-        mPipTaskOrganizer.scheduleAnimateResizePip(bounds,
-                animationDuration, rect -> {
-                    mTvPipMenuController.updateExpansionState();
-                });
-        mTvPipMenuController.onPipTransitionStarted(bounds);
+    @Override
+    public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) {
+        mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds,
+                animationDuration, rect -> mTvPipMenuController.updateExpansionState());
+        mTvPipMenuController.onPipTransitionStarted(newTargetBounds);
     }
 
     /**
@@ -431,7 +394,7 @@
 
     @Override
     public void closeEduText() {
-        updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs);
+        updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false);
     }
 
     private void registerSessionListenerForCurrentUser() {
@@ -473,6 +436,7 @@
         mPipNotificationController.dismiss();
         mTvPipMenuController.closeMenu();
         mTvPipBoundsState.resetTvPipState();
+        mTvPipBoundsController.onPipDismissed();
         setState(STATE_NO_PIP);
         mPinnedTaskId = NONEXISTENT_TASK_ID;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt
index 07dccd5..1e54436 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt
@@ -33,17 +33,14 @@
 import kotlin.math.roundToInt
 
 private const val DEFAULT_PIP_MARGINS = 48
-private const val DEFAULT_STASH_DURATION = 5000L
 private const val RELAX_DEPTH = 1
 private const val DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION = 0.15
 
 /**
  * This class calculates an appropriate position for a Picture-In-Picture (PiP) window, taking
  * into account app defined keep clear areas.
- *
- * @param clock A function returning a current timestamp (in milliseconds)
  */
-class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
+class TvPipKeepClearAlgorithm() {
     /**
      * Result of the positioning algorithm.
      *
@@ -51,17 +48,17 @@
      * @param anchorBounds The bounds of the PiP anchor position
      *     (where the PiP would be placed if there were no keep clear areas)
      * @param stashType Where the PiP has been stashed, if at all
-     * @param unstashDestinationBounds If stashed, the PiP should move to this position after
-     *     [stashDuration] has passed.
-     * @param unstashTime If stashed, the time at which the PiP should move
-     *     to [unstashDestinationBounds]
+     * @param unstashDestinationBounds If stashed, the PiP should move to this position when
+     *     unstashing.
+     * @param triggerStash Whether this placement should trigger the PiP to stash, or extend
+     *     the unstash timeout if already stashed.
      */
     data class Placement(
         val bounds: Rect,
         val anchorBounds: Rect,
         @PipBoundsState.StashType val stashType: Int = STASH_TYPE_NONE,
         val unstashDestinationBounds: Rect? = null,
-        val unstashTime: Long = 0L
+        val triggerStash: Boolean = false
     ) {
         /** Bounds to use if the PiP should not be stashed. */
         fun getUnstashedBounds() = unstashDestinationBounds ?: bounds
@@ -79,12 +76,6 @@
     /** The distance the PiP peeks into the screen when stashed */
     var stashOffset = DEFAULT_PIP_MARGINS
 
-    /**
-     * How long (in milliseconds) the PiP should stay stashed for after the last time the
-     * keep clear areas causing the PiP to stash have changed.
-     */
-    var stashDuration = DEFAULT_STASH_DURATION
-
     /** The fraction of screen width/height restricted keep clear areas can move the PiP */
     var maxRestrictedDistanceFraction = DEFAULT_MAX_RESTRICTED_DISTANCE_FRACTION
 
@@ -93,14 +84,10 @@
     private var transformedMovementBounds = Rect()
 
     private var lastAreasOverlappingUnstashPosition: Set<Rect> = emptySet()
-    private var lastStashTime: Long = Long.MIN_VALUE
 
     /** Spaces around the PiP that we should leave space for when placing the PiP. Permanent PiP
      * decorations are relevant for calculating intersecting keep clear areas */
     private var pipPermanentDecorInsets = Insets.NONE
-    /** Spaces around the PiP that we should leave space for when placing the PiP. Temporary PiP
-     * decorations are not relevant for calculating intersecting keep clear areas */
-    private var pipTemporaryDecorInsets = Insets.NONE
 
     /**
      * Calculates the position the PiP should be placed at, taking into consideration the
@@ -113,8 +100,8 @@
      * always try to respect these areas.
      *
      * If no free space the PiP is allowed to move to can be found, a stashed position is returned
-     * as [Placement.bounds], along with a position to move to once [Placement.unstashTime] has
-     * passed as [Placement.unstashDestinationBounds].
+     * as [Placement.bounds], along with a position to move to when the PiP unstashes
+     * as [Placement.unstashDestinationBounds].
      *
      * @param pipSize The size of the PiP window
      * @param restrictedAreas The restricted keep clear areas
@@ -130,13 +117,11 @@
         val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas)
 
         val pipSizeWithAllDecors = addDecors(pipSize)
-        val pipAnchorBoundsWithAllDecors =
+        val pipAnchorBoundsWithDecors =
             getNormalPipAnchorBounds(pipSizeWithAllDecors, transformedMovementBounds)
 
-        val pipAnchorBoundsWithPermanentDecors =
-            removeTemporaryDecorsTransformed(pipAnchorBoundsWithAllDecors)
         val result = calculatePipPositionTransformed(
-            pipAnchorBoundsWithPermanentDecors,
+            pipAnchorBoundsWithDecors,
             transformedRestrictedAreas,
             transformedUnrestrictedAreas
         )
@@ -152,7 +137,7 @@
             anchorBounds,
             getStashType(pipBounds, unstashedDestBounds),
             unstashedDestBounds,
-            result.unstashTime
+            result.triggerStash
         )
     }
 
@@ -213,26 +198,13 @@
             !lastAreasOverlappingUnstashPosition.containsAll(areasOverlappingUnstashPosition)
         lastAreasOverlappingUnstashPosition = areasOverlappingUnstashPosition
 
-        val now = clock()
-        if (areasOverlappingUnstashPositionChanged) {
-            lastStashTime = now
-        }
-
-        // If overlapping areas haven't changed and the stash duration has passed, we can
-        // place the PiP at the unstash position
-        val unstashTime = lastStashTime + stashDuration
-        if (now >= unstashTime) {
-            return Placement(unstashBounds, pipAnchorBounds)
-        }
-
-        // Otherwise, we'll stash it close to the unstash position
         val stashedBounds = getNearbyStashedPosition(unstashBounds, keepClearAreas)
         return Placement(
             stashedBounds,
             pipAnchorBounds,
             getStashType(stashedBounds, unstashBounds),
             unstashBounds,
-            unstashTime
+            areasOverlappingUnstashPositionChanged
         )
     }
 
@@ -439,14 +411,6 @@
     }
 
     /**
-     * Prevents the PiP from being stashed for the current set of keep clear areas.
-     * The PiP may stash again if keep clear areas change.
-     */
-    fun keepUnstashedForCurrentKeepClearAreas() {
-        lastStashTime = Long.MIN_VALUE
-    }
-
-    /**
      * Updates the size of the screen.
      *
      * @param size The new size of the screen
@@ -492,10 +456,6 @@
         pipPermanentDecorInsets = insets
     }
 
-    fun setPipTemporaryDecorInsets(insets: Insets) {
-        pipTemporaryDecorInsets = insets
-    }
-
     /**
      * @param open Whether this event marks the opening of an occupied segment
      * @param pos The coordinate of this event
@@ -790,7 +750,6 @@
     private fun addDecors(size: Size): Size {
         val bounds = Rect(0, 0, size.width, size.height)
         bounds.inset(pipPermanentDecorInsets)
-        bounds.inset(pipTemporaryDecorInsets)
 
         return Size(bounds.width(), bounds.height())
     }
@@ -805,19 +764,6 @@
         return bounds
     }
 
-    /**
-     * Removes the space that was reserved for temporary decorations around the PiP
-     * @param bounds the bounds (in base case) to remove the insets from
-     */
-    private fun removeTemporaryDecorsTransformed(bounds: Rect): Rect {
-        if (pipTemporaryDecorInsets == Insets.NONE) return bounds
-
-        val reverseInsets = Insets.subtract(Insets.NONE, pipTemporaryDecorInsets)
-        val boundsInScreenSpace = fromTransformedSpace(bounds)
-        boundsInScreenSpace.inset(reverseInsets)
-        return toTransformedSpace(boundsInScreenSpace)
-    }
-
     private fun Rect.offsetCopy(dx: Int, dy: Int) = Rect(this).apply { offset(dx, dy) }
     private fun Rect.intersectsX(other: Rect) = right >= other.left && left <= other.right
     private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index 132c044..4ce45e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -209,7 +209,7 @@
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: showMovementMenuOnly()", TAG);
         }
-        mInMoveMode = true;
+        setInMoveMode(true);
         mCloseAfterExitMoveMenu = true;
         showMenuInternal();
     }
@@ -219,7 +219,7 @@
         if (DEBUG) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG);
         }
-        mInMoveMode = false;
+        setInMoveMode(false);
         mCloseAfterExitMoveMenu = false;
         showMenuInternal();
     }
@@ -293,6 +293,17 @@
         return mInMoveMode;
     }
 
+    private void setInMoveMode(boolean moveMode) {
+        if (mInMoveMode == moveMode) {
+            return;
+        }
+
+        mInMoveMode = moveMode;
+        if (mDelegate != null) {
+            mDelegate.onInMoveModeChanged();
+        }
+    }
+
     @Override
     public void onEnterMoveMode() {
         if (DEBUG) {
@@ -300,7 +311,7 @@
                     "%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode,
                     mCloseAfterExitMoveMenu);
         }
-        mInMoveMode = true;
+        setInMoveMode(true);
         mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
     }
 
@@ -312,13 +323,13 @@
                     mCloseAfterExitMoveMenu);
         }
         if (mCloseAfterExitMoveMenu) {
-            mInMoveMode = false;
+            setInMoveMode(false);
             mCloseAfterExitMoveMenu = false;
             closeMenu();
             return true;
         }
         if (mInMoveMode) {
-            mInMoveMode = false;
+            setInMoveMode(false);
             mPipMenuView.showButtonsMenu();
             return true;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 63774fb..1d976ce 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -1115,7 +1115,7 @@
             final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
             mDividerFadeInAnimator = ValueAnimator.ofFloat(0f, 1f);
             mDividerFadeInAnimator.addUpdateListener(animation -> {
-                if (dividerLeash == null) {
+                if (dividerLeash == null || !dividerLeash.isValid()) {
                     mDividerFadeInAnimator.cancel();
                     return;
                 }
@@ -1125,7 +1125,7 @@
             mDividerFadeInAnimator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationStart(Animator animation) {
-                    if (dividerLeash == null) {
+                    if (dividerLeash == null || !dividerLeash.isValid()) {
                         mDividerFadeInAnimator.cancel();
                         return;
                     }
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index a899709..ea10be5 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -37,6 +37,7 @@
         "androidx.test.ext.junit",
         "androidx.dynamicanimation_dynamicanimation",
         "dagger2",
+        "frameworks-base-testutils",
         "kotlinx-coroutines-android",
         "kotlinx-coroutines-core",
         "mockito-target-extended-minus-junit4",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 42b1014..49b6968 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -26,8 +26,10 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 import android.app.IActivityTaskManager;
 import android.app.WindowConfiguration;
@@ -180,7 +182,8 @@
         // b/207481538, we check that the surface is not moved for now, we can re-enable this once
         // we implement the animation
         verify(mTransaction, never()).setScale(eq(screenshotSurface), anyInt(), anyInt());
-        verify(mTransaction, never()).setPosition(animationTarget.leash, 100, 100);
+        verify(mTransaction, never()).setPosition(
+                animationTarget.leash, 100, 100);
         verify(mTransaction, atLeastOnce()).apply();
     }
 
@@ -251,6 +254,47 @@
         verify(mIOnBackInvokedCallback, never()).onBackInvoked();
     }
 
+    @Test
+    public void ignoresGesture_transitionInProgress() throws RemoteException {
+        mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
+        RemoteAnimationTarget animationTarget = createAnimationTarget();
+        createNavigationInfo(animationTarget, null, null,
+                BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+
+        triggerBackGesture();
+        // Check that back invocation is dispatched.
+        verify(mIOnBackInvokedCallback).onBackInvoked();
+
+        reset(mIOnBackInvokedCallback);
+        // Verify that we prevent animation from restarting if another gestures happens before
+        // the previous transition is finished.
+        doMotionEvent(MotionEvent.ACTION_DOWN, 0);
+        verifyNoMoreInteractions(mIOnBackInvokedCallback);
+
+        // Verify that we start accepting gestures again once transition finishes.
+        mController.onBackToLauncherAnimationFinished();
+        doMotionEvent(MotionEvent.ACTION_DOWN, 0);
+        doMotionEvent(MotionEvent.ACTION_MOVE, 100);
+        verify(mIOnBackInvokedCallback).onBackStarted();
+    }
+
+    @Test
+    public void acceptsGesture_transitionTimeout() throws RemoteException {
+        mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
+        RemoteAnimationTarget animationTarget = createAnimationTarget();
+        createNavigationInfo(animationTarget, null, null,
+                BackNavigationInfo.TYPE_RETURN_TO_HOME, null);
+
+        triggerBackGesture();
+        reset(mIOnBackInvokedCallback);
+
+        // Simulate transition timeout.
+        mShellExecutor.flushAll();
+        doMotionEvent(MotionEvent.ACTION_DOWN, 0);
+        doMotionEvent(MotionEvent.ACTION_MOVE, 100);
+        verify(mIOnBackInvokedCallback).onBackStarted();
+    }
+
     private void doMotionEvent(int actionDown, int coordinate) {
         mController.onMotionEvent(
                 MotionEvent.obtain(0, mEventTime, actionDown, coordinate, coordinate, 0),
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 bde94d9..e6711ac 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
@@ -129,8 +129,8 @@
         mEntryA3 = createBubbleEntry(1, "a3", "package.a", null);
         mEntryB1 = createBubbleEntry(1, "b1", "package.b", null);
         mEntryB2 = createBubbleEntry(1, "b2", "package.b", null);
-        mEntryB3 = createBubbleEntry(1, "b3", "package.b", null);
-        mEntryC1 = createBubbleEntry(1, "c1", "package.c", null);
+        mEntryB3 = createBubbleEntry(11, "b3", "package.b", null);
+        mEntryC1 = createBubbleEntry(11, "c1", "package.c", null);
 
         NotificationListenerService.Ranking ranking =
                 mock(NotificationListenerService.Ranking.class);
@@ -1058,6 +1058,37 @@
         assertBubbleListContains(mBubbleA2, mBubbleA1, mBubbleLocusId);
     }
 
+    @Test
+    public void test_removeBubblesForUser() {
+        // A is user 1
+        sendUpdatedEntryAtTime(mEntryA1, 2000);
+        sendUpdatedEntryAtTime(mEntryA2, 3000);
+        // B & C belong to user 11
+        sendUpdatedEntryAtTime(mEntryB3, 4000);
+        sendUpdatedEntryAtTime(mEntryC1, 5000);
+        mBubbleData.setListener(mListener);
+
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
+        verifyUpdateReceived();
+        assertOverflowChangedTo(ImmutableList.of(mBubbleA1));
+        assertBubbleListContains(mBubbleC1, mBubbleB3, mBubbleA2);
+
+        // Remove all the A bubbles
+        mBubbleData.removeBubblesForUser(1);
+        verifyUpdateReceived();
+
+        // Verify the update has the removals.
+        BubbleData.Update update = mUpdateCaptor.getValue();
+        assertThat(update.removedBubbles.get(0)).isEqualTo(
+                Pair.create(mBubbleA2, Bubbles.DISMISS_USER_REMOVED));
+        assertThat(update.removedBubbles.get(1)).isEqualTo(
+                Pair.create(mBubbleA1, Bubbles.DISMISS_USER_REMOVED));
+
+        // Verify no A bubbles in active or overflow.
+        assertBubbleListContains(mBubbleC1, mBubbleB3);
+        assertOverflowChangedTo(ImmutableList.of());
+    }
+
     private void verifyUpdateReceived() {
         verify(mListener).applyUpdate(mUpdateCaptor.capture());
         reset(mListener);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt
index bfdf520..9f0d89b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/storage/BubbleVolatileRepositoryTest.kt
@@ -23,14 +23,19 @@
 import androidx.test.filters.SmallTest
 import org.junit.Test
 import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertEquals
 import org.junit.Before
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mockito
 import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
+import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -41,17 +46,17 @@
     private val user11 = UserHandle.of(11)
 
     // user, package, shortcut, notification key, height, res-height, title, taskId, locusId
-    private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1",
-            "0key-1", 120, 0, null, 1, null)
-    private val bubble2 = BubbleEntity(10, "com.example.chat", "alice and bob",
-            "10key-2", 0, 16537428, "title", 2, null)
-    private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2",
-            "0key-3", 120, 0, null, INVALID_TASK_ID, null)
+    private val bubble1 = BubbleEntity(user0.identifier,
+            "com.example.messenger", "shortcut-1", "0key-1", 120, 0, null, 1, null)
+    private val bubble2 = BubbleEntity(user10_managed.identifier,
+            "com.example.chat", "alice and bob", "10key-2", 0, 16537428, "title", 2, null)
+    private val bubble3 = BubbleEntity(user0.identifier,
+            "com.example.messenger", "shortcut-2", "0key-3", 120, 0, null, INVALID_TASK_ID, null)
 
-    private val bubble11 = BubbleEntity(11, "com.example.messenger",
-            "shortcut-1", "01key-1", 120, 0, null, 3)
-    private val bubble12 = BubbleEntity(11, "com.example.chat", "alice and bob",
-            "11key-2", 0, 16537428, "title", INVALID_TASK_ID)
+    private val bubble11 = BubbleEntity(user11.identifier,
+            "com.example.messenger", "shortcut-1", "01key-1", 120, 0, null, 3)
+    private val bubble12 = BubbleEntity(user11.identifier,
+            "com.example.chat", "alice and bob", "11key-2", 0, 16537428, "title", INVALID_TASK_ID)
 
     private val user0bubbles = listOf(bubble1, bubble2, bubble3)
     private val user11bubbles = listOf(bubble11, bubble12)
@@ -151,6 +156,125 @@
         repository.addBubbles(user0.identifier, listOf(bubbleModified))
         assertEquals(bubbleModified, repository.getEntities(user0.identifier).get(0))
     }
+
+    @Test
+    fun testRemoveBubblesForUser() {
+        repository.addBubbles(user0.identifier, user0bubbles)
+        assertThat(repository.getEntities(user0.identifier).toList())
+                .isEqualTo(listOf(bubble1, bubble2, bubble3))
+
+        val ret = repository.removeBubblesForUser(user0.identifier, -1)
+        assertThat(ret).isTrue() // bubbles were removed
+
+        assertThat(repository.getEntities(user0.identifier).toList()).isEmpty()
+        verify(launcherApps, never()).uncacheShortcuts(anyString(),
+                any(),
+                any(UserHandle::class.java), anyInt())
+    }
+
+    @Test
+    fun testRemoveBubblesForUser_parentUserRemoved() {
+        repository.addBubbles(user0.identifier, user0bubbles)
+        // bubble2 is the work profile bubble
+        assertThat(repository.getEntities(user0.identifier).toList())
+                .isEqualTo(listOf(bubble1, bubble2, bubble3))
+
+        val ret = repository.removeBubblesForUser(user10_managed.identifier, user0.identifier)
+        assertThat(ret).isTrue() // bubbles were removed
+
+        assertThat(repository.getEntities(user0.identifier).toList())
+                .isEqualTo(listOf(bubble1, bubble3))
+        verify(launcherApps, never()).uncacheShortcuts(anyString(),
+                any(),
+                any(UserHandle::class.java), anyInt())
+    }
+
+    @Test
+    fun testRemoveBubblesForUser_withoutBubbles() {
+        repository.addBubbles(user0.identifier, user0bubbles)
+        assertThat(repository.getEntities(user0.identifier).toList())
+                .isEqualTo(listOf(bubble1, bubble2, bubble3))
+
+        val ret = repository.removeBubblesForUser(user11.identifier, -1)
+        assertThat(ret).isFalse() // bubbles were NOT removed
+
+        assertThat(repository.getEntities(user0.identifier).toList())
+                .isEqualTo(listOf(bubble1, bubble2, bubble3))
+        verify(launcherApps, never()).uncacheShortcuts(anyString(),
+                any(),
+                any(UserHandle::class.java), anyInt())
+    }
+
+    @Test
+    fun testSanitizeBubbles_noChanges() {
+        repository.addBubbles(user0.identifier, user0bubbles)
+        assertThat(repository.getEntities(user0.identifier).toList())
+                .isEqualTo(listOf(bubble1, bubble2, bubble3))
+        repository.addBubbles(user11.identifier, user11bubbles)
+        assertThat(repository.getEntities(user11.identifier).toList())
+                .isEqualTo(listOf(bubble11, bubble12))
+
+        val ret = repository.sanitizeBubbles(listOf(user0.identifier,
+                user10_managed.identifier,
+                user11.identifier))
+        assertThat(ret).isFalse() // bubbles were NOT removed
+
+        verify(launcherApps, never()).uncacheShortcuts(anyString(),
+                any(),
+                any(UserHandle::class.java), anyInt())
+    }
+
+    @Test
+    fun testSanitizeBubbles_userRemoved() {
+        repository.addBubbles(user0.identifier, user0bubbles)
+        assertThat(repository.getEntities(user0.identifier).toList())
+                .isEqualTo(listOf(bubble1, bubble2, bubble3))
+        repository.addBubbles(user11.identifier, user11bubbles)
+        assertThat(repository.getEntities(user11.identifier).toList())
+                .isEqualTo(listOf(bubble11, bubble12))
+
+        val ret = repository.sanitizeBubbles(listOf(user11.identifier))
+        assertThat(ret).isTrue() // bubbles were removed
+
+        assertThat(repository.getEntities(user0.identifier).toList()).isEmpty()
+        verify(launcherApps, never()).uncacheShortcuts(anyString(),
+                any(),
+                any(UserHandle::class.java), anyInt())
+
+        // User 11 bubbles should still be here
+        assertThat(repository.getEntities(user11.identifier).toList())
+                .isEqualTo(listOf(bubble11, bubble12))
+    }
+
+    @Test
+    fun testSanitizeBubbles_userParentRemoved() {
+        repository.addBubbles(user0.identifier, user0bubbles)
+        assertThat(repository.getEntities(user0.identifier).toList())
+                .isEqualTo(listOf(bubble1, bubble2, bubble3))
+
+        repository.addBubbles(user11.identifier, user11bubbles)
+        assertThat(repository.getEntities(user11.identifier).toList())
+                .isEqualTo(listOf(bubble11, bubble12))
+
+        val ret = repository.sanitizeBubbles(listOf(user0.identifier, user11.identifier))
+        assertThat(ret).isTrue() // bubbles were removed
+        // bubble2 is the work profile bubble and should be removed
+        assertThat(repository.getEntities(user0.identifier).toList())
+                .isEqualTo(listOf(bubble1, bubble3))
+        verify(launcherApps, never()).uncacheShortcuts(anyString(),
+                any(),
+                any(UserHandle::class.java), anyInt())
+
+        // User 11 bubbles should still be here
+        assertThat(repository.getEntities(user11.identifier).toList())
+                .isEqualTo(listOf(bubble11, bubble12))
+    }
+
+    @Test
+    fun testRemoveBubbleForUser_invalidInputDoesntCrash() {
+        repository.removeBubblesForUser(-1, 0)
+        repository.removeBubblesForUser(-1, -1)
+    }
 }
 
 private const val PKG_MESSENGER = "com.example.messenger"
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
index d8aebc2..96938eb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/TaskStackListenerImplTest.java
@@ -109,9 +109,10 @@
 
     @Test
     public void testOnTaskProfileLocked() {
-        mImpl.onTaskProfileLocked(1, 2);
-        verify(mCallback).onTaskProfileLocked(eq(1), eq(2));
-        verify(mOtherCallback).onTaskProfileLocked(eq(1), eq(2));
+        ActivityManager.RunningTaskInfo info = mock(ActivityManager.RunningTaskInfo.class);
+        mImpl.onTaskProfileLocked(info);
+        verify(mCallback).onTaskProfileLocked(eq(info));
+        verify(mOtherCallback).onTaskProfileLocked(eq(info));
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt
new file mode 100644
index 0000000..05e4722
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipBoundsControllerTest.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip.tv
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.os.Handler
+import android.os.test.TestLooper
+import android.testing.AndroidTestingRunner
+
+import com.android.wm.shell.R
+import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT
+import com.android.wm.shell.pip.tv.TvPipBoundsController.POSITION_DEBOUNCE_TIMEOUT_MILLIS
+import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement
+
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.AdditionalAnswers.returnsFirstArg
+import org.mockito.Mock
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+class TvPipBoundsControllerTest {
+    val ANIMATION_DURATION = 100
+    val STASH_DURATION = 5000
+    val FAR_FUTURE = 60 * 60000L
+    val ANCHOR_BOUNDS = Rect(90, 90, 100, 100)
+    val STASHED_BOUNDS = Rect(99, 90, 109, 100)
+    val MOVED_BOUNDS = Rect(90, 80, 100, 90)
+    val STASHED_MOVED_BOUNDS = Rect(99, 80, 109, 90)
+    val ANCHOR_PLACEMENT = Placement(ANCHOR_BOUNDS, ANCHOR_BOUNDS)
+    val STASHED_PLACEMENT = Placement(STASHED_BOUNDS, ANCHOR_BOUNDS,
+            STASH_TYPE_RIGHT, ANCHOR_BOUNDS, false)
+    val STASHED_PLACEMENT_RESTASH = Placement(STASHED_BOUNDS, ANCHOR_BOUNDS,
+            STASH_TYPE_RIGHT, ANCHOR_BOUNDS, true)
+    val MOVED_PLACEMENT = Placement(MOVED_BOUNDS, ANCHOR_BOUNDS)
+    val STASHED_MOVED_PLACEMENT = Placement(STASHED_MOVED_BOUNDS, ANCHOR_BOUNDS,
+            STASH_TYPE_RIGHT, MOVED_BOUNDS, false)
+    val STASHED_MOVED_PLACEMENT_RESTASH = Placement(STASHED_MOVED_BOUNDS, ANCHOR_BOUNDS,
+            STASH_TYPE_RIGHT, MOVED_BOUNDS, true)
+
+    lateinit var boundsController: TvPipBoundsController
+    var time = 0L
+    lateinit var testLooper: TestLooper
+    lateinit var mainHandler: Handler
+
+    var inMenu = false
+    var inMoveMode = false
+
+    @Mock
+    lateinit var context: Context
+    @Mock
+    lateinit var resources: Resources
+    @Mock
+    lateinit var tvPipBoundsState: TvPipBoundsState
+    @Mock
+    lateinit var tvPipBoundsAlgorithm: TvPipBoundsAlgorithm
+    @Mock
+    lateinit var listener: TvPipBoundsController.PipBoundsListener
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        time = 0L
+        inMenu = false
+        inMoveMode = false
+
+        testLooper = TestLooper { time }
+        mainHandler = Handler(testLooper.getLooper())
+
+        whenever(context.resources).thenReturn(resources)
+        whenever(resources.getInteger(R.integer.config_pipStashDuration)).thenReturn(STASH_DURATION)
+        whenever(tvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(any()))
+                .then(returnsFirstArg<Rect>())
+
+        boundsController = TvPipBoundsController(
+                context,
+                { time },
+                mainHandler,
+                tvPipBoundsState,
+                tvPipBoundsAlgorithm)
+        boundsController.setListener(listener)
+    }
+
+    @Test
+    fun testPlacement_MovedAfterDebounceTimeout() {
+        triggerPlacement(MOVED_PLACEMENT)
+        assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS)
+        assertNoMovementUpTo(time + FAR_FUTURE)
+    }
+
+    @Test
+    fun testStashedPlacement_MovedAfterDebounceTimeout_Unstashes() {
+        triggerPlacement(STASHED_PLACEMENT_RESTASH)
+        assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
+        assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS)
+    }
+
+    @Test
+    fun testDebounceSamePlacement_MovesDebounceTimeoutAfterFirstPlacement() {
+        triggerPlacement(MOVED_PLACEMENT)
+        advanceTimeTo(POSITION_DEBOUNCE_TIMEOUT_MILLIS / 2)
+        triggerPlacement(MOVED_PLACEMENT)
+
+        assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS)
+    }
+
+    @Test
+    fun testNoMovementUntilPlacementStabilizes() {
+        triggerPlacement(ANCHOR_PLACEMENT)
+        advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10)
+        triggerPlacement(MOVED_PLACEMENT)
+        advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10)
+        triggerPlacement(ANCHOR_PLACEMENT)
+        advanceTimeTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS / 10)
+        triggerPlacement(MOVED_PLACEMENT)
+
+        assertMovementAt(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS, MOVED_BOUNDS)
+    }
+
+    @Test
+    fun testUnstashIfStashNoLongerNecessary() {
+        triggerPlacement(STASHED_PLACEMENT_RESTASH)
+        assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
+
+        triggerPlacement(ANCHOR_PLACEMENT)
+        assertMovementAt(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS, ANCHOR_BOUNDS)
+    }
+
+    @Test
+    fun testRestashingPlacementDelaysUnstash() {
+        triggerPlacement(STASHED_PLACEMENT_RESTASH)
+        assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
+
+        assertNoMovementUpTo(time + STASH_DURATION / 2)
+        triggerPlacement(STASHED_PLACEMENT_RESTASH)
+        assertNoMovementUpTo(time + POSITION_DEBOUNCE_TIMEOUT_MILLIS)
+        assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS)
+    }
+
+    @Test
+    fun testNonRestashingPlacementDoesNotDelayUnstash() {
+        triggerPlacement(STASHED_PLACEMENT_RESTASH)
+        assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS, STASHED_BOUNDS)
+
+        assertNoMovementUpTo(time + STASH_DURATION / 2)
+        triggerPlacement(STASHED_PLACEMENT)
+        assertMovementAt(POSITION_DEBOUNCE_TIMEOUT_MILLIS + STASH_DURATION, ANCHOR_BOUNDS)
+    }
+
+    @Test
+    fun testImmediatePlacement() {
+        triggerImmediatePlacement(STASHED_PLACEMENT_RESTASH)
+        assertMovement(STASHED_BOUNDS)
+        assertMovementAt(time + STASH_DURATION, ANCHOR_BOUNDS)
+    }
+
+    @Test
+    fun testInMoveMode_KeepAtAnchor() {
+        startMoveMode()
+        triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH)
+        assertMovement(ANCHOR_BOUNDS)
+        assertNoMovementUpTo(time + FAR_FUTURE)
+    }
+
+    @Test
+    fun testInMenu_Unstashed() {
+        openPipMenu()
+        triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH)
+        assertMovement(MOVED_BOUNDS)
+        assertNoMovementUpTo(time + FAR_FUTURE)
+    }
+
+    @Test
+    fun testCloseMenu_DoNotRestash() {
+        openPipMenu()
+        triggerImmediatePlacement(STASHED_MOVED_PLACEMENT_RESTASH)
+        assertMovement(MOVED_BOUNDS)
+
+        closePipMenu()
+        triggerPlacement(STASHED_MOVED_PLACEMENT)
+        assertNoMovementUpTo(time + FAR_FUTURE)
+    }
+
+    fun assertMovement(bounds: Rect) {
+        verify(listener).onPipTargetBoundsChange(eq(bounds), anyInt())
+        reset(listener)
+    }
+
+    fun assertMovementAt(timeMs: Long, bounds: Rect) {
+        assertNoMovementUpTo(timeMs - 1)
+        advanceTimeTo(timeMs)
+        assertMovement(bounds)
+    }
+
+    fun assertNoMovementUpTo(timeMs: Long) {
+        advanceTimeTo(timeMs)
+        verify(listener, never()).onPipTargetBoundsChange(any(), anyInt())
+    }
+
+    fun triggerPlacement(placement: Placement, immediate: Boolean = false) {
+        whenever(tvPipBoundsAlgorithm.getTvPipPlacement()).thenReturn(placement)
+        val stayAtAnchorPosition = inMoveMode
+        val disallowStashing = inMenu || stayAtAnchorPosition
+        boundsController.recalculatePipBounds(stayAtAnchorPosition, disallowStashing,
+                ANIMATION_DURATION, immediate)
+    }
+
+    fun triggerImmediatePlacement(placement: Placement) {
+        triggerPlacement(placement, true)
+    }
+
+    fun openPipMenu() {
+        inMenu = true
+        inMoveMode = false
+    }
+
+    fun closePipMenu() {
+        inMenu = false
+        inMoveMode = false
+    }
+
+    fun startMoveMode() {
+        inMenu = true
+        inMoveMode = true
+    }
+
+    fun advanceTimeTo(ms: Long) {
+        time = ms
+        testLooper.dispatchAll()
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt
index 46f388d..0fcc5cf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt
@@ -30,7 +30,9 @@
 import org.junit.Before
 import org.junit.Test
 import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertNull
+import junit.framework.Assert.assertTrue
 
 @RunWith(AndroidTestingRunner::class)
 class TvPipKeepClearAlgorithmTest {
@@ -46,7 +48,6 @@
     private lateinit var pipSize: Size
     private lateinit var movementBounds: Rect
     private lateinit var algorithm: TvPipKeepClearAlgorithm
-    private var currentTime = 0L
     private var restrictedAreas = mutableSetOf<Rect>()
     private var unrestrictedAreas = mutableSetOf<Rect>()
     private var gravity: Int = 0
@@ -58,16 +59,14 @@
 
         restrictedAreas.clear()
         unrestrictedAreas.clear()
-        currentTime = 0L
         pipSize = DEFAULT_PIP_SIZE
         gravity = Gravity.BOTTOM or Gravity.RIGHT
 
-        algorithm = TvPipKeepClearAlgorithm({ currentTime })
+        algorithm = TvPipKeepClearAlgorithm()
         algorithm.setScreenSize(SCREEN_SIZE)
         algorithm.setMovementBounds(movementBounds)
         algorithm.pipAreaPadding = PADDING
         algorithm.stashOffset = STASH_OFFSET
-        algorithm.stashDuration = 5000L
         algorithm.setGravity(gravity)
         algorithm.maxRestrictedDistanceFraction = 0.3
     }
@@ -265,7 +264,7 @@
         assertEquals(expectedBounds, placement.bounds)
         assertEquals(STASH_TYPE_BOTTOM, placement.stashType)
         assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds)
-        assertEquals(algorithm.stashDuration, placement.unstashTime)
+        assertTrue(placement.triggerStash)
     }
 
     @Test
@@ -305,7 +304,7 @@
         assertEquals(expectedBounds, placement.bounds)
         assertEquals(STASH_TYPE_RIGHT, placement.stashType)
         assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
-        assertEquals(algorithm.stashDuration, placement.unstashTime)
+        assertTrue(placement.triggerStash)
     }
 
     @Test
@@ -352,9 +351,7 @@
         assertEquals(expectedBounds, placement.bounds)
         assertEquals(STASH_TYPE_RIGHT, placement.stashType)
         assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
-        assertEquals(algorithm.stashDuration, placement.unstashTime)
-
-        currentTime += 1000
+        assertTrue(placement.triggerStash)
 
         restrictedAreas.remove(sideBar)
         placement = getActualPlacement()
@@ -363,7 +360,7 @@
     }
 
     @Test
-    fun test_Stashed_UnstashBoundsStaysObstructed_UnstashesAfterTimeout() {
+    fun test_Stashed_UnstashBoundsStaysObstructed_DoesNotTriggerStash() {
         gravity = Gravity.BOTTOM or Gravity.RIGHT
 
         val bottomBar = makeBottomBar(BOTTOM_SHEET_HEIGHT)
@@ -384,13 +381,13 @@
         assertEquals(expectedBounds, placement.bounds)
         assertEquals(STASH_TYPE_RIGHT, placement.stashType)
         assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
-        assertEquals(algorithm.stashDuration, placement.unstashTime)
-
-        currentTime += algorithm.stashDuration
+        assertTrue(placement.triggerStash)
 
         placement = getActualPlacement()
-        assertEquals(expectedUnstashBounds, placement.bounds)
-        assertNotStashed(placement)
+        assertEquals(expectedBounds, placement.bounds)
+        assertEquals(STASH_TYPE_RIGHT, placement.stashType)
+        assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
+        assertFalse(placement.triggerStash)
     }
 
     @Test
@@ -415,9 +412,7 @@
         assertEquals(expectedBounds, placement.bounds)
         assertEquals(STASH_TYPE_RIGHT, placement.stashType)
         assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
-        assertEquals(algorithm.stashDuration, placement.unstashTime)
-
-        currentTime += 1000
+        assertTrue(placement.triggerStash)
 
         val newObstruction = Rect(
                 0,
@@ -431,7 +426,7 @@
         assertEquals(expectedBounds, placement.bounds)
         assertEquals(STASH_TYPE_RIGHT, placement.stashType)
         assertEquals(expectedUnstashBounds, placement.unstashDestinationBounds)
-        assertEquals(currentTime + algorithm.stashDuration, placement.unstashTime)
+        assertTrue(placement.triggerStash)
     }
 
     @Test
@@ -458,21 +453,9 @@
 
     @Test
     fun test_PipInsets() {
-        val permInsets = Insets.of(-1, -2, -3, -4)
-        algorithm.setPipPermanentDecorInsets(permInsets)
-        testInsetsForAllPositions(permInsets)
+        val insets = Insets.of(-1, -2, -3, -4)
+        algorithm.setPipPermanentDecorInsets(insets)
 
-        val tempInsets = Insets.of(-4, -3, -2, -1)
-        algorithm.setPipPermanentDecorInsets(Insets.NONE)
-        algorithm.setPipTemporaryDecorInsets(tempInsets)
-        testInsetsForAllPositions(tempInsets)
-
-        algorithm.setPipPermanentDecorInsets(permInsets)
-        algorithm.setPipTemporaryDecorInsets(tempInsets)
-        testInsetsForAllPositions(Insets.add(permInsets, tempInsets))
-    }
-
-    private fun testInsetsForAllPositions(insets: Insets) {
         gravity = Gravity.BOTTOM or Gravity.RIGHT
         testAnchorPositionWithInsets(insets)
 
@@ -546,6 +529,6 @@
     private fun assertNotStashed(actual: Placement) {
         assertEquals(STASH_TYPE_NONE, actual.stashType)
         assertNull(actual.unstashDestinationBounds)
-        assertEquals(0L, actual.unstashTime)
+        assertFalse(actual.triggerStash)
     }
 }
diff --git a/libs/hwui/AnimatorManager.cpp b/libs/hwui/AnimatorManager.cpp
index 4826d5a..0780414 100644
--- a/libs/hwui/AnimatorManager.cpp
+++ b/libs/hwui/AnimatorManager.cpp
@@ -31,7 +31,8 @@
     animator->detach();
 }
 
-AnimatorManager::AnimatorManager(RenderNode& parent) : mParent(parent), mAnimationHandle(nullptr) {}
+AnimatorManager::AnimatorManager(RenderNode& parent)
+        : mParent(parent), mAnimationHandle(nullptr), mCancelAllAnimators(false) {}
 
 AnimatorManager::~AnimatorManager() {
     for_each(mNewAnimators.begin(), mNewAnimators.end(), detach);
@@ -82,8 +83,16 @@
         }
         mNewAnimators.clear();
     }
-    for (auto& animator : mAnimators) {
-        animator->pushStaging(mAnimationHandle->context());
+
+    if (mCancelAllAnimators) {
+        for (auto& animator : mAnimators) {
+            animator->forceEndNow(mAnimationHandle->context());
+        }
+        mCancelAllAnimators = false;
+    } else {
+        for (auto& animator : mAnimators) {
+            animator->pushStaging(mAnimationHandle->context());
+        }
     }
 }
 
@@ -184,5 +193,9 @@
     mAnimationHandle->release();
 }
 
+void AnimatorManager::forceEndAnimators() {
+    mCancelAllAnimators = true;
+}
+
 } /* namespace uirenderer */
 } /* namespace android */
diff --git a/libs/hwui/AnimatorManager.h b/libs/hwui/AnimatorManager.h
index a0df01d..6002661d 100644
--- a/libs/hwui/AnimatorManager.h
+++ b/libs/hwui/AnimatorManager.h
@@ -16,11 +16,11 @@
 #ifndef ANIMATORMANAGER_H
 #define ANIMATORMANAGER_H
 
-#include <vector>
-
 #include <cutils/compiler.h>
 #include <utils/StrongPointer.h>
 
+#include <vector>
+
 #include "utils/Macros.h"
 
 namespace android {
@@ -56,6 +56,8 @@
     // Hard-ends all animators. May only be called on the UI thread.
     void endAllStagingAnimators();
 
+    void forceEndAnimators();
+
     // Hard-ends all animators that have been pushed. Used for cleanup if
     // the ActivityContext is being destroyed
     void endAllActiveAnimators();
@@ -71,6 +73,8 @@
     // To improve the efficiency of resizing & removing from the vector
     std::vector<sp<BaseRenderNodeAnimator> > mNewAnimators;
     std::vector<sp<BaseRenderNodeAnimator> > mAnimators;
+
+    bool mCancelAllAnimators;
 };
 
 } /* namespace uirenderer */
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 86ae399..5a67eb9 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -134,7 +134,7 @@
     skpCaptureEnabled = debuggingEnabled && base::GetBoolProperty(PROPERTY_CAPTURE_SKP_ENABLED, false);
 
     SkAndroidFrameworkTraceUtil::setEnableTracing(
-            base::GetBoolProperty(PROPERTY_SKIA_ATRACE_ENABLED, true));
+            base::GetBoolProperty(PROPERTY_SKIA_ATRACE_ENABLED, false));
 
     runningInEmulator = base::GetBoolProperty(PROPERTY_IS_EMULATOR, false);
 
diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp
index 944393c..db76390 100644
--- a/libs/hwui/jni/android_graphics_RenderNode.cpp
+++ b/libs/hwui/jni/android_graphics_RenderNode.cpp
@@ -543,6 +543,12 @@
     renderNode->animators().endAllStagingAnimators();
 }
 
+static void android_view_RenderNode_forceEndAnimators(JNIEnv* env, jobject clazz,
+                                                      jlong renderNodePtr) {
+    RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
+    renderNode->animators().forceEndAnimators();
+}
+
 // ----------------------------------------------------------------------------
 // SurfaceView position callback
 // ----------------------------------------------------------------------------
@@ -745,6 +751,7 @@
         {"nGetAllocatedSize", "(J)I", (void*)android_view_RenderNode_getAllocatedSize},
         {"nAddAnimator", "(JJ)V", (void*)android_view_RenderNode_addAnimator},
         {"nEndAllAnimators", "(J)V", (void*)android_view_RenderNode_endAllAnimators},
+        {"nForceEndAnimators", "(J)V", (void*)android_view_RenderNode_forceEndAnimators},
         {"nRequestPositionUpdates", "(JLjava/lang/ref/WeakReference;)V",
          (void*)android_view_RenderNode_requestPositionUpdates},
 
diff --git a/libs/input/TEST_MAPPING b/libs/input/TEST_MAPPING
index fe74c62..9626d8d 100644
--- a/libs/input/TEST_MAPPING
+++ b/libs/input/TEST_MAPPING
@@ -1,7 +1,7 @@
 {
-    "presubmit": [
-        {
-            "name": "libinputservice_test"
-        }
-    ]
+  "imports": [
+    {
+      "path": "frameworks/native/services/inputflinger"
+    }
+  ]
 }
diff --git a/media/java/android/media/AudioDeviceVolumeManager.java b/media/java/android/media/AudioDeviceVolumeManager.java
index 11cacd0..fe58cca 100644
--- a/media/java/android/media/AudioDeviceVolumeManager.java
+++ b/media/java/android/media/AudioDeviceVolumeManager.java
@@ -235,13 +235,7 @@
                     mDeviceVolumeDispatcherStub = new DeviceVolumeDispatcherStub();
                 }
             } else {
-                for (ListenerInfo info : mDeviceVolumeListeners) {
-                    if (info.mListener == vclistener) {
-                        throw new IllegalArgumentException(
-                                "attempt to call setDeviceAbsoluteMultiVolumeBehavior() "
-                                        + "on a previously registered listener");
-                    }
-                }
+                mDeviceVolumeListeners.removeIf(info -> info.mDevice.equalTypeAddress(device));
             }
             mDeviceVolumeListeners.add(listenerInfo);
             mDeviceVolumeDispatcherStub.register(true, device, volumes, handlesVolumeAdjustment);
@@ -304,6 +298,28 @@
                 "removeOnDeviceVolumeBehaviorChangedListener");
     }
 
+    /**
+     * Return human-readable name for volume behavior
+     * @param behavior one of the volume behaviors defined in AudioManager
+     * @return a string for the given behavior
+     */
+    public static String volumeBehaviorName(@AudioManager.DeviceVolumeBehavior int behavior) {
+        switch (behavior) {
+            case AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE:
+                return "DEVICE_VOLUME_BEHAVIOR_VARIABLE";
+            case AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL:
+                return "DEVICE_VOLUME_BEHAVIOR_FULL";
+            case AudioManager.DEVICE_VOLUME_BEHAVIOR_FIXED:
+                return "DEVICE_VOLUME_BEHAVIOR_FIXED";
+            case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE:
+                return "DEVICE_VOLUME_BEHAVIOR_ABSOLUTE";
+            case AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE:
+                return "DEVICE_VOLUME_BEHAVIOR_ABSOLUTE_MULTI_MODE";
+            default:
+                return "invalid volume behavior " + behavior;
+        }
+    }
+
     private final class DeviceVolumeBehaviorDispatcherStub
             extends IDeviceVolumeBehaviorDispatcher.Stub implements CallbackUtil.DispatcherStub {
         public void register(boolean register) {
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 55c558f..85cd342 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -2910,6 +2910,10 @@
      * For portability, an application should prime the data path to the maximum allowed
      * by writing data until the write() method returns a short transfer count.
      * This allows play() to start immediately, and reduces the chance of underrun.
+     *<p>
+     * As of {@link android.os.Build.VERSION_CODES#S} the minimum level to start playing
+     * can be obtained using {@link #getStartThresholdInFrames()} and set with
+     * {@link #setStartThresholdInFrames(int)}.
      *
      * @throws IllegalStateException if the track isn't properly initialized
      */
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl
index fed86dc..2e792b3 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManagerCallback.aidl
@@ -22,10 +22,10 @@
  * Interface to receive callbacks from ITvInteractiveAppManager regardless of sessions.
  * @hide
  */
-interface ITvInteractiveAppManagerCallback {
+oneway interface ITvInteractiveAppManagerCallback {
     void onInteractiveAppServiceAdded(in String iAppServiceId);
     void onInteractiveAppServiceRemoved(in String iAppServiceId);
     void onInteractiveAppServiceUpdated(in String iAppServiceId);
     void onTvInteractiveAppServiceInfoUpdated(in TvInteractiveAppServiceInfo tvIAppInfo);
     void onStateChanged(in String iAppServiceId, int type, int state, int err);
-}
\ No newline at end of file
+}
diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp
index c8d2d1e..5850a81 100644
--- a/media/jni/android_media_MediaCodec.cpp
+++ b/media/jni/android_media_MediaCodec.cpp
@@ -1294,45 +1294,46 @@
     std::string defaultMsg = "Unknown Error";
 
     /* translate OS errors to Java API CryptoException errorCodes (which are positive) */
+    jint jerr = 0;
     switch (err) {
         case ERROR_DRM_NO_LICENSE:
-            err = gCryptoErrorCodes.cryptoErrorNoKey;
+            jerr = gCryptoErrorCodes.cryptoErrorNoKey;
             defaultMsg = "Crypto key not available";
             break;
         case ERROR_DRM_LICENSE_EXPIRED:
-            err = gCryptoErrorCodes.cryptoErrorKeyExpired;
+            jerr = gCryptoErrorCodes.cryptoErrorKeyExpired;
             defaultMsg = "License expired";
             break;
         case ERROR_DRM_RESOURCE_BUSY:
-            err = gCryptoErrorCodes.cryptoErrorResourceBusy;
+            jerr = gCryptoErrorCodes.cryptoErrorResourceBusy;
             defaultMsg = "Resource busy or unavailable";
             break;
         case ERROR_DRM_INSUFFICIENT_OUTPUT_PROTECTION:
-            err = gCryptoErrorCodes.cryptoErrorInsufficientOutputProtection;
+            jerr = gCryptoErrorCodes.cryptoErrorInsufficientOutputProtection;
             defaultMsg = "Required output protections are not active";
             break;
         case ERROR_DRM_SESSION_NOT_OPENED:
-            err = gCryptoErrorCodes.cryptoErrorSessionNotOpened;
+            jerr = gCryptoErrorCodes.cryptoErrorSessionNotOpened;
             defaultMsg = "Attempted to use a closed session";
             break;
         case ERROR_DRM_INSUFFICIENT_SECURITY:
-            err = gCryptoErrorCodes.cryptoErrorInsufficientSecurity;
+            jerr = gCryptoErrorCodes.cryptoErrorInsufficientSecurity;
             defaultMsg = "Required security level is not met";
             break;
         case ERROR_DRM_CANNOT_HANDLE:
-            err = gCryptoErrorCodes.cryptoErrorUnsupportedOperation;
+            jerr = gCryptoErrorCodes.cryptoErrorUnsupportedOperation;
             defaultMsg = "Operation not supported in this configuration";
             break;
         case ERROR_DRM_FRAME_TOO_LARGE:
-            err = gCryptoErrorCodes.cryptoErrorFrameTooLarge;
+            jerr = gCryptoErrorCodes.cryptoErrorFrameTooLarge;
             defaultMsg = "Decrytped frame exceeds size of output buffer";
             break;
         case ERROR_DRM_SESSION_LOST_STATE:
-            err = gCryptoErrorCodes.cryptoErrorLostState;
+            jerr = gCryptoErrorCodes.cryptoErrorLostState;
             defaultMsg = "Session state was lost, open a new session and retry";
             break;
         default:  /* Other negative DRM error codes go out best-effort. */
-            err = MediaErrorToJavaError(err);
+            jerr = MediaErrorToJavaError(err);
             defaultMsg = StrCryptoError(err);
             break;
     }
@@ -1344,7 +1345,7 @@
     jstring msgObj = env->NewStringUTF(msgStr.c_str());
 
     jthrowable exception =
-        (jthrowable)env->NewObject(clazz.get(), constructID, err, msgObj);
+        (jthrowable)env->NewObject(clazz.get(), constructID, jerr, msgObj);
 
     env->Throw(exception);
 }
diff --git a/media/jni/android_media_Utils.cpp b/media/jni/android_media_Utils.cpp
index f4a39b3..b7ad6dc 100644
--- a/media/jni/android_media_Utils.cpp
+++ b/media/jni/android_media_Utils.cpp
@@ -18,10 +18,10 @@
 #define LOG_TAG "AndroidMediaUtils"
 
 #include <aidl/android/hardware/graphics/common/PlaneLayoutComponentType.h>
-#include <hardware/camera3.h>
 #include <ui/GraphicBufferMapper.h>
 #include <ui/GraphicTypes.h>
 #include <utils/Log.h>
+
 #include "android_media_Utils.h"
 
 #define ALIGN(x, mask) ( ((x) + (mask) - 1) & ~((mask) - 1) )
@@ -122,8 +122,8 @@
     }
 
     // First check for BLOB transport header at the end of the buffer
-    uint8_t* header = blobBuffer + (width - sizeof(struct camera3_jpeg_blob));
-    struct camera3_jpeg_blob *blob = (struct camera3_jpeg_blob*)(header);
+    uint8_t* header = blobBuffer + (width - sizeof(struct camera3_jpeg_blob_v2));
+    struct camera3_jpeg_blob_v2 *blob = (struct camera3_jpeg_blob_v2*)(header);
     if (blob->jpeg_blob_id == CAMERA3_JPEG_BLOB_ID ||
             blob->jpeg_blob_id == CAMERA3_HEIC_BLOB_ID) {
         size = blob->jpeg_size;
diff --git a/media/jni/android_media_Utils.h b/media/jni/android_media_Utils.h
index 4feb4f51..c12cec1 100644
--- a/media/jni/android_media_Utils.h
+++ b/media/jni/android_media_Utils.h
@@ -50,6 +50,20 @@
 
 int getBufferHeight(BufferItem *buffer);
 
+// Must be in sync with AIDL CameraBlob : android.hardware.camera.device.CameraBlob
+// HALs must NOT copy this definition.
+// for details: http://b/229688810
+typedef struct camera3_jpeg_blob_v2 {
+  uint32_t jpeg_blob_id;
+  uint32_t jpeg_size;
+} camera3_jpeg_blobv2_t;
+
+// Must be in sync with AIDL CameraBlob : android.hardware.camera.device.CameraBlobId
+enum {
+      CAMERA3_JPEG_BLOB_ID = 0x00FF,
+      CAMERA3_JPEG_APP_SEGMENTS_BLOB_ID = 0x0100,
+};
+
 };  // namespace android
 
 #endif //  _ANDROID_MEDIA_UTILS_H_
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index 3d6bf15..a389bfc 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -20,7 +20,7 @@
     <string name="app_label">Companion Device Manager</string>
 
     <!-- Title of the device association confirmation dialog. -->
-    <string name="confirmation_title">Allow &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to manage your &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;</string>
+    <string name="confirmation_title">Allow &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%1$s</xliff:g>&lt;/strong&gt; to access your &lt;strong&gt;<xliff:g id="device_name" example="ASUS ZenWatch 2">%2$s</xliff:g>&lt;/strong&gt;</string>
 
     <!-- ================= DEVICE_PROFILE_WATCH and null profile ================= -->
 
@@ -31,10 +31,7 @@
     <string name="chooser_title">Choose a <xliff:g id="profile_name" example="watch">%1$s</xliff:g> to be managed by &lt;strong&gt;<xliff:g id="app_name" example="Android Wear">%2$s</xliff:g>&lt;/strong&gt;</string>
 
     <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
-    <string name="summary_watch" product="default"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string>
-
-    <!-- Description of the privileges the application will get if associated with the companion device of WATCH profile (type) [CHAR LIMIT=NONE] -->
-    <string name="summary_watch" product="tablet"><xliff:g id="app_name" example="Wear">%1$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts and Calendar permissions.</string>
+    <string name="summary_watch">This app is needed to manage your <xliff:g id="device_name" example="My Watch">%1$s</xliff:g>. <xliff:g id="app_name" example="Android Wear">%2$s</xliff:g> will be allowed to interact with your notifications and access your Phone, SMS, Contacts, Calendar, Call logs and Nearby devices permissions.</string>
 
     <!-- ================= DEVICE_PROFILE_APP_STREAMING ================= -->
 
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index 9e9ec04..a3aa010 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -497,17 +497,19 @@
         }
 
         final String deviceName = mSelectedDevice.getDisplayName();
-        final Spanned title = getHtmlFromResources(
-                this, R.string.confirmation_title, appLabel, deviceName);
+        final String profileName = getString(R.string.profile_name_watch);
+        final Spanned title;
         final Spanned summary;
         final Drawable profileIcon;
 
         if (deviceProfile == null) {
+            title = getHtmlFromResources(this, R.string.confirmation_title, appLabel, deviceName);
             summary = getHtmlFromResources(this, R.string.summary_generic);
             profileIcon = getIcon(this, R.drawable.ic_device_other);
             mSummary.setVisibility(View.GONE);
         } else if (deviceProfile.equals(DEVICE_PROFILE_WATCH)) {
-            summary = getHtmlFromResources(this, R.string.summary_watch, appLabel, deviceName);
+            title = getHtmlFromResources(this, R.string.confirmation_title, appLabel, profileName);
+            summary = getHtmlFromResources(this, R.string.summary_watch, deviceName, appLabel);
             profileIcon = getIcon(this, R.drawable.ic_watch);
         } else {
             throw new RuntimeException("Unsupported profile " + deviceProfile);
@@ -535,7 +537,7 @@
             mSummary.setVisibility(View.GONE);
         } else if (deviceProfile.equals(DEVICE_PROFILE_WATCH)) {
             profileName = getString(R.string.profile_name_watch);
-            summary = getHtmlFromResources(this, R.string.summary_watch, appLabel);
+            summary = getHtmlFromResources(this, R.string.summary_watch, profileName, appLabel);
             profileIcon = getIcon(this, R.drawable.ic_watch);
         } else {
             throw new RuntimeException("Unsupported profile " + deviceProfile);
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
index f333b86..fc5ff08 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceDiscoveryService.java
@@ -123,6 +123,7 @@
         intent.setAction(ACTION_START_DISCOVERY);
         intent.putExtra(EXTRA_ASSOCIATION_REQUEST, associationRequest);
         sStateLiveData.setValue(DiscoveryState.STARTING);
+        sScanResultsLiveData.setValue(Collections.emptyList());
 
         context.startService(intent);
     }
@@ -173,7 +174,6 @@
 
     @Override
     public void onDestroy() {
-        sScanResultsLiveData.setValue(Collections.emptyList());
         super.onDestroy();
         if (DEBUG) Log.d(TAG, "onDestroy()");
     }
@@ -187,7 +187,6 @@
         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/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java
index faca1ae..1f59d30 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/DeviceFilterPair.java
@@ -47,7 +47,7 @@
     }
 
     String getDisplayName() {
-        if (mFilter != null) mFilter.getDeviceDisplayName(mDevice);
+        if (mFilter != null) return mFilter.getDeviceDisplayName(mDevice);
 
         if (mDevice instanceof BluetoothDevice) {
             return getDeviceDisplayNameInternal((BluetoothDevice) mDevice);
diff --git a/packages/CtsShim/apk/arm/CtsShim.apk b/packages/CtsShim/apk/arm/CtsShim.apk
index 0c3c4bb..fb09286 100644
--- a/packages/CtsShim/apk/arm/CtsShim.apk
+++ b/packages/CtsShim/apk/arm/CtsShim.apk
Binary files differ
diff --git a/packages/CtsShim/apk/arm/CtsShimPriv.apk b/packages/CtsShim/apk/arm/CtsShimPriv.apk
index ee42d08..07915ce 100644
--- a/packages/CtsShim/apk/arm/CtsShimPriv.apk
+++ b/packages/CtsShim/apk/arm/CtsShimPriv.apk
Binary files differ
diff --git a/packages/CtsShim/apk/x86/CtsShim.apk b/packages/CtsShim/apk/x86/CtsShim.apk
index 0c3c4bb..fb09286 100644
--- a/packages/CtsShim/apk/x86/CtsShim.apk
+++ b/packages/CtsShim/apk/x86/CtsShim.apk
Binary files differ
diff --git a/packages/CtsShim/apk/x86/CtsShimPriv.apk b/packages/CtsShim/apk/x86/CtsShimPriv.apk
index 2bb3750..20e94b6 100644
--- a/packages/CtsShim/apk/x86/CtsShimPriv.apk
+++ b/packages/CtsShim/apk/x86/CtsShimPriv.apk
Binary files differ
diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp
index 7cf5385..af3e210 100644
--- a/packages/CtsShim/build/Android.bp
+++ b/packages/CtsShim/build/Android.bp
@@ -47,6 +47,7 @@
     uses_libs: ["android.test.runner"],
 
     apex_available: [
+        "//apex_available:platform",
         "com.android.apex.cts.shim.v2_apk_in_apex_upgrades",
     ],
 }
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp
index 4106ac5..4757513 100644
--- a/packages/PackageInstaller/Android.bp
+++ b/packages/PackageInstaller/Android.bp
@@ -47,3 +47,39 @@
         "androidx.leanback_leanback",
     ],
 }
+
+android_app {
+    name: "PackageInstaller_tablet",
+    defaults: ["platform_app_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    certificate: "platform",
+    privileged: true,
+    platform_apis: true,
+    rename_resources_package: false,
+
+    static_libs: [
+        "xz-java",
+        "androidx.leanback_leanback",
+    ],
+    aaptflags: ["--product tablet"],
+}
+
+android_app {
+    name: "PackageInstaller_tv",
+    defaults: ["platform_app_defaults"],
+
+    srcs: ["src/**/*.java"],
+
+    certificate: "platform",
+    privileged: true,
+    platform_apis: true,
+    rename_resources_package: false,
+
+    static_libs: [
+        "xz-java",
+        "androidx.leanback_leanback",
+    ],
+    aaptflags: ["--product tv"],
+}
diff --git a/packages/SettingsLib/AndroidManifest.xml b/packages/SettingsLib/AndroidManifest.xml
index 3e67abf..13f8a37 100644
--- a/packages/SettingsLib/AndroidManifest.xml
+++ b/packages/SettingsLib/AndroidManifest.xml
@@ -22,16 +22,6 @@
         <activity
             android:name="com.android.settingslib.users.AvatarPickerActivity"
             android:theme="@style/SudThemeGlifV2.DayNight"/>
-
-        <activity
-            android:name="com.android.settingslib.qrcode.QrCodeScanModeActivity"
-            android:exported="true">
-            <intent-filter>
-                <action android:name="android.settings.BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER"/>
-                <category android:name="android.intent.category.DEFAULT"/>
-            </intent-filter>
-        </activity>
-
     </application>
 
 </manifest>
diff --git a/packages/SettingsLib/AppPreference/res/layout/preference_app_header.xml b/packages/SettingsLib/AppPreference/res/layout/preference_app_header.xml
new file mode 100644
index 0000000..e0d01f9
--- /dev/null
+++ b/packages/SettingsLib/AppPreference/res/layout/preference_app_header.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingBottom="16dp"
+    android:paddingTop="8dp"
+    android:clickable="false">
+
+    <TextView
+        android:id="@+id/apps_top_intro_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textDirection="locale"
+        android:clickable="false"
+        android:longClickable="false"
+        android:textAppearance="@style/TextAppearance.TopIntroText" />
+
+</LinearLayout>
diff --git a/packages/SettingsLib/SettingsTheme/res/values/styles.xml b/packages/SettingsLib/SettingsTheme/res/values/styles.xml
index aaab0f0..00bd141 100644
--- a/packages/SettingsLib/SettingsTheme/res/values/styles.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values/styles.xml
@@ -26,4 +26,11 @@
     <style name="TextAppearance.CategoryTitle.SettingsLib"
            parent="@android:style/TextAppearance.DeviceDefault.Medium">
     </style>
+
+    <style name="TextAppearance.TopIntroText"
+        parent="@android:style/TextAppearance.DeviceDefault">
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">?android:attr/textColorSecondary</item>
+    </style>
+
 </resources>
diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp
index cd0bdea..ecf2a72 100644
--- a/packages/SettingsLib/TopIntroPreference/Android.bp
+++ b/packages/SettingsLib/TopIntroPreference/Android.bp
@@ -16,6 +16,7 @@
     static_libs: [
         "androidx.annotation_annotation",
         "androidx.preference_preference",
+        "SettingsLibSettingsTheme",
     ],
     sdk_version: "system_current",
     min_sdk_version: "21",
diff --git a/packages/SettingsLib/TopIntroPreference/res/values/styles.xml b/packages/SettingsLib/TopIntroPreference/res/values/styles.xml
deleted file mode 100644
index b6ca41f..0000000
--- a/packages/SettingsLib/TopIntroPreference/res/values/styles.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  Copyright (C) 2021 The Android Open Source Project
-
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  -->
-<resources>
-    <style name="TextAppearance.TopIntroText"
-           parent="@android:style/TextAppearance.DeviceDefault">
-        <item name="android:textSize">14sp</item>
-        <item name="android:textColor">?android:attr/textColorSecondary</item>
-    </style>
-</resources>
diff --git a/packages/SettingsLib/res/drawable/ic_qr_code_scanner.xml b/packages/SettingsLib/res/drawable/ic_qr_code_scanner.xml
deleted file mode 100644
index f6f63c5..0000000
--- a/packages/SettingsLib/res/drawable/ic_qr_code_scanner.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-    Copyright (C) 2022 The Android Open Source Project
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
--->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
-        android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"
-        android:tint="?attr/colorControlNormal">
-    <path android:fillColor="@android:color/white"
-          android:pathData="M2,7V2H7V4H4V7ZM2,22V17H4V20H7V22ZM17,22V20H20V17H22V22ZM20,7V4H17V2H22V7ZM17.5,17.5H19V19H17.5ZM17.5,14.5H19V16H17.5ZM16,16H17.5V17.5H16ZM14.5,17.5H16V19H14.5ZM13,16H14.5V17.5H13ZM16,13H17.5V14.5H16ZM14.5,14.5H16V16H14.5ZM13,13H14.5V14.5H13ZM19,5V11H13V5ZM11,13V19H5V13ZM11,5V11H5V5ZM9.5,17.5V14.5H6.5V17.5ZM9.5,9.5V6.5H6.5V9.5ZM17.5,9.5V6.5H14.5V9.5Z"/>
-</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/layout/qrcode_scan_mode_activity.xml b/packages/SettingsLib/res/layout/qrcode_scan_mode_activity.xml
deleted file mode 100644
index f0a182b..0000000
--- a/packages/SettingsLib/res/layout/qrcode_scan_mode_activity.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2022 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/root"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
-
-    <LinearLayout
-        android:id="@+id/fragment_container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
-
-</LinearLayout>
diff --git a/packages/SettingsLib/res/layout/qrcode_scanner_fragment.xml b/packages/SettingsLib/res/layout/qrcode_scanner_fragment.xml
deleted file mode 100644
index 0a7fe09..0000000
--- a/packages/SettingsLib/res/layout/qrcode_scanner_fragment.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2022 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
-
-    <LinearLayout
-        android:id="@+id/sud_layout_icon_container"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_weight="3"
-        android:layout_marginBottom="35dp">
-        <LinearLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_gravity="bottom"
-            android:gravity="center"
-            android:orientation="vertical">
-            <ImageView
-                android:id="@+id/sud_layout_icon"
-                android:src="@drawable/ic_qr_code_scanner"
-                android:tint="?androidprv:attr/colorAccentPrimaryVariant"
-                android:layout_width="@dimen/qrcode_icon_size"
-                android:layout_height="@dimen/qrcode_icon_size"
-                android:contentDescription="@null"/>
-
-            <TextView
-                android:id="@+id/sud_layout_title"
-                style="@style/QrCodeScanner"
-                android:textSize="24sp"
-                android:text="@string/bt_le_audio_scan_qr_code"
-                android:gravity="center"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="19dp"/>
-
-            <TextView
-                android:id="@+id/sud_layout_subtitle"
-                style="@style/QrCodeScanner"
-                android:text="@string/bt_le_audio_scan_qr_code_scanner"
-                android:gravity="center"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"/>
-        </LinearLayout>
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_weight="7"
-        android:orientation="vertical">
-
-        <FrameLayout
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="top"
-            android:gravity="center"
-            android:clipChildren="true">
-            <TextureView
-                android:id="@+id/preview_view"
-                android:layout_marginStart="@dimen/qrcode_preview_margin"
-                android:layout_marginEnd="@dimen/qrcode_preview_margin"
-                android:layout_width="match_parent"
-                android:layout_height="@dimen/qrcode_preview_size"/>
-        </FrameLayout>
-
-        <TextView
-            android:id="@+id/error_message"
-            style="@style/TextAppearance.ErrorText"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="16dp"
-            android:layout_marginStart="?attr/sudMarginStart"
-            android:layout_marginEnd="?attr/sudMarginEnd"
-            android:gravity="center"
-            android:layout_gravity="center"
-            android:visibility="invisible"/>
-
-    </LinearLayout>
-
-
-</LinearLayout>
-
diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml
index cbc79d2..226b119 100644
--- a/packages/SettingsLib/res/values/dimens.xml
+++ b/packages/SettingsLib/res/values/dimens.xml
@@ -115,12 +115,6 @@
     <!-- Minimum density scale. This is available on all devices. -->
     <fraction name="display_density_min_scale">85%</fraction>
 
-    <!-- QR code picture size -->
-    <dimen name="qrcode_preview_size">360dp</dimen>
-    <dimen name="qrcode_preview_margin">40dp</dimen>
-    <dimen name="qrcode_preview_radius">30dp</dimen>
-    <dimen name="qrcode_icon_size">27dp</dimen>
-
     <!-- Broadcast dialog -->
     <dimen name="broadcast_dialog_title_img_margin_top">18dp</dimen>
     <dimen name="broadcast_dialog_title_text_size">24sp</dimen>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index a171f86..847f1dc 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1589,13 +1589,6 @@
     <!-- Description for a setting which controls whether an app can turn the screen on [CHAR LIMIT=NONE] -->
     <string name="allow_turn_screen_on_description">Allow an app to turn the screen on. If granted, the app may turn on the screen at any time without your explicit intent.</string>
 
-    <!-- [CHAR LIMIT=NONE] Le audio QR code scanner title -->
-    <string name="bt_le_audio_scan_qr_code">Scan QR code</string>
-    <!-- [CHAR LIMIT=NONE] Le audio QR code scanner sub-title -->
-    <string name="bt_le_audio_scan_qr_code_scanner">To start listening, center the QR code below</string>
-    <!-- [CHAR LIMIT=NONE] Hint for QR code process failure -->
-    <string name="bt_le_audio_qr_code_is_not_valid_format">QR code isn\u0027t a valid format</string>
-
     <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, title -->
     <string name="bt_le_audio_broadcast_dialog_title">Stop broadcasting <xliff:g id="app_name" example="App Name 1">%1$s</xliff:g>?</string>
     <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, sub-title -->
diff --git a/packages/SettingsLib/res/values/styles.xml b/packages/SettingsLib/res/values/styles.xml
index 3234515..5237b4f 100644
--- a/packages/SettingsLib/res/values/styles.xml
+++ b/packages/SettingsLib/res/values/styles.xml
@@ -33,13 +33,6 @@
         <item name="android:textColor">?android:attr/colorError</item>
     </style>
 
-    <style name="QrCodeScanner">
-        <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
-        <item name="android:textSize">16sp</item>
-        <item name="android:textColor">?android:attr/textColorPrimary</item>
-        <item name="android:textDirection">locale</item>
-    </style>
-
     <style name="BroadcastDialogTitleStyle">
         <item name="android:textAppearance">@style/TextAppearanceBroadcastDialogTitle</item>
         <item name="android:layout_marginStart">@dimen/broadcast_dialog_title_text_margin</item>
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index c9af4d5..fea7475 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -30,6 +30,9 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 public class BluetoothUtils {
     private static final String TAG = "BluetoothUtils";
@@ -39,6 +42,8 @@
 
     public static final int META_INT_ERROR = -1;
     public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled";
+    private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
+    private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH";
 
     private static ErrorListener sErrorListener;
 
@@ -384,8 +389,43 @@
         return Uri.parse(data);
     }
 
+    /**
+     * Get URI Bluetooth metadata for extra control
+     *
+     * @param bluetoothDevice the BluetoothDevice to get metadata
+     * @return the URI metadata
+     */
+    public static String getControlUriMetaData(BluetoothDevice bluetoothDevice) {
+        String data = getStringMetaData(bluetoothDevice, METADATA_FAST_PAIR_CUSTOMIZED_FIELDS);
+        return extraTagValue(KEY_HEARABLE_CONTROL_SLICE, data);
+    }
+
     @SuppressLint("NewApi") // Hidden API made public
     private static boolean doesClassMatch(BluetoothClass btClass, int classId) {
         return btClass.doesClassMatch(classId);
     }
+
+    private static String extraTagValue(String tag, String metaData) {
+        if (TextUtils.isEmpty(metaData)) {
+            return null;
+        }
+        Pattern pattern = Pattern.compile(generateExpressionWithTag(tag, "(.*?)"));
+        Matcher matcher = pattern.matcher(metaData);
+        if (matcher.find()) {
+            return matcher.group(1);
+        }
+        return null;
+    }
+
+    private static String getTagStart(String tag) {
+        return String.format(Locale.ENGLISH, "<%s>", tag);
+    }
+
+    private static String getTagEnd(String tag) {
+        return String.format(Locale.ENGLISH, "</%s>", tag);
+    }
+
+    private static String generateExpressionWithTag(String tag, String value) {
+        return getTagStart(tag) + value + getTagEnd(tag);
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 6919cf2..a9da2e0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -77,6 +77,8 @@
     private final LocalBluetoothProfileManager mProfileManager;
     private final Object mProfileLock = new Object();
     BluetoothDevice mDevice;
+    private int mDeviceSide;
+    private int mDeviceMode;
     private long mHiSyncId;
     private int mGroupId;
     // Need this since there is no method for getting RSSI
@@ -335,6 +337,22 @@
         connectDevice();
     }
 
+    public int getDeviceSide() {
+        return mDeviceSide;
+    }
+
+    public void setDeviceSide(int side) {
+        mDeviceSide = side;
+    }
+
+    public int getDeviceMode() {
+        return mDeviceMode;
+    }
+
+    public void setDeviceMode(int mode) {
+        mDeviceMode = mode;
+    }
+
     public long getHiSyncId() {
         return mHiSyncId;
     }
@@ -1000,7 +1018,6 @@
                         == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET)) {
                     EventLog.writeEvent(0x534e4554, "138529441", -1, "");
                 }
-                mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
             }
         }
     }
@@ -1111,7 +1128,8 @@
                 stringRes = R.string.bluetooth_battery_level;
             }
 
-            // Set active string in following device connected situation.
+            // Set active string in following device connected situation, also show battery
+            // information if they have.
             //    1. Hearing Aid device active.
             //    2. Headset device active with in-calling state.
             //    3. A2DP device active without in-calling state.
@@ -1130,6 +1148,24 @@
                         stringRes = R.string.bluetooth_active_no_battery_level;
                     }
                 }
+
+                // Try to show left/right information if can not get it from battery for hearing
+                // aids specifically.
+                if (mIsActiveDeviceHearingAid
+                        && stringRes == R.string.bluetooth_active_no_battery_level) {
+                    final CachedBluetoothDevice subDevice = getSubDevice();
+                    if (subDevice != null && subDevice.isConnected()) {
+                        stringRes = R.string.bluetooth_hearing_aid_left_and_right_active;
+                    } else {
+                        if (mDeviceSide == HearingAidProfile.DeviceSide.SIDE_LEFT) {
+                            stringRes = R.string.bluetooth_hearing_aid_left_active;
+                        } else if (mDeviceSide == HearingAidProfile.DeviceSide.SIDE_RIGHT) {
+                            stringRes = R.string.bluetooth_hearing_aid_right_active;
+                        } else {
+                            stringRes = R.string.bluetooth_active_no_battery_level;
+                        }
+                    }
+                }
             }
         }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
index 6f2d4de..a491455 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
@@ -29,13 +29,48 @@
 import android.content.Context;
 import android.util.Log;
 
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+
 import com.android.settingslib.R;
 import com.android.settingslib.Utils;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
 
 public class HearingAidProfile implements LocalBluetoothProfile {
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            DeviceSide.SIDE_INVALID,
+            DeviceSide.SIDE_LEFT,
+            DeviceSide.SIDE_RIGHT
+    })
+
+    /** Side definition for hearing aids. See {@link BluetoothHearingAid}. */
+    public @interface DeviceSide {
+        int SIDE_INVALID = -1;
+        int SIDE_LEFT = 0;
+        int SIDE_RIGHT = 1;
+    }
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            DeviceMode.MODE_INVALID,
+            DeviceMode.MODE_MONAURAL,
+            DeviceMode.MODE_BINAURAL
+    })
+
+    /** Mode definition for hearing aids. See {@link BluetoothHearingAid}. */
+    public @interface DeviceMode {
+        int MODE_INVALID = -1;
+        int MODE_MONAURAL = 0;
+        int MODE_BINAURAL = 1;
+    }
+
     private static final String TAG = "HearingAidProfile";
     private static boolean V = true;
 
@@ -212,6 +247,11 @@
         return isEnabled;
     }
 
+    /**
+     * Tells remote device to set an absolute volume.
+     *
+     * @param volume Absolute volume to be set on remote
+     */
     public void setVolume(int volume) {
         if (mService == null) {
             return;
@@ -219,6 +259,12 @@
         mService.setVolume(volume);
     }
 
+    /**
+     * Gets the HiSyncId (unique hearing aid device identifier) of the device.
+     *
+     * @param device Bluetooth device
+     * @return the HiSyncId of the device
+     */
     public long getHiSyncId(BluetoothDevice device) {
         if (mService == null || device == null) {
             return BluetoothHearingAid.HI_SYNC_ID_INVALID;
@@ -226,6 +272,59 @@
         return mService.getHiSyncId(device);
     }
 
+    /**
+     * Gets the side of the device.
+     *
+     * @param device Bluetooth device.
+     * @return side of the device. See {@link DeviceSide}.
+     */
+    @DeviceSide
+    public int getDeviceSide(@NonNull BluetoothDevice device) {
+        final int defaultValue = DeviceSide.SIDE_INVALID;
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to HearingAidService");
+            return defaultValue;
+        }
+
+        try {
+            Method method = mService.getClass().getDeclaredMethod("getDeviceSideInternal",
+                    BluetoothDevice.class);
+            method.setAccessible(true);
+            return (int) method.invoke(mService, device);
+        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+            Log.e(TAG, "fail to get getDeviceSideInternal\n" + e.toString() + "\n"
+                    + Log.getStackTraceString(new Throwable()));
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Gets the mode of the device.
+     *
+     * @param device Bluetooth device
+     * @return mode of the device. See {@link DeviceMode}.
+     */
+    @DeviceMode
+    public int getDeviceMode(@NonNull BluetoothDevice device) {
+        final int defaultValue = DeviceMode.MODE_INVALID;
+        if (mService == null) {
+            Log.w(TAG, "Proxy not attached to HearingAidService");
+            return defaultValue;
+        }
+
+        try {
+            Method method = mService.getClass().getDeclaredMethod("getDeviceModeInternal",
+                    BluetoothDevice.class);
+            method.setAccessible(true);
+            return (int) method.invoke(mService, device);
+        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+            Log.e(TAG, "fail to get getDeviceModeInternal\n" + e.toString() + "\n"
+                    + Log.getStackTraceString(new Throwable()));
+
+            return defaultValue;
+        }
+    }
+
     public String toString() {
         return NAME;
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 0619986..58944f6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -340,6 +340,11 @@
             if (getHearingAidProfile() != null &&
                 mProfile instanceof HearingAidProfile &&
                 (newState == BluetoothProfile.STATE_CONNECTED)) {
+                final int side = getHearingAidProfile().getDeviceSide(cachedDevice.getDevice());
+                final int mode = getHearingAidProfile().getDeviceMode(cachedDevice.getDevice());
+                cachedDevice.setDeviceSide(side);
+                cachedDevice.setDeviceMode(mode);
+
                 // Check if the HiSyncID has being initialized
                 if (cachedDevice.getHiSyncId() == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
                     long newHiSyncId = getHearingAidProfile().getHiSyncId(cachedDevice.getDevice());
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
index dd7db21..afafd9f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
@@ -59,7 +59,7 @@
 
     @Override
     public Drawable getIcon() {
-        return BluetoothUtils.getBtDrawableWithDescription(mContext, mCachedDevice).first;
+        return BluetoothUtils.getBtClassDrawableWithDescription(mContext, mCachedDevice).first;
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeActivity.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeActivity.java
deleted file mode 100644
index 15a910e..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeActivity.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.qrcode;
-
-import static com.android.settingslib.bluetooth.BluetoothBroadcastUtils.EXTRA_BLUETOOTH_DEVICE_SINK;
-import static com.android.settingslib.bluetooth.BluetoothBroadcastUtils.EXTRA_BLUETOOTH_SINK_IS_GROUP;
-
-import android.bluetooth.BluetoothDevice;
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-
-import androidx.fragment.app.FragmentTransaction;
-
-import com.android.settingslib.R;
-import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
-import com.android.settingslib.bluetooth.BluetoothUtils;
-
-public class QrCodeScanModeActivity extends QrCodeScanModeBaseActivity {
-    private static final boolean DEBUG = BluetoothUtils.D;
-    private static final String TAG = "QrCodeScanModeActivity";
-
-    private boolean mIsGroupOp;
-    private BluetoothDevice mSink;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-    }
-
-    @Override
-    protected void handleIntent(Intent intent) {
-        String action = intent != null ? intent.getAction() : null;
-        if (DEBUG) {
-            Log.d(TAG, "handleIntent(), action = " + action);
-        }
-
-        if (action == null) {
-            finish();
-            return;
-        }
-
-        switch (action) {
-            case BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER:
-                showQrCodeScannerFragment(intent);
-                break;
-            default:
-                if (DEBUG) {
-                    Log.e(TAG, "Launch with an invalid action");
-                }
-                finish();
-        }
-    }
-
-    protected void showQrCodeScannerFragment(Intent intent) {
-        if (DEBUG) {
-            Log.d(TAG, "showQrCodeScannerFragment");
-        }
-
-        if (intent != null) {
-            mSink = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE_SINK);
-            mIsGroupOp = intent.getBooleanExtra(EXTRA_BLUETOOTH_SINK_IS_GROUP, false);
-            if (DEBUG) {
-                Log.d(TAG, "get extra from intent");
-            }
-        } else {
-            if (DEBUG) {
-                Log.d(TAG, "intent is null, can not get bluetooth information from intent.");
-            }
-        }
-
-        QrCodeScanModeFragment fragment =
-                (QrCodeScanModeFragment) mFragmentManager.findFragmentByTag(
-                        BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
-
-        if (fragment == null) {
-            fragment = new QrCodeScanModeFragment(mIsGroupOp, mSink);
-        } else {
-            if (fragment.isVisible()) {
-                return;
-            }
-
-            // When the fragment in back stack but not on top of the stack, we can simply pop
-            // stack because current fragment transactions are arranged in an order
-            mFragmentManager.popBackStackImmediate();
-            return;
-        }
-        final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
-
-        fragmentTransaction.replace(R.id.fragment_container, fragment,
-                BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
-        fragmentTransaction.commit();
-    }
-}
-
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeBaseActivity.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeBaseActivity.java
deleted file mode 100644
index 361fd5b..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeBaseActivity.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.qrcode;
-
-import android.content.Intent;
-import android.os.Bundle;
-
-import androidx.fragment.app.FragmentManager;
-
-import com.android.settingslib.R;
-import com.android.settingslib.core.lifecycle.ObservableActivity;
-
-public abstract class QrCodeScanModeBaseActivity extends ObservableActivity {
-
-    protected FragmentManager mFragmentManager;
-
-    protected abstract void handleIntent(Intent intent);
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setTheme(R.style.SudThemeGlifV3_DayNight);
-
-        setContentView(R.layout.qrcode_scan_mode_activity);
-        mFragmentManager = getSupportFragmentManager();
-
-        if (savedInstanceState == null) {
-            handleIntent(getIntent());
-        }
-    }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeController.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeController.java
deleted file mode 100644
index 153d2d2..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeController.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.qrcode;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothLeBroadcastMetadata;
-import android.content.Context;
-import android.util.Log;
-
-import com.android.settingslib.bluetooth.BluetoothUtils;
-import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
-import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
-
-public class QrCodeScanModeController {
-
-    private static final boolean DEBUG = BluetoothUtils.D;
-    private static final String TAG = "QrCodeScanModeController";
-
-    private LocalBluetoothLeBroadcastMetadata mLocalBroadcastMetadata;
-    private LocalBluetoothLeBroadcastAssistant mLocalBroadcastAssistant;
-    private LocalBluetoothManager mLocalBluetoothManager;
-    private LocalBluetoothProfileManager mProfileManager;
-
-    private LocalBluetoothManager.BluetoothManagerCallback
-            mOnInitCallback = new LocalBluetoothManager.BluetoothManagerCallback() {
-        @Override
-        public void onBluetoothManagerInitialized(Context appContext,
-                LocalBluetoothManager bluetoothManager) {
-            BluetoothUtils.setErrorListener(mErrorListener);
-        }
-    };
-
-    private BluetoothUtils.ErrorListener
-            mErrorListener = new BluetoothUtils.ErrorListener() {
-        @Override
-        public void onShowError(Context context, String name, int messageResId) {
-            if (DEBUG) {
-                Log.d(TAG, "Get error when initializing BluetoothManager. ");
-            }
-        }
-    };
-
-    public QrCodeScanModeController(Context context) {
-        if (DEBUG) {
-            Log.d(TAG, "QrCodeScanModeController constructor.");
-        }
-        mLocalBluetoothManager = LocalBluetoothManager.getInstance(context, mOnInitCallback);
-        mProfileManager = mLocalBluetoothManager.getProfileManager();
-        mLocalBroadcastMetadata = new LocalBluetoothLeBroadcastMetadata();
-        CachedBluetoothDeviceManager cachedDeviceManager = new CachedBluetoothDeviceManager(context,
-                mLocalBluetoothManager);
-        mLocalBroadcastAssistant = new LocalBluetoothLeBroadcastAssistant(context,
-                cachedDeviceManager, mProfileManager);
-    }
-
-    private BluetoothLeBroadcastMetadata convertToBroadcastMetadata(String qrCodeString) {
-        return mLocalBroadcastMetadata.convertToBroadcastMetadata(qrCodeString);
-    }
-
-    public void addSource(BluetoothDevice sink, String sourceMetadata,
-            boolean isGroupOp) {
-        mLocalBroadcastAssistant.addSource(sink,
-                convertToBroadcastMetadata(sourceMetadata), isGroupOp);
-    }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeFragment.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeFragment.java
deleted file mode 100644
index 069b950..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCodeScanModeFragment.java
+++ /dev/null
@@ -1,235 +0,0 @@
-/**
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.qrcode;
-
-import android.bluetooth.BluetoothDevice;
-import android.content.Context;
-import android.graphics.Matrix;
-import android.graphics.Outline;
-import android.graphics.Rect;
-import android.graphics.SurfaceTexture;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.util.Log;
-import android.util.Size;
-import android.view.LayoutInflater;
-import android.view.TextureView;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.TextView;
-
-import com.android.settingslib.R;
-import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
-import com.android.settingslib.bluetooth.BluetoothUtils;
-import com.android.settingslib.core.lifecycle.ObservableFragment;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
-
-public class QrCodeScanModeFragment extends ObservableFragment implements
-        TextureView.SurfaceTextureListener,
-        QrCamera.ScannerCallback {
-    private static final boolean DEBUG = BluetoothUtils.D;
-    private static final String TAG = "QrCodeScanModeFragment";
-
-    /** Message sent to hide error message */
-    private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1;
-    /** Message sent to show error message */
-    private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2;
-    /** Message sent to broadcast QR code */
-    private static final int MESSAGE_SCAN_BROADCAST_SUCCESS = 3;
-
-    private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000;
-    private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000;
-
-    private boolean mIsGroupOp;
-    private int mCornerRadius;
-    private BluetoothDevice mSink;
-    private String mBroadcastMetadata;
-    private Context mContext;
-    private QrCamera mCamera;
-    private QrCodeScanModeController mController;
-    private TextureView mTextureView;
-    private TextView mSummary;
-    private TextView mErrorMessage;
-
-    public QrCodeScanModeFragment(boolean isGroupOp, BluetoothDevice sink) {
-        mIsGroupOp = isGroupOp;
-        mSink = sink;
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        mContext = getContext();
-        mController = new QrCodeScanModeController(mContext);
-    }
-
-    @Override
-    public final View onCreateView(LayoutInflater inflater, ViewGroup container,
-            Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.qrcode_scanner_fragment, container,
-                /* attachToRoot */ false);
-    }
-
-    @Override
-    public void onViewCreated(View view, Bundle savedInstanceState) {
-        mTextureView = view.findViewById(R.id.preview_view);
-        mCornerRadius = mContext.getResources().getDimensionPixelSize(
-                R.dimen.qrcode_preview_radius);
-        mTextureView.setSurfaceTextureListener(this);
-        mTextureView.setOutlineProvider(new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                outline.setRoundRect(0,0, view.getWidth(), view.getHeight(), mCornerRadius);
-            }
-        });
-        mTextureView.setClipToOutline(true);
-        mErrorMessage = view.findViewById(R.id.error_message);
-    }
-
-    private void initCamera(SurfaceTexture surface) {
-        // Check if the camera has already created.
-        if (mCamera == null) {
-            mCamera = new QrCamera(mContext, this);
-            mCamera.start(surface);
-        }
-    }
-
-    private void destroyCamera() {
-        if (mCamera != null) {
-            mCamera.stop();
-            mCamera = null;
-        }
-    }
-
-    @Override
-    public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
-        initCamera(surface);
-    }
-
-    @Override
-    public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width,
-            int height) {
-    }
-
-    @Override
-    public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
-        destroyCamera();
-        return true;
-    }
-
-    @Override
-    public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {
-    }
-
-    @Override
-    public void handleSuccessfulResult(String qrCode) {
-        if (DEBUG) {
-            Log.d(TAG, "handleSuccessfulResult(), get the qr code string.");
-        }
-        mBroadcastMetadata = qrCode;
-        handleBtLeAudioScanner();
-    }
-
-    @Override
-    public void handleCameraFailure() {
-        destroyCamera();
-    }
-
-    @Override
-    public Size getViewSize() {
-        return new Size(mTextureView.getWidth(), mTextureView.getHeight());
-    }
-
-    @Override
-    public Rect getFramePosition(Size previewSize, int cameraOrientation) {
-        return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight());
-    }
-
-    @Override
-    public void setTransform(Matrix transform) {
-        mTextureView.setTransform(transform);
-    }
-
-    @Override
-    public boolean isValid(String qrCode) {
-        if (qrCode.startsWith(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)) {
-            return true;
-        } else {
-            showErrorMessage(R.string.bt_le_audio_qr_code_is_not_valid_format);
-            return false;
-        }
-    }
-
-    protected boolean isDecodeTaskAlive() {
-        return mCamera != null && mCamera.isDecodeTaskAlive();
-    }
-
-    private final Handler mHandler = new Handler() {
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MESSAGE_HIDE_ERROR_MESSAGE:
-                    mErrorMessage.setVisibility(View.INVISIBLE);
-                    break;
-
-                case MESSAGE_SHOW_ERROR_MESSAGE:
-                    final String errorMessage = (String) msg.obj;
-
-                    mErrorMessage.setVisibility(View.VISIBLE);
-                    mErrorMessage.setText(errorMessage);
-                    mErrorMessage.sendAccessibilityEvent(
-                            AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
-
-                    // Cancel any pending messages to hide error view and requeue the message so
-                    // user has time to see error
-                    removeMessages(MESSAGE_HIDE_ERROR_MESSAGE);
-                    sendEmptyMessageDelayed(MESSAGE_HIDE_ERROR_MESSAGE,
-                            SHOW_ERROR_MESSAGE_INTERVAL);
-                    break;
-
-                case MESSAGE_SCAN_BROADCAST_SUCCESS:
-                    mController.addSource(mSink, mBroadcastMetadata, mIsGroupOp);
-                    updateSummary();
-                    mSummary.sendAccessibilityEvent(
-                            AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
-                    break;
-                default:
-            }
-        }
-    };
-
-    private void showErrorMessage(@StringRes int messageResId) {
-        final Message message = mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE,
-                getString(messageResId));
-        message.sendToTarget();
-    }
-
-    private void handleBtLeAudioScanner() {
-        Message message = mHandler.obtainMessage(MESSAGE_SCAN_BROADCAST_SUCCESS);
-        mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL);
-    }
-
-    private void updateSummary() {
-        mSummary.setText(getString(R.string.bt_le_audio_scan_qr_code_scanner,
-                null /* broadcast_name*/));;
-    }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java
index 9ef6bdf..afab046 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java
@@ -29,6 +29,7 @@
 import android.net.wifi.WifiInfo;
 import android.os.Bundle;
 import android.os.SystemClock;
+import android.util.Log;
 
 import androidx.annotation.VisibleForTesting;
 
@@ -40,6 +41,8 @@
 
 public class WifiUtils {
 
+    private static final String TAG = "WifiUtils";
+
     private static final int INVALID_RSSI = -127;
 
     /**
@@ -314,13 +317,17 @@
      *
      * @param level The number of bars to show (0-4)
      * @param noInternet True if a connected Wi-Fi network cannot access the Internet
-     * @throws IllegalArgumentException if an invalid RSSI level is given.
      */
     public static int getInternetIconResource(int level, boolean noInternet) {
-        if (level < 0 || level >= WIFI_PIE.length) {
-            throw new IllegalArgumentException("No Wifi icon found for level: " + level);
+        int wifiLevel = level;
+        if (wifiLevel < 0) {
+            Log.e(TAG, "Wi-Fi level is out of range! level:" + level);
+            wifiLevel = 0;
+        } else if (level >= WIFI_PIE.length) {
+            Log.e(TAG, "Wi-Fi level is out of range! level:" + level);
+            wifiLevel = WIFI_PIE.length - 1;
         }
-        return noInternet ? NO_INTERNET_WIFI_PIE[level] : WIFI_PIE[level];
+        return noInternet ? NO_INTERNET_WIFI_PIE[wifiLevel] : WIFI_PIE[wifiLevel];
     }
 
     /**
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index 2e85514..1c0ea1a 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -51,6 +51,11 @@
     private static final String STRING_METADATA = "string_metadata";
     private static final String BOOL_METADATA = "true";
     private static final String INT_METADATA = "25";
+    private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
+    private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH";
+    private static final String CONTROL_METADATA =
+            "<HEARABLE_CONTROL_SLICE_WITH_WIDTH>" + STRING_METADATA
+                    + "</HEARABLE_CONTROL_SLICE_WITH_WIDTH>";
 
     @Before
     public void setUp() {
@@ -152,6 +157,15 @@
     }
 
     @Test
+    public void getControlUriMetaData_hasMetaData_returnsCorrectMetaData() {
+        when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS)).thenReturn(
+                CONTROL_METADATA.getBytes());
+
+        assertThat(BluetoothUtils.getControlUriMetaData(mBluetoothDevice)).isEqualTo(
+                STRING_METADATA);
+    }
+
+    @Test
     public void isAdvancedDetailsHeader_untetheredHeadset_returnTrue() {
         when(mBluetoothDevice.getMetadata(
                 BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn(
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index 55d125e..be2a55e 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -78,6 +78,7 @@
     @Mock
     private BluetoothDevice mSubDevice;
     private CachedBluetoothDevice mCachedDevice;
+    private CachedBluetoothDevice mSubCachedDevice;
     private AudioManager mAudioManager;
     private Context mContext;
     private int mBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
@@ -95,7 +96,9 @@
         when(mPanProfile.isProfileReady()).thenReturn(true);
         when(mHearingAidProfile.isProfileReady()).thenReturn(true);
         mCachedDevice = spy(new CachedBluetoothDevice(mContext, mProfileManager, mDevice));
+        mSubCachedDevice = spy(new CachedBluetoothDevice(mContext, mProfileManager, mSubDevice));
         doAnswer((invocation) -> mBatteryLevel).when(mCachedDevice).getBatteryLevel();
+        doAnswer((invocation) -> mBatteryLevel).when(mSubCachedDevice).getBatteryLevel();
     }
 
     @Test
@@ -351,8 +354,9 @@
         assertThat(mCachedDevice.getConnectionSummary()).isNull();
 
         // Set device as Active for Hearing Aid and test connection state summary
+        mCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT);
         mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID);
-        assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active");
+        assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active, left only");
 
         // Set Hearing Aid profile to be disconnected and test connection state summary
         mCachedDevice.onActiveDeviceChanged(false, BluetoothProfile.HEARING_AID);
@@ -390,17 +394,36 @@
     }
 
     @Test
-    public void getConnectionSummary_testHearingAidInCall_returnActive() {
+    public void getConnectionSummary_testHearingAidRightEarInCall_returnActiveRightEar() {
         // Arrange:
-        //   1. Profile:       {HEARING_AID, Connected, Active}
+        //   1. Profile:       {HEARING_AID, Connected, Active, Right ear}
         //   2. Audio Manager: In Call
         updateProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+        mCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_RIGHT);
         mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID);
         mAudioManager.setMode(AudioManager.MODE_IN_CALL);
 
         // Act & Assert:
         //   Get "Active" result without Battery Level.
-        assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active");
+        assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active, right only");
+    }
+
+    @Test
+    public void getConnectionSummary_testHearingAidBothEarInCall_returnActiveBothEar() {
+        // Arrange:
+        //   1. Profile:       {HEARING_AID, Connected, Active, Both ear}
+        //   2. Audio Manager: In Call
+        mCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_RIGHT);
+        updateProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+        mSubCachedDevice.setDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT);
+        updateSubDeviceProfileStatus(mHearingAidProfile, BluetoothProfile.STATE_CONNECTED);
+        mCachedDevice.setSubDevice(mSubCachedDevice);
+        mCachedDevice.onActiveDeviceChanged(true, BluetoothProfile.HEARING_AID);
+        mAudioManager.setMode(AudioManager.MODE_IN_CALL);
+
+        // Act & Assert:
+        //   Get "Active" result without Battery Level.
+        assertThat(mCachedDevice.getConnectionSummary()).isEqualTo("Active, left and right");
     }
 
     @Test
@@ -925,39 +948,41 @@
         mCachedDevice.onProfileStateChanged(profile, status);
     }
 
+    private void updateSubDeviceProfileStatus(LocalBluetoothProfile profile, int status) {
+        doReturn(status).when(profile).getConnectionStatus(mSubDevice);
+        mSubCachedDevice.onProfileStateChanged(profile, status);
+    }
+
     @Test
     public void getSubDevice_setSubDevice() {
-        CachedBluetoothDevice subCachedDevice = new CachedBluetoothDevice(mContext, mProfileManager,
-                mSubDevice);
-        mCachedDevice.setSubDevice(subCachedDevice);
+        mCachedDevice.setSubDevice(mSubCachedDevice);
 
-        assertThat(mCachedDevice.getSubDevice()).isEqualTo(subCachedDevice);
+        assertThat(mCachedDevice.getSubDevice()).isEqualTo(mSubCachedDevice);
     }
 
     @Test
     public void switchSubDeviceContent() {
-        CachedBluetoothDevice subCachedDevice = new CachedBluetoothDevice(mContext, mProfileManager,
-                mSubDevice);
+
         mCachedDevice.mRssi = RSSI_1;
         mCachedDevice.mJustDiscovered = JUSTDISCOVERED_1;
-        subCachedDevice.mRssi = RSSI_2;
-        subCachedDevice.mJustDiscovered = JUSTDISCOVERED_2;
-        mCachedDevice.setSubDevice(subCachedDevice);
+        mSubCachedDevice.mRssi = RSSI_2;
+        mSubCachedDevice.mJustDiscovered = JUSTDISCOVERED_2;
+        mCachedDevice.setSubDevice(mSubCachedDevice);
 
         assertThat(mCachedDevice.mRssi).isEqualTo(RSSI_1);
         assertThat(mCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_1);
         assertThat(mCachedDevice.mDevice).isEqualTo(mDevice);
-        assertThat(subCachedDevice.mRssi).isEqualTo(RSSI_2);
-        assertThat(subCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_2);
-        assertThat(subCachedDevice.mDevice).isEqualTo(mSubDevice);
+        assertThat(mSubCachedDevice.mRssi).isEqualTo(RSSI_2);
+        assertThat(mSubCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_2);
+        assertThat(mSubCachedDevice.mDevice).isEqualTo(mSubDevice);
         mCachedDevice.switchSubDeviceContent();
 
         assertThat(mCachedDevice.mRssi).isEqualTo(RSSI_2);
         assertThat(mCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_2);
         assertThat(mCachedDevice.mDevice).isEqualTo(mSubDevice);
-        assertThat(subCachedDevice.mRssi).isEqualTo(RSSI_1);
-        assertThat(subCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_1);
-        assertThat(subCachedDevice.mDevice).isEqualTo(mDevice);
+        assertThat(mSubCachedDevice.mRssi).isEqualTo(RSSI_1);
+        assertThat(mSubCachedDevice.mJustDiscovered).isEqualTo(JUSTDISCOVERED_1);
+        assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice);
     }
 
     @Test
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
index 6956105..b60dc6a 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
@@ -188,6 +188,19 @@
     }
 
     @Test
+    public void getInternetIconResource_levelOutOfRange_shouldNotCrash() {
+        // Verify that Wi-Fi level is less than the minimum level (0)
+        int level = -1;
+        WifiUtils.getInternetIconResource(level, false /* noInternet*/);
+        WifiUtils.getInternetIconResource(level, true /* noInternet*/);
+
+        // Verify that Wi-Fi level is greater than the maximum level (4)
+        level = WifiUtils.WIFI_PIE.length;
+        WifiUtils.getInternetIconResource(level, false /* noInternet*/);
+        WifiUtils.getInternetIconResource(level, true /* noInternet*/);
+    }
+
+    @Test
     public void testInternetIconInjector_getIcon_returnsCorrectValues() {
         WifiUtils.InternetIconInjector iconInjector = new WifiUtils.InternetIconInjector(mContext);
 
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
index 0c6d40aa..5088533 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java
@@ -1074,13 +1074,63 @@
             if (DEBUG) Log.d(TAG, "Successfully unMarshaled SoftApConfiguration ");
             // Depending on device hardware, we may need to notify the user of a setting change
             SoftApConfiguration storedConfig = mWifiManager.getSoftApConfiguration();
-            if (!storedConfig.equals(configInCloud)) {
+
+            if (isNeedToNotifyUserConfigurationHasChanged(configInCloud, storedConfig)) {
                 Log.d(TAG, "restored ap configuration requires a conversion, notify the user");
                 WifiSoftApConfigChangedNotifier.notifyUserOfConfigConversion(this);
             }
         }
     }
 
+    private boolean isNeedToNotifyUserConfigurationHasChanged(SoftApConfiguration configInCloud,
+            SoftApConfiguration storedConfig) {
+        // Check if the cloud configuration was modified when restored to the device.
+        // All elements of the configuration are compared except:
+        // 1. Persistent randomized MAC address (which is per device)
+        // 2. The flag indicating whether the configuration is "user modified"
+        return !(Objects.equals(configInCloud.getWifiSsid(), storedConfig.getWifiSsid())
+                && Objects.equals(configInCloud.getBssid(), storedConfig.getBssid())
+                && Objects.equals(configInCloud.getPassphrase(), storedConfig.getPassphrase())
+                && configInCloud.isHiddenSsid() == storedConfig.isHiddenSsid()
+                && configInCloud.getChannels().toString().equals(
+                        storedConfig.getChannels().toString())
+                && configInCloud.getSecurityType() == storedConfig.getSecurityType()
+                && configInCloud.getMaxNumberOfClients() == storedConfig.getMaxNumberOfClients()
+                && configInCloud.isAutoShutdownEnabled() == storedConfig.isAutoShutdownEnabled()
+                && configInCloud.getShutdownTimeoutMillis()
+                        == storedConfig.getShutdownTimeoutMillis()
+                && configInCloud.isClientControlByUserEnabled()
+                        == storedConfig.isClientControlByUserEnabled()
+                && Objects.equals(configInCloud.getBlockedClientList(),
+                        storedConfig.getBlockedClientList())
+                && Objects.equals(configInCloud.getAllowedClientList(),
+                        storedConfig.getAllowedClientList())
+                && configInCloud.getMacRandomizationSetting()
+                        == storedConfig.getMacRandomizationSetting()
+                && configInCloud.isBridgedModeOpportunisticShutdownEnabled()
+                        == storedConfig.isBridgedModeOpportunisticShutdownEnabled()
+                && configInCloud.isIeee80211axEnabled() == storedConfig.isIeee80211axEnabled()
+                && configInCloud.isIeee80211beEnabled() == storedConfig.isIeee80211beEnabled()
+                && configInCloud.getBridgedModeOpportunisticShutdownTimeoutMillis()
+                        == storedConfig.getBridgedModeOpportunisticShutdownTimeoutMillis()
+                && Objects.equals(configInCloud.getVendorElements(),
+                        storedConfig.getVendorElements())
+                && (configInCloud.getPersistentRandomizedMacAddress() != null
+                        ? Objects.equals(configInCloud.getPersistentRandomizedMacAddress(),
+                        storedConfig.getPersistentRandomizedMacAddress()) : true)
+                && Arrays.equals(configInCloud.getAllowedAcsChannels(
+                        SoftApConfiguration.BAND_2GHZ),
+                        storedConfig.getAllowedAcsChannels(SoftApConfiguration.BAND_2GHZ))
+                && Arrays.equals(configInCloud.getAllowedAcsChannels(
+                        SoftApConfiguration.BAND_5GHZ),
+                        storedConfig.getAllowedAcsChannels(SoftApConfiguration.BAND_5GHZ))
+                && Arrays.equals(configInCloud.getAllowedAcsChannels(
+                        SoftApConfiguration.BAND_6GHZ),
+                        storedConfig.getAllowedAcsChannels(SoftApConfiguration.BAND_6GHZ))
+                && configInCloud.getMaxChannelBandwidth() == storedConfig.getMaxChannelBandwidth()
+                        );
+    }
+
     private byte[] getNetworkPolicies() {
         NetworkPolicyManager networkPolicyManager =
                 (NetworkPolicyManager) getSystemService(NETWORK_POLICY_SERVICE);
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 290ce34..5dd1b09 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -323,6 +323,8 @@
     <!-- To read safety center status -->
     <uses-permission android:name="android.permission.READ_SAFETY_CENTER_STATUS" />
 
+    <uses-permission android:name="android.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS" />
+
     <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" />
     <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" />
     <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" />
@@ -721,7 +723,7 @@
                   android:excludeFromRecents="true"
                   android:stateNotNeeded="true"
                   android:resumeWhilePausing="true"
-                  android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
+                  android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
             <intent-filter>
                 <action android:name="android.app.action.CONFIRM_DEVICE_CREDENTIAL_WITH_USER" />
                 <category android:name="android.intent.category.DEFAULT" />
diff --git a/packages/SystemUI/animation/res/values/ids.xml b/packages/SystemUI/animation/res/values/ids.xml
index 4e9a4be..03ca462 100644
--- a/packages/SystemUI/animation/res/values/ids.xml
+++ b/packages/SystemUI/animation/res/values/ids.xml
@@ -16,7 +16,8 @@
 -->
 <resources>
     <!-- DialogLaunchAnimator -->
-    <item type="id" name="launch_animation_running"/>
+    <item type="id" name="tag_launch_animation_running"/>
+    <item type="id" name="tag_dialog_background"/>
 
     <!-- ViewBoundsAnimator -->
     <item type="id" name="tag_animator"/>
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index 50178f4..63276c9 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -62,7 +62,7 @@
             positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator
         )
 
-        private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running
+        private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.tag_launch_animation_running
     }
 
     /**
@@ -447,6 +447,7 @@
             dialogContentWithBackground
         }
         this.dialogContentWithBackground = dialogContentWithBackground
+        dialogContentWithBackground.setTag(R.id.tag_dialog_background, true)
 
         val background = dialogContentWithBackground.background
         originalDialogBackgroundColor =
@@ -584,7 +585,7 @@
                 GhostView.removeGhost(touchSurface)
             },
             onLaunchAnimationEnd = {
-                touchSurface.setTag(R.id.launch_animation_running, null)
+                touchSurface.setTag(R.id.tag_launch_animation_running, null)
 
                 // We hide the touch surface when the dialog is showing. We will make this
                 // view visible again when dismissing the dialog.
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
index dd45b6f..0c82022 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
@@ -21,7 +21,6 @@
 import android.graphics.Color
 import com.android.internal.graphics.ColorUtils
 import com.android.internal.graphics.cam.Cam
-import com.android.internal.graphics.cam.CamUtils.lstarFromInt
 import kotlin.math.absoluteValue
 import kotlin.math.max
 import kotlin.math.roundToInt
@@ -50,7 +49,7 @@
             val previousHue = hueAndRotations[previousIndex].first
             if (ColorScheme.angleIsBetween(sourceHue, thisHue, previousHue)) {
                 return ColorScheme.wrapDegreesDouble(sourceHue.toDouble() +
-                        hueAndRotations[previousIndex].first)
+                        hueAndRotations[previousIndex].second)
             }
         }
 
@@ -143,12 +142,24 @@
     }
 }
 
+internal class ChromaMultiple(val multiple: Double) : Chroma {
+    override fun get(sourceColor: Cam): Double {
+        return sourceColor.chroma * multiple
+    }
+}
+
 internal class ChromaConstant(val chroma: Double) : Chroma {
     override fun get(sourceColor: Cam): Double {
         return chroma
     }
 }
 
+internal class ChromaSource : Chroma {
+    override fun get(sourceColor: Cam): Double {
+        return sourceColor.chroma.toDouble()
+    }
+}
+
 internal class TonalSpec(val hue: Hue = HueSource(), val chroma: Chroma) {
     fun shades(sourceColor: Cam): List<Int> {
         val hue = hue.get(sourceColor)
@@ -184,8 +195,8 @@
             a1 = TonalSpec(HueSource(), ChromaMinimum(48.0)),
             a2 = TonalSpec(HueVibrantSecondary(), ChromaConstant(24.0)),
             a3 = TonalSpec(HueVibrantTertiary(), ChromaConstant(32.0)),
-            n1 = TonalSpec(HueSource(), ChromaConstant(10.0)),
-            n2 = TonalSpec(HueSource(), ChromaConstant(12.0))
+            n1 = TonalSpec(HueSource(), ChromaConstant(12.0)),
+            n2 = TonalSpec(HueSource(), ChromaConstant(14.0))
     )),
     EXPRESSIVE(CoreSpec(
             a1 = TonalSpec(HueAdd(240.0), ChromaConstant(40.0)),
@@ -208,6 +219,13 @@
             n1 = TonalSpec(HueSource(), ChromaConstant(10.0)),
             n2 = TonalSpec(HueSource(), ChromaConstant(16.0))
     )),
+    CONTENT(CoreSpec(
+            a1 = TonalSpec(HueSource(), ChromaSource()),
+            a2 = TonalSpec(HueSource(), ChromaMultiple(0.33)),
+            a3 = TonalSpec(HueSource(), ChromaMultiple(0.66)),
+            n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)),
+            n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666))
+    )),
 }
 
 class ColorScheme(
@@ -231,7 +249,7 @@
         darkTheme: Boolean,
         style: Style = Style.TONAL_SPOT
     ):
-            this(getSeedColor(wallpaperColors), darkTheme, style)
+            this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style)
 
     val allAccentColors: List<Int>
         get() {
@@ -260,7 +278,7 @@
         val proposedSeedCam = Cam.fromInt(seed)
         val seedArgb = if (seed == Color.TRANSPARENT) {
             GOOGLE_BLUE
-        } else if (proposedSeedCam.chroma < 5) {
+        } else if (style != Style.CONTENT && proposedSeedCam.chroma < 5) {
             GOOGLE_BLUE
         } else {
             seed
@@ -289,22 +307,26 @@
          * Identifies a color to create a color scheme from.
          *
          * @param wallpaperColors Colors extracted from an image via quantization.
+         * @param filter If false, allow colors that have low chroma, creating grayscale themes.
          * @return ARGB int representing the color
          */
         @JvmStatic
+        @JvmOverloads
         @ColorInt
-        fun getSeedColor(wallpaperColors: WallpaperColors): Int {
-            return getSeedColors(wallpaperColors).first()
+        fun getSeedColor(wallpaperColors: WallpaperColors, filter: Boolean = true): Int {
+            return getSeedColors(wallpaperColors, filter).first()
         }
 
         /**
          * Filters and ranks colors from WallpaperColors.
          *
          * @param wallpaperColors Colors extracted from an image via quantization.
+         * @param filter If false, allow colors that have low chroma, creating grayscale themes.
          * @return List of ARGB ints, ordered from highest scoring to lowest.
          */
         @JvmStatic
-        fun getSeedColors(wallpaperColors: WallpaperColors): List<Int> {
+        @JvmOverloads
+        fun getSeedColors(wallpaperColors: WallpaperColors, filter: Boolean = true): List<Int> {
             val totalPopulation = wallpaperColors.allColors.values.reduce { a, b -> a + b }
                     .toDouble()
             val totalPopulationMeaningless = (totalPopulation == 0.0)
@@ -317,9 +339,12 @@
                 val distinctColors = wallpaperColors.mainColors.map {
                     it.toArgb()
                 }.distinct().filter {
-                    Cam.fromInt(it).chroma >= MIN_CHROMA
+                    if (!filter) {
+                        true
+                    } else {
+                        Cam.fromInt(it).chroma >= MIN_CHROMA
+                    }
                 }.toList()
-
                 if (distinctColors.isEmpty()) {
                     return listOf(GOOGLE_BLUE)
                 }
@@ -332,7 +357,7 @@
             val intToCam = wallpaperColors.allColors.mapValues { Cam.fromInt(it.key) }
 
             // Get an array with 360 slots. A slot contains the percentage of colors with that hue.
-            val hueProportions = huePopulations(intToCam, intToProportion)
+            val hueProportions = huePopulations(intToCam, intToProportion, filter)
             // Map each color to the percentage of the image with its hue.
             val intToHueProportion = wallpaperColors.allColors.mapValues {
                 val cam = intToCam[it.key]!!
@@ -346,13 +371,12 @@
             // Remove any inappropriate seed colors. For example, low chroma colors look grayscale
             // raising their chroma will turn them to a much louder color that may not have been
             // in the image.
-            val filteredIntToCam = intToCam.filter {
+            val filteredIntToCam = if (!filter) intToCam else (intToCam.filter {
                 val cam = it.value
-                val lstar = lstarFromInt(it.key)
                 val proportion = intToHueProportion[it.key]!!
                 cam.chroma >= MIN_CHROMA &&
                         (totalPopulationMeaningless || proportion > 0.01)
-            }
+            })
             // Sort the colors by score, from high to low.
             val intToScoreIntermediate = filteredIntToCam.mapValues {
                 score(it.value, intToHueProportion[it.key]!!)
@@ -444,7 +468,8 @@
 
         private fun huePopulations(
             camByColor: Map<Int, Cam>,
-            populationByColor: Map<Int, Double>
+            populationByColor: Map<Int, Double>,
+            filter: Boolean = true
         ): List<Double> {
             val huePopulation = List(size = 360, init = { 0.0 }).toMutableList()
 
@@ -452,7 +477,7 @@
                 val population = populationByColor[entry.key]!!
                 val cam = camByColor[entry.key]!!
                 val hue = cam.hue.roundToInt() % 360
-                if (cam.chroma <= MIN_CHROMA) {
+                if (filter && cam.chroma <= MIN_CHROMA) {
                     continue
                 }
                 huePopulation[hue] = huePopulation[hue] + population
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
index 3d2f570..894bb5f 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
@@ -181,7 +181,7 @@
     public interface Callbacks {
         int VERSION = 1;
 
-        void onShowRequested(int reason);
+        void onShowRequested(int reason, boolean keyguardLocked, int lockTaskModeState);
         void onDismissRequested(int reason);
         void onStateChanged(State state);
         void onLayoutDirectionChanged(int layoutDirection);
diff --git a/packages/SystemUI/res/drawable/ic_chevron_icon.xml b/packages/SystemUI/res/drawable/ic_chevron_icon.xml
index acbbbcb..d60cc8c 100644
--- a/packages/SystemUI/res/drawable/ic_chevron_icon.xml
+++ b/packages/SystemUI/res/drawable/ic_chevron_icon.xml
@@ -15,14 +15,6 @@
   ~ limitations under the License.
   -->
 
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="18dp"
-    android:height="31dp"
-    android:viewportWidth="18"
-    android:viewportHeight="31">
-  <path
-      android:pathData="M0.0061,27.8986L2.6906,30.5831L17.9219,15.3518L2.6906,0.1206L0.0061,2.8051L12.5338,15.3518"
-      android:strokeAlpha="0.7"
-      android:fillColor="#FFFFFF"
-      android:fillAlpha="0.7"/>
-</vector>
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" android:tint="?attr/colorControlNormal">
+    <path android:fillColor="@android:color/white" android:pathData="M9.4,18 L8,16.6 12.6,12 8,7.4 9.4,6 15.4,12Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_status_check.xml b/packages/SystemUI/res/drawable/media_output_status_check.xml
index 5fbc42b..3d64f83 100644
--- a/packages/SystemUI/res/drawable/media_output_status_check.xml
+++ b/packages/SystemUI/res/drawable/media_output_status_check.xml
@@ -21,6 +21,6 @@
         android:viewportHeight="24"
         android:tint="?attr/colorControlNormal">
     <path
-        android:fillColor="@color/media_dialog_item_main_content"
-        android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
+        android:fillColor="@android:color/white"
+        android:pathData="M12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.9,2.787Q17.725,3.575 19.075,4.925Q20.425,6.275 21.212,8.1Q22,9.925 22,12Q22,14.075 21.212,15.9Q20.425,17.725 19.075,19.075Q17.725,20.425 15.9,21.212Q14.075,22 12,22ZM10.6,16.6 L17.65,9.55 16.25,8.15 10.6,13.8 7.75,10.95 6.35,12.35Z"/>
 </vector>
diff --git a/packages/SystemUI/res/drawable/overlay_cancel.xml b/packages/SystemUI/res/drawable/overlay_cancel.xml
index f9786e2..3fa12dd 100644
--- a/packages/SystemUI/res/drawable/overlay_cancel.xml
+++ b/packages/SystemUI/res/drawable/overlay_cancel.xml
@@ -24,6 +24,6 @@
         android:fillColor="?androidprv:attr/colorAccentTertiary"
         android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"/>
     <path
-        android:fillColor="?android:attr/textColorPrimary"
+        android:fillColor="?attr/overlayButtonTextColor"
         android:pathData="M23,10.41L21.59,9 16,14.59 10.41,9 9,10.41 14.59,16 9,21.59 10.41,23 16,17.41 21.59,23 23,21.59 17.41,16z"/>
 </vector>
diff --git a/packages/SystemUI/res/layout/auth_biometric_background.xml b/packages/SystemUI/res/layout/auth_biometric_background.xml
new file mode 100644
index 0000000..7ce81ad
--- /dev/null
+++ b/packages/SystemUI/res/layout/auth_biometric_background.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="80dp"
+        android:layout_height="80dp"
+        android:layout_marginBottom="160dp"/>
+
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/clipboard_edit_text_activity.xml b/packages/SystemUI/res/layout/clipboard_edit_text_activity.xml
index 1122ca6..1c09e81 100644
--- a/packages/SystemUI/res/layout/clipboard_edit_text_activity.xml
+++ b/packages/SystemUI/res/layout/clipboard_edit_text_activity.xml
@@ -12,6 +12,7 @@
         android:layout_height="48dp"
         android:layout_marginTop="8dp"
         android:layout_marginStart="12dp"
+        android:paddingHorizontal="16dp"
         android:background="@drawable/overlay_button_background"
         android:text="@string/clipboard_edit_text_done"
         app:layout_constraintStart_toStartOf="parent"
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index 085a581..1712b48 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -67,9 +67,9 @@
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:layout_marginStart="@dimen/overlay_offset_x"
-        android:layout_marginBottom="8dp"
+        android:layout_marginBottom="12dp"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintBottom_toBottomOf="@id/actions_container_background"
+        app:layout_constraintBottom_toBottomOf="parent"
         android:elevation="7dp"
         app:layout_constraintEnd_toEndOf="@id/clipboard_preview_end"
         app:layout_constraintTop_toTopOf="@id/clipboard_preview_top"
diff --git a/packages/SystemUI/res/values/defaults.xml b/packages/SystemUI/res/values/defaults.xml
deleted file mode 100644
index f96c178..0000000
--- a/packages/SystemUI/res/values/defaults.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/**
- * Copyright (c) 2009, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); 
- * you may not use this file except in compliance with the License. 
- * You may obtain a copy of the License at 
- *
- *     http://www.apache.org/licenses/LICENSE-2.0 
- *
- * Unless required by applicable law or agreed to in writing, software 
- * distributed under the License is distributed on an "AS IS" BASIS, 
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
- * See the License for the specific language governing permissions and 
- * limitations under the License.
- */
--->
-<resources>
-    <!-- Default for SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER.
-    To be set if the device wants to support out of the box QR code scanning experience -->
-    <string name="def_qr_code_component" translatable="false"></string>
-</resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 0bc3594..8ee29d7 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -30,10 +30,10 @@
     <dimen name="navigation_bar_deadzone_size_max">32dp</dimen>
 
     <!-- dimensions for the navigation bar handle -->
-    <dimen name="navigation_handle_radius">1dp</dimen>
-    <dimen name="navigation_handle_bottom">6dp</dimen>
+    <dimen name="navigation_handle_radius">2dp</dimen>
+    <dimen name="navigation_handle_bottom">8dp</dimen>
     <dimen name="navigation_handle_sample_horizontal_margin">10dp</dimen>
-    <dimen name="navigation_home_handle_width">72dp</dimen>
+    <dimen name="navigation_home_handle_width">108dp</dimen>
 
     <!-- Size of the nav bar edge panels, should be greater to the
          edge sensitivity + the drag threshold -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2426f01..7010a28 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -804,6 +804,14 @@
     <!-- Message shown when non-bypass face authentication succeeds and UDFPS is supported. Provides extra instructions for how the user can enter their device [CHAR LIMIT=60] -->
     <string name="keyguard_face_successful_unlock_press">Unlocked by face. Press the unlock icon to open.</string>
 
+    <!-- Messages shown when users press outside of udfps region during -->
+    <string-array name="udfps_accessibility_touch_hints">
+        <item>Move left</item>
+        <item>Move down</item>
+        <item>Move right</item>
+        <item>Move up</item>
+    </string-array>
+
     <!-- Message shown when face authentication fails and the pin pad is visible. [CHAR LIMIT=60] -->
     <string name="keyguard_retry">Swipe up to try again</string>
 
@@ -2271,6 +2279,8 @@
     <string name="media_output_dialog_unknown_launch_app_name">Unknown app</string>
     <!-- Button text for stopping casting [CHAR LIMIT=60] -->
     <string name="media_output_dialog_button_stop_casting">Stop casting</string>
+    <!-- Accessibility text describing purpose of media output dialog. [CHAR LIMIT=NONE] -->
+    <string name="media_output_dialog_accessibility_title">Available devices for audio output.</string>
 
     <!-- Media Output Broadcast Dialog -->
     <!-- Title for Broadcast First Notify Dialog [CHAR LIMIT=60] -->
@@ -2484,6 +2494,12 @@
     <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>
+    <!-- Accessibility announcement informing user that text has been copied [CHAR LIMIT=NONE] -->
+    <string name="clipboard_text_copied">Text copied</string>
+    <!-- Accessibility announcement informing user that text has been copied [CHAR LIMIT=NONE] -->
+    <string name="clipboard_image_copied">Image copied</string>
+    <!-- Accessibility announcement informing user that something has been copied [CHAR LIMIT=NONE] -->
+    <string name="clipboard_content_copied">Content copied</string>
 
     <!-- Generic "add" string [CHAR LIMIT=NONE] -->
     <string name="add">Add</string>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java
index f65d82a..acd4222 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListener.java
@@ -61,7 +61,7 @@
         onActivityLaunchOnSecondaryDisplayRerouted();
     }
 
-    default void onTaskProfileLocked(int taskId, int userId) { }
+    default void onTaskProfileLocked(RunningTaskInfo taskInfo) { }
     default void onTaskCreated(int taskId, ComponentName componentName) { }
     default void onTaskRemoved(int taskId) { }
     default void onTaskMovedToFront(int taskId) { }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
index b5019b4..2fd5aae 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java
@@ -211,8 +211,8 @@
         }
 
         @Override
-        public void onTaskProfileLocked(int taskId, int userId) {
-            mHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskId, userId).sendToTarget();
+        public void onTaskProfileLocked(RunningTaskInfo taskInfo) {
+            mHandler.obtainMessage(ON_TASK_PROFILE_LOCKED, taskInfo).sendToTarget();
         }
 
         @Override
@@ -357,8 +357,9 @@
                         break;
                     }
                     case ON_TASK_PROFILE_LOCKED: {
+                        final RunningTaskInfo info = (RunningTaskInfo) msg.obj;
                         for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) {
-                            mTaskStackListeners.get(i).onTaskProfileLocked(msg.arg1, msg.arg2);
+                            mTaskStackListeners.get(i).onTaskProfileLocked(info);
                         }
                         break;
                     }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
index 804d146..12fa401 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostViewController.java
@@ -71,13 +71,16 @@
                 public void onTrustGrantedWithFlags(int flags, int userId) {
                     if (userId != KeyguardUpdateMonitor.getCurrentUser()) return;
                     boolean bouncerVisible = mView.isVisibleToUser();
+                    boolean temporaryAndRenewable =
+                            (flags & TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE)
+                            != 0;
                     boolean initiatedByUser =
                             (flags & TrustAgentService.FLAG_GRANT_TRUST_INITIATED_BY_USER) != 0;
                     boolean dismissKeyguard =
                             (flags & TrustAgentService.FLAG_GRANT_TRUST_DISMISS_KEYGUARD) != 0;
 
                     if (initiatedByUser || dismissKeyguard) {
-                        if (mViewMediatorCallback.isScreenOn()
+                        if ((mViewMediatorCallback.isScreenOn() || temporaryAndRenewable)
                                 && (bouncerVisible || dismissKeyguard)) {
                             if (!bouncerVisible) {
                                 // The trust agent dismissed the keyguard without the user proving
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
index 5c9f5db..2cc5ccdc 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java
@@ -32,6 +32,7 @@
 import android.content.Context;
 import android.graphics.Insets;
 import android.graphics.Rect;
+import android.os.Trace;
 import android.util.AttributeSet;
 import android.view.WindowInsetsAnimationControlListener;
 import android.view.WindowInsetsAnimationController;
@@ -44,6 +45,7 @@
 
 import com.android.internal.widget.LockscreenCredential;
 import com.android.internal.widget.TextViewInputDisabler;
+import com.android.systemui.DejankUtils;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 /**
@@ -194,9 +196,17 @@
 
                             @Override
                             public void onAnimationEnd(Animator animation) {
-                                controller.finish(false);
-                                runOnFinishImeAnimationRunnable();
-                                finishRunnable.run();
+                                // Run this in the next frame since it results in a slow binder call
+                                // to InputMethodManager#hideSoftInput()
+                                DejankUtils.postAfterTraversal(() -> {
+                                    Trace.beginSection("KeyguardPasswordView#onAnimationEnd");
+                                    // // TODO(b/230620476): Make hideSoftInput oneway
+                                    // controller.finish() eventually calls hideSoftInput
+                                    controller.finish(false);
+                                    runOnFinishImeAnimationRunnable();
+                                    finishRunnable.run();
+                                    Trace.endSection();
+                                });
                             }
                         });
                         anim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index f8c0590..cce516d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -15,6 +15,8 @@
  */
 package com.android.keyguard;
 
+import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE;
+import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.systemBars;
 import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
@@ -32,6 +34,7 @@
 import android.animation.ValueAnimator;
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -676,7 +679,11 @@
                         attempts, remaining);
                 break;
             case USER_TYPE_WORK_PROFILE:
-                message = mContext.getString(R.string.kg_failed_attempts_almost_at_erase_profile,
+                message = mContext.getSystemService(DevicePolicyManager.class).getResources()
+                        .getString(KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE,
+                                () -> mContext.getString(
+                                        R.string.kg_failed_attempts_almost_at_erase_profile,
+                                        attempts, remaining),
                         attempts, remaining);
                 break;
         }
@@ -695,7 +702,10 @@
                         attempts);
                 break;
             case USER_TYPE_WORK_PROFILE:
-                message = mContext.getString(R.string.kg_failed_attempts_now_erasing_profile,
+                message = mContext.getSystemService(DevicePolicyManager.class).getResources()
+                        .getString(KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE,
+                                () -> mContext.getString(
+                                        R.string.kg_failed_attempts_now_erasing_profile, attempts),
                         attempts);
                 break;
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index e0f1b65..13690f3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2417,7 +2417,7 @@
         // Triggers:
         final boolean triggerActiveUnlockForAssistant = shouldTriggerActiveUnlockForAssistant();
         final boolean awakeKeyguard = mBouncerFullyShown || mUdfpsBouncerShowing
-                || (mKeyguardIsVisible && mDeviceInteractive && !mGoingToSleep
+                || (mKeyguardIsVisible && !mGoingToSleep
                 && mStatusBarState != StatusBarState.SHADE_LOCKED);
 
         // Gates:
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index d79b145..680b8bd 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -123,7 +123,7 @@
     private float mHeightPixels;
     private float mWidthPixels;
     private int mBottomPaddingPx;
-    private int mScaledPaddingPx;
+    private int mDefaultPaddingPx;
 
     private boolean mShowUnlockIcon;
     private boolean mShowLockIcon;
@@ -188,7 +188,6 @@
     protected void onViewAttached() {
         updateIsUdfpsEnrolled();
         updateConfiguration();
-        updateLockIconLocation();
         updateKeyguardShowing();
         mUserUnlockedWithBiometric = false;
 
@@ -340,25 +339,27 @@
         mWidthPixels = bounds.right;
         mHeightPixels = bounds.bottom;
         mBottomPaddingPx = getResources().getDimensionPixelSize(R.dimen.lock_icon_margin_bottom);
+        mDefaultPaddingPx =
+                getResources().getDimensionPixelSize(R.dimen.lock_icon_padding);
 
         mUnlockedLabel = mView.getContext().getResources().getString(
                 R.string.accessibility_unlock_button);
         mLockedLabel = mView.getContext()
                 .getResources().getString(R.string.accessibility_lock_icon);
+        updateLockIconLocation();
     }
 
     private void updateLockIconLocation() {
+        final float scaleFactor = mAuthController.getScaleFactor();
+        final int scaledPadding = (int) (mDefaultPaddingPx * scaleFactor);
         if (mUdfpsSupported) {
-            final int defaultPaddingPx =
-                    getResources().getDimensionPixelSize(R.dimen.lock_icon_padding);
-            mScaledPaddingPx = (int) (defaultPaddingPx * mAuthController.getScaleFactor());
             mView.setCenterLocation(mAuthController.getUdfpsLocation(),
-                    mAuthController.getUdfpsRadius(), mScaledPaddingPx);
+                    mAuthController.getUdfpsRadius(), scaledPadding);
         } else {
             mView.setCenterLocation(
                     new PointF(mWidthPixels / 2,
-                        mHeightPixels - mBottomPaddingPx - sLockIconRadiusPx),
-                        sLockIconRadiusPx, mScaledPaddingPx);
+                        mHeightPixels - ((mBottomPaddingPx + sLockIconRadiusPx) * scaleFactor)),
+                        sLockIconRadiusPx * scaleFactor, scaledPadding);
         }
     }
 
@@ -690,7 +691,6 @@
         mExecutor.execute(() -> {
             updateIsUdfpsEnrolled();
             updateConfiguration();
-            updateLockIconLocation();
         });
     }
 
@@ -707,7 +707,7 @@
 
         @Override
         public void onUdfpsLocationChanged() {
-            updateLockIconLocation();
+            updateUdfpsConfig();
         }
     };
 
diff --git a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt
index e51a63f..032a27a 100644
--- a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt
+++ b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt
@@ -54,7 +54,7 @@
 
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     @JvmField val displayInfo = DisplayInfo()
-    @JvmField protected var pendingRotationChange = false
+    @JvmField protected var pendingConfigChange = false
     @JvmField protected val paint = Paint()
     @JvmField protected val cutoutPath = Path()
 
@@ -145,7 +145,7 @@
 
     @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     open fun updateCutout() {
-        if (pendingRotationChange) {
+        if (pendingConfigChange) {
             return
         }
         cutoutPath.reset()
@@ -225,7 +225,7 @@
     }
 
     protected open fun updateProtectionBoundingPath() {
-        if (pendingRotationChange) {
+        if (pendingConfigChange) {
             return
         }
         val m = Matrix()
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index dd31218..685c585 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -53,6 +53,7 @@
 import android.util.DisplayUtils;
 import android.util.Log;
 import android.util.Size;
+import android.view.Display;
 import android.view.DisplayCutout;
 import android.view.DisplayCutout.BoundsPosition;
 import android.view.DisplayInfo;
@@ -151,12 +152,13 @@
     private SettingObserver mColorInversionSetting;
     private DelayableExecutor mExecutor;
     private Handler mHandler;
-    boolean mPendingRotationChange;
+    boolean mPendingConfigChange;
     @VisibleForTesting
     String mDisplayUniqueId;
     private int mTintColor = Color.BLACK;
     @VisibleForTesting
     protected DisplayDecorationSupport mHwcScreenDecorationSupport;
+    private Display.Mode mDisplayMode;
 
     private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback =
             new CameraAvailabilityListener.CameraTransitionCallback() {
@@ -324,6 +326,7 @@
         mWindowManager = mContext.getSystemService(WindowManager.class);
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         mRotation = mContext.getDisplay().getRotation();
+        mDisplayMode = mContext.getDisplay().getMode();
         mDisplayUniqueId = mContext.getDisplay().getUniqueId();
         mRoundedCornerResDelegate = new RoundedCornerResDelegate(mContext.getResources(),
                 mDisplayUniqueId);
@@ -349,8 +352,10 @@
             @Override
             public void onDisplayChanged(int displayId) {
                 final int newRotation = mContext.getDisplay().getRotation();
+                final Display.Mode newDisplayMode = mContext.getDisplay().getMode();
                 if ((mOverlays != null || mScreenDecorHwcWindow != null)
-                        && mRotation != newRotation) {
+                        && (mRotation != newRotation
+                        || displayModeChanged(mDisplayMode, newDisplayMode))) {
                     // We cannot immediately update the orientation. Otherwise
                     // WindowManager is still deferring layout until it has finished dispatching
                     // the config changes, which may cause divergence between what we draw
@@ -358,10 +363,16 @@
                     // Instead we wait until either:
                     // - we are trying to redraw. This because WM resized our window and told us to.
                     // - the config change has been dispatched, so WM is no longer deferring layout.
-                    mPendingRotationChange = true;
+                    mPendingConfigChange = true;
                     if (DEBUG) {
-                        Log.i(TAG, "Rotation changed, deferring " + newRotation + ", staying at "
-                                + mRotation);
+                        if (mRotation != newRotation) {
+                            Log.i(TAG, "Rotation changed, deferring " + newRotation
+                                            + ", staying at " + mRotation);
+                        }
+                        if (displayModeChanged(mDisplayMode, newDisplayMode)) {
+                            Log.i(TAG, "Resolution changed, deferring " + newDisplayMode
+                                    + ", staying at " + mDisplayMode);
+                        }
                     }
 
                     if (mOverlays != null) {
@@ -369,7 +380,8 @@
                             if (mOverlays[i] != null) {
                                 final ViewGroup overlayView = mOverlays[i].getRootView();
                                 overlayView.getViewTreeObserver().addOnPreDrawListener(
-                                        new RestartingPreDrawListener(overlayView, i, newRotation));
+                                        new RestartingPreDrawListener(
+                                                overlayView, i, newRotation, newDisplayMode));
                             }
                         }
                     }
@@ -379,7 +391,10 @@
                                 new RestartingPreDrawListener(
                                         mScreenDecorHwcWindow,
                                         -1, // Pass -1 for views with no specific position.
-                                        newRotation));
+                                        newRotation, newDisplayMode));
+                    }
+                    if (mScreenDecorHwcLayer != null) {
+                        mScreenDecorHwcLayer.pendingConfigChange = true;
                     }
                 }
 
@@ -435,7 +450,7 @@
         };
 
         mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
-        updateOrientation();
+        updateConfiguration();
     }
 
     @Nullable
@@ -807,6 +822,17 @@
         }
     }
 
+    private static boolean displayModeChanged(Display.Mode oldMode, Display.Mode newMode) {
+        if (oldMode == null) {
+            return true;
+        }
+
+        // We purposely ignore refresh rate and id changes here, because we don't need to
+        // invalidate for those, and they can trigger the refresh rate to increase
+        return oldMode.getPhysicalWidth() != newMode.getPhysicalWidth()
+                || oldMode.getPhysicalHeight() != newMode.getPhysicalHeight();
+    }
+
     private int getOverlayWindowGravity(@BoundsPosition int pos) {
         final int rotated = getBoundPositionFromRotation(pos, mRotation);
         switch (rotated) {
@@ -913,8 +939,8 @@
 
         mExecutor.execute(() -> {
             int oldRotation = mRotation;
-            mPendingRotationChange = false;
-            updateOrientation();
+            mPendingConfigChange = false;
+            updateConfiguration();
             if (DEBUG) Log.i(TAG, "onConfigChanged from rot " + oldRotation + " to " + mRotation);
             setupDecorations();
             if (mOverlays != null) {
@@ -941,7 +967,7 @@
         pw.println("  DEBUG_DISABLE_SCREEN_DECORATIONS:" + DEBUG_DISABLE_SCREEN_DECORATIONS);
         pw.println("  mIsPrivacyDotEnabled:" + isPrivacyDotEnabled());
         pw.println("  isOnlyPrivacyDotInSwLayer:" + isOnlyPrivacyDotInSwLayer());
-        pw.println("  mPendingRotationChange:" + mPendingRotationChange);
+        pw.println("  mPendingConfigChange:" + mPendingConfigChange);
         if (mHwcScreenDecorationSupport != null) {
             pw.println("  mHwcScreenDecorationSupport:");
             pw.println("    format="
@@ -957,15 +983,23 @@
         } else {
             pw.println("  mScreenDecorHwcLayer: null");
         }
-        pw.println("  mOverlays(left,top,right,bottom)=("
-                + (mOverlays != null && mOverlays[BOUNDS_POSITION_LEFT] != null) + ","
-                + (mOverlays != null && mOverlays[BOUNDS_POSITION_TOP] != null) + ","
-                + (mOverlays != null && mOverlays[BOUNDS_POSITION_RIGHT] != null) + ","
-                + (mOverlays != null && mOverlays[BOUNDS_POSITION_BOTTOM] != null) + ")");
+        if (mOverlays != null) {
+            pw.println("  mOverlays(left,top,right,bottom)=("
+                    + (mOverlays[BOUNDS_POSITION_LEFT] != null) + ","
+                    + (mOverlays[BOUNDS_POSITION_TOP] != null) + ","
+                    + (mOverlays[BOUNDS_POSITION_RIGHT] != null) + ","
+                    + (mOverlays[BOUNDS_POSITION_BOTTOM] != null) + ")");
+
+            for (int i = BOUNDS_POSITION_LEFT; i < BOUNDS_POSITION_LENGTH; i++) {
+                if (mOverlays[i] != null) {
+                    mOverlays[i].dump(pw, getWindowTitleByPos(i));
+                }
+            }
+        }
         mRoundedCornerResDelegate.dump(pw, args);
     }
 
-    private void updateOrientation() {
+    private void updateConfiguration() {
         Preconditions.checkState(mHandler.getLooper().getThread() == Thread.currentThread(),
                 "must call on " + mHandler.getLooper().getThread()
                         + ", but was " + Thread.currentThread());
@@ -974,11 +1008,14 @@
         if (mRotation != newRotation) {
             mDotViewController.setNewRotation(newRotation);
         }
+        final Display.Mode newMod = mContext.getDisplay().getMode();
 
-        if (!mPendingRotationChange && newRotation != mRotation) {
+        if (!mPendingConfigChange
+                && (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod))) {
             mRotation = newRotation;
+            mDisplayMode = newMod;
             if (mScreenDecorHwcLayer != null) {
-                mScreenDecorHwcLayer.pendingRotationChange = false;
+                mScreenDecorHwcLayer.pendingConfigChange = false;
                 mScreenDecorHwcLayer.updateRotation(mRotation);
                 updateHwLayerRoundedCornerExistAndSize();
                 updateHwLayerRoundedCornerDrawable();
@@ -1189,7 +1226,7 @@
         @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
         @Override
         public void updateCutout() {
-            if (!isAttachedToWindow() || pendingRotationChange) {
+            if (!isAttachedToWindow() || pendingConfigChange) {
                 return;
             }
             mPosition = getBoundPositionFromRotation(mInitialPosition, mRotation);
@@ -1330,40 +1367,47 @@
 
         private final View mView;
         private final int mTargetRotation;
+        private final Display.Mode mTargetDisplayMode;
         // Pass -1 for ScreenDecorHwcLayer since it's a fullscreen window and has no specific
         // position.
         private final int mPosition;
 
         private RestartingPreDrawListener(View view, @BoundsPosition int position,
-                int targetRotation) {
+                int targetRotation, Display.Mode targetDisplayMode) {
             mView = view;
             mTargetRotation = targetRotation;
+            mTargetDisplayMode = targetDisplayMode;
             mPosition = position;
         }
 
         @Override
         public boolean onPreDraw() {
             mView.getViewTreeObserver().removeOnPreDrawListener(this);
-
-            if (mTargetRotation == mRotation) {
+            if (mTargetRotation == mRotation
+                    && !displayModeChanged(mDisplayMode, mTargetDisplayMode)) {
                 if (DEBUG) {
                     final String title = mPosition < 0 ? "ScreenDecorHwcLayer"
                             : getWindowTitleByPos(mPosition);
                     Log.i(TAG, title + " already in target rot "
-                            + mTargetRotation + ", allow draw without restarting it");
+                            + mTargetRotation + " and in target resolution "
+                            + mTargetDisplayMode.getPhysicalWidth() + "x"
+                            + mTargetDisplayMode.getPhysicalHeight()
+                            + ", allow draw without restarting it");
                 }
                 return true;
             }
 
-            mPendingRotationChange = false;
+            mPendingConfigChange = false;
             // This changes the window attributes - we need to restart the traversal for them to
             // take effect.
-            updateOrientation();
+            updateConfiguration();
             if (DEBUG) {
                 final String title = mPosition < 0 ? "ScreenDecorHwcLayer"
                         : getWindowTitleByPos(mPosition);
                 Log.i(TAG, title
-                        + " restarting listener fired, restarting draw for rot " + mRotation);
+                        + " restarting listener fired, restarting draw for rot " + mRotation
+                        + ", resolution " + mDisplayMode.getPhysicalWidth() + "x"
+                        + mDisplayMode.getPhysicalHeight());
             }
             mView.invalidate();
             return false;
@@ -1371,8 +1415,8 @@
     }
 
     /**
-     * A pre-draw listener, that validates that the rotation we draw in matches the displays
-     * rotation before continuing the draw.
+     * A pre-draw listener, that validates that the rotation and display resolution we draw in
+     * matches the display's rotation and resolution before continuing the draw.
      *
      * This is to prevent a race condition, where we have not received the display changed event
      * yet, and would thus draw in an old orientation.
@@ -1388,10 +1432,20 @@
         @Override
         public boolean onPreDraw() {
             final int displayRotation = mContext.getDisplay().getRotation();
-            if (displayRotation != mRotation && !mPendingRotationChange) {
+            final Display.Mode displayMode = mContext.getDisplay().getMode();
+            if (displayRotation != mRotation && displayModeChanged(mDisplayMode, displayMode)
+                    && !mPendingConfigChange) {
                 if (DEBUG) {
-                    Log.i(TAG, "Drawing rot " + mRotation + ", but display is at rot "
-                            + displayRotation + ". Restarting draw");
+                    if (displayRotation != mRotation) {
+                        Log.i(TAG, "Drawing rot " + mRotation + ", but display is at rot "
+                                + displayRotation + ". Restarting draw");
+                    }
+                    if (displayModeChanged(mDisplayMode, displayMode)) {
+                        Log.i(TAG, "Drawing at " + mDisplayMode.getPhysicalWidth()
+                                + "x" + mDisplayMode.getPhysicalHeight() + ", but display is at "
+                                + displayMode.getPhysicalWidth() + "x"
+                                + displayMode.getPhysicalHeight() + ". Restarting draw");
+                    }
                 }
                 mView.invalidate();
                 return false;
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index 3d0c08b..fe6dbe5 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -46,6 +46,8 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.wm.shell.animation.FlingAnimationUtils;
 
+import java.util.function.Consumer;
+
 public class SwipeHelper implements Gefingerpoken {
     static final String TAG = "com.android.systemui.SwipeHelper";
     private static final boolean DEBUG = false;
@@ -399,7 +401,7 @@
      * @param useAccelerateInterpolator Should an accelerating Interpolator be used
      * @param fixedDuration If not 0, this exact duration will be taken
      */
-    public void dismissChild(final View animView, float velocity, final Runnable endAction,
+    public void dismissChild(final View animView, float velocity, final Consumer<Boolean> endAction,
             long delay, boolean useAccelerateInterpolator, long fixedDuration,
             boolean isDismissAll) {
         final boolean canBeDismissed = mCallback.canChildBeDismissed(animView);
@@ -487,7 +489,7 @@
                     resetSwipeState();
                 }
                 if (endAction != null) {
-                    endAction.run();
+                    endAction.accept(mCancelled);
                 }
                 if (!mDisableHwLayers) {
                     animView.setLayerType(View.LAYER_TYPE_NONE, null);
diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
index 56fbe6d..6b85976 100644
--- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
@@ -102,6 +102,7 @@
             AppOpsManager.OP_PHONE_CALL_CAMERA,
             AppOpsManager.OP_SYSTEM_ALERT_WINDOW,
             AppOpsManager.OP_RECORD_AUDIO,
+            AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
             AppOpsManager.OP_PHONE_CALL_MICROPHONE,
             AppOpsManager.OP_COARSE_LOCATION,
             AppOpsManager.OP_FINE_LOCATION
@@ -375,7 +376,7 @@
             Log.w(TAG, String.format("onActiveChanged(%d,%d,%s,%s,%d,%d)", code, uid, packageName,
                     Boolean.toString(active), attributionChainId, attributionFlags));
         }
-        if (attributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE
+        if (active && attributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE
                 && attributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE
                 && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR) == 0
                 && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_TRUSTED) == 0) {
@@ -535,7 +536,8 @@
     }
 
     private boolean isOpMicrophone(int op) {
-        return op == AppOpsManager.OP_RECORD_AUDIO || op == AppOpsManager.OP_PHONE_CALL_MICROPHONE;
+        return op == AppOpsManager.OP_RECORD_AUDIO || op == AppOpsManager.OP_PHONE_CALL_MICROPHONE
+                || op == AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
     }
 
     protected class H extends Handler {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 75339aa..1739646 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -484,7 +484,13 @@
         if (mFaceProps == null || mFaceAuthSensorLocation == null) {
             return null;
         }
-        return new PointF(mFaceAuthSensorLocation.x, mFaceAuthSensorLocation.y);
+        DisplayInfo displayInfo = new DisplayInfo();
+        mContext.getDisplay().getDisplayInfo(displayInfo);
+        final float scaleFactor = android.util.DisplayUtils.getPhysicalPixelDisplaySizeRatio(
+                mStableDisplaySize.x, mStableDisplaySize.y, displayInfo.getNaturalWidth(),
+                displayInfo.getNaturalHeight());
+        return new PointF(mFaceAuthSensorLocation.x * scaleFactor,
+                mFaceAuthSensorLocation.y * scaleFactor);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
index 5cfbdb0..742c65c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
@@ -192,4 +192,9 @@
      * Called on touches outside of the view if listenForTouchesOutsideView returns true
      */
     open fun onTouchOutsideView() {}
+
+    /**
+     * Called when a view should announce an accessibility event.
+     */
+    open fun doAnnounceForAccessibility(str: String) {}
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 7657269..903b3de 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -51,9 +51,9 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.LatencyTracker;
-import com.android.keyguard.ActiveUnlockConfig;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.biometrics.dagger.BiometricsBackground;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -77,6 +77,7 @@
 import java.util.HashSet;
 import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
@@ -108,6 +109,7 @@
     @NonNull private final LayoutInflater mInflater;
     private final WindowManager mWindowManager;
     private final DelayableExecutor mFgExecutor;
+    @NonNull private final Executor mBiometricExecutor;
     @NonNull private final PanelExpansionStateManager mPanelExpansionStateManager;
     @NonNull private final StatusBarStateController mStatusBarStateController;
     @NonNull private final KeyguardStateController mKeyguardStateController;
@@ -382,6 +384,27 @@
                 && mOverlayParams.getSensorBounds().contains((int) x, (int) y);
     }
 
+    private Point getTouchInNativeCoordinates(@NonNull MotionEvent event, int idx) {
+        Point portraitTouch = new Point(
+                (int) event.getRawX(idx),
+                (int) event.getRawY(idx)
+        );
+        final int rot = mOverlayParams.getRotation();
+        if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
+            RotationUtils.rotatePoint(portraitTouch,
+                    RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
+                    mOverlayParams.getLogicalDisplayWidth(),
+                    mOverlayParams.getLogicalDisplayHeight()
+            );
+        }
+
+        // Scale the coordinates to native resolution.
+        final float scale = mOverlayParams.getScaleFactor();
+        portraitTouch.x = (int) (portraitTouch.x / scale);
+        portraitTouch.y = (int) (portraitTouch.y / scale);
+        return portraitTouch;
+    }
+
     @VisibleForTesting
     boolean onTouch(long requestId, @NonNull MotionEvent event, boolean fromUdfpsView) {
         if (mOverlay == null) {
@@ -435,6 +458,7 @@
                     mKeyguardViewManager.notifyKeyguardAuthenticated(false /* strongAuth */);
                     mAttemptedToDismissKeyguard = true;
                 }
+
                 Trace.endSection();
                 break;
 
@@ -458,6 +482,8 @@
                         mAttemptedToDismissKeyguard = true;
                         break;
                     }
+                    // Map the touch to portrait mode if the device is in landscape mode.
+                    final Point scaledTouch = getTouchInNativeCoordinates(event, idx);
                     if (actionMoveWithinSensorArea) {
                         if (mVelocityTracker == null) {
                             // touches could be injected, so the velocity tracker may not have
@@ -478,28 +504,13 @@
                         final long sinceLastLog = mSystemClock.elapsedRealtime() - mTouchLogTime;
                         if (!isIlluminationRequested && !mAcquiredReceived
                                 && !exceedsVelocityThreshold) {
-                            // Map the touch to portrait mode if the device is in landscape mode.
-                            Point portraitTouch = new Point(
-                                    (int) event.getRawX(idx),
-                                    (int) event.getRawY(idx)
-                            );
-                            final int rot = mOverlayParams.getRotation();
-                            if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
-                                RotationUtils.rotatePoint(portraitTouch,
-                                        RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
-                                        mOverlayParams.getLogicalDisplayWidth(),
-                                        mOverlayParams.getLogicalDisplayHeight()
-                                );
-                            }
 
-                            // Scale the coordinates to native resolution.
                             final float scale = mOverlayParams.getScaleFactor();
-                            int scaledX = (int) (portraitTouch.x / scale);
-                            int scaledY = (int) (portraitTouch.y / scale);
                             float scaledMinor = minor / scale;
                             float scaledMajor = major / scale;
 
-                            onFingerDown(requestId, scaledX, scaledY, scaledMinor, scaledMajor);
+                            onFingerDown(requestId, scaledTouch.x, scaledTouch.y, scaledMinor,
+                                    scaledMajor);
                             Log.v(TAG, "onTouch | finger down: " + touchInfo);
                             mTouchLogTime = mSystemClock.elapsedRealtime();
                             mPowerManager.userActivity(mSystemClock.uptimeMillis(),
@@ -512,6 +523,24 @@
                     } else {
                         Log.v(TAG, "onTouch | finger outside");
                         onFingerUp(requestId, udfpsView);
+                        // Maybe announce for accessibility.
+                        mFgExecutor.execute(() -> {
+                            if (mOverlay == null) {
+                                Log.e(TAG, "touch outside sensor area received"
+                                        + "but serverRequest is null");
+                                return;
+                            }
+                            // Scale the coordinates to native resolution.
+                            final float scale = mOverlayParams.getScaleFactor();
+                            final float scaledSensorX =
+                                    mOverlayParams.getSensorBounds().centerX() / scale;
+                            final float scaledSensorY =
+                                    mOverlayParams.getSensorBounds().centerY() / scale;
+
+                            mOverlay.onTouchOutsideOfSensorArea(
+                                    scaledTouch.x, scaledTouch.y, scaledSensorX, scaledSensorY,
+                                    mOverlayParams.getRotation());
+                        });
                     }
                 }
                 Trace.endSection();
@@ -577,7 +606,8 @@
             @NonNull LatencyTracker latencyTracker,
             @NonNull ActivityLaunchAnimator activityLaunchAnimator,
             @NonNull Optional<AlternateUdfpsTouchProvider> aternateTouchProvider,
-            @NonNull BroadcastSender broadcastSender) {
+            @NonNull BroadcastSender broadcastSender,
+            @BiometricsBackground Executor biometricsExecutor) {
         mContext = context;
         mExecution = execution;
         mVibrator = vibrator;
@@ -608,6 +638,7 @@
         mActivityLaunchAnimator = activityLaunchAnimator;
         mAlternateTouchProvider = aternateTouchProvider.orElse(null);
         mBroadcastSender = broadcastSender;
+        mBiometricExecutor = biometricsExecutor;
 
         mOrientationListener = new BiometricDisplayListener(
                 context,
@@ -808,28 +839,29 @@
             if (!mKeyguardUpdateMonitor.isFaceDetectionRunning()) {
                 mKeyguardUpdateMonitor.requestFaceAuth(/* userInitiatedRequest */ false);
             }
-
-            mKeyguardUpdateMonitor.requestActiveUnlock(
-                    ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.UNLOCK_INTENT,
-                    "udfpsFingerDown");
         }
         mOnFingerDown = true;
         if (mAlternateTouchProvider != null) {
-            mAlternateTouchProvider.onPointerDown(requestId, x, y, minor, major);
+            mBiometricExecutor.execute(() -> {
+                mAlternateTouchProvider.onPointerDown(requestId, x, y, minor, major);
+            });
         } else {
             mFingerprintManager.onPointerDown(requestId, mSensorId, x, y, minor, major);
         }
         Trace.endAsyncSection("UdfpsController.e2e.onPointerDown", 0);
-
         final UdfpsView view = mOverlay.getOverlayView();
         if (view != null) {
             view.startIllumination(() -> {
                 if (mAlternateTouchProvider != null) {
-                    mAlternateTouchProvider.onUiReady();
+                    mBiometricExecutor.execute(() -> {
+                        mAlternateTouchProvider.onUiReady();
+                        mLatencyTracker.onActionEnd(LatencyTracker.ACTION_UDFPS_ILLUMINATE);
+                    });
                 } else {
                     mFingerprintManager.onUiReady(requestId, mSensorId);
+                    mLatencyTracker.onActionEnd(LatencyTracker.ACTION_UDFPS_ILLUMINATE);
+
                 }
-                mLatencyTracker.onActionEnd(LatencyTracker.ACTION_UDFPS_ILLUMINATE);
             });
         }
 
@@ -844,7 +876,9 @@
         mAcquiredReceived = false;
         if (mOnFingerDown) {
             if (mAlternateTouchProvider != null) {
-                mAlternateTouchProvider.onPointerUp(requestId);
+                mBiometricExecutor.execute(() -> {
+                    mAlternateTouchProvider.onPointerUp(requestId);
+                });
             } else {
                 mFingerprintManager.onPointerUp(requestId, mSensorId);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index faa93a5..37db2bd 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -130,6 +130,8 @@
     val animationViewController: UdfpsAnimationViewController<*>?
         get() = overlayView?.animationViewController
 
+    private var touchExplorationEnabled = false
+
     /** Show the overlay or return false and do nothing if it is already showing. */
     @SuppressLint("ClickableViewAccessibility")
     fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean {
@@ -154,14 +156,16 @@
                     }
 
                     windowManager.addView(this, coreLayoutParams.updateDimensions(animation))
-
+                    touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
                     overlayTouchListener = TouchExplorationStateChangeListener {
                         if (accessibilityManager.isTouchExplorationEnabled) {
                             setOnHoverListener { v, event -> onTouch(v, event, true) }
                             setOnTouchListener(null)
+                            touchExplorationEnabled = true
                         } else {
                             setOnHoverListener(null)
                             setOnTouchListener { v, event -> onTouch(v, event, true) }
+                            touchExplorationEnabled = false
                         }
                     }
                     accessibilityManager.addTouchExplorationStateChangeListener(
@@ -179,7 +183,7 @@
         return false
     }
 
-    private fun inflateUdfpsAnimation(
+    fun inflateUdfpsAnimation(
         view: UdfpsView,
         controller: UdfpsController
     ): UdfpsAnimationViewController<*>? {
@@ -278,6 +282,88 @@
         enrollHelper?.onEnrollmentHelp()
     }
 
+    /**
+     * This function computes the angle of touch relative to the sensor and maps
+     * the angle to a list of help messages which are announced if accessibility is enabled.
+     *
+     */
+    fun onTouchOutsideOfSensorArea(
+        touchX: Float,
+        touchY: Float,
+        sensorX: Float,
+        sensorY: Float,
+        rotation: Int
+    ) {
+
+        if (!touchExplorationEnabled) {
+            return
+        }
+        val touchHints =
+            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
+        if (touchHints.size != 4) {
+            Log.e(TAG, "expected exactly 4 touch hints, got $touchHints.size?")
+            return
+        }
+        val theStr = onTouchOutsideOfSensorAreaImpl(touchX, touchY, sensorX, sensorY, rotation)
+        Log.v(TAG, "Announcing touch outside : " + theStr)
+        animationViewController?.doAnnounceForAccessibility(theStr)
+    }
+
+    /**
+     * This function computes the angle of touch relative to the sensor and maps
+     * the angle to a list of help messages which are announced if accessibility is enabled.
+     *
+     * There are 4 quadrants of the circle (90 degree arcs)
+     *
+     * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left"
+     * [45,  135)            -> touchHints[1] = "Move Fingerprint down"
+     * And so on.
+     */
+    fun onTouchOutsideOfSensorAreaImpl(
+        touchX: Float,
+        touchY: Float,
+        sensorX: Float,
+        sensorY: Float,
+        rotation: Int
+    ): String {
+        val touchHints =
+            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
+
+        val xRelativeToSensor = touchX - sensorX
+        // Touch coordinates are with respect to the upper left corner, so reverse
+        // this calculation
+        val yRelativeToSensor = sensorY - touchY
+
+        var angleInRad =
+            Math.atan2(yRelativeToSensor.toDouble(), xRelativeToSensor.toDouble())
+        // If the radians are negative, that means we are counting clockwise.
+        // So we need to add 360 degrees
+        if (angleInRad < 0.0) {
+            angleInRad += 2.0 * Math.PI
+        }
+        // rad to deg conversion
+        val degrees = Math.toDegrees(angleInRad)
+
+        val degreesPerBucket = 360.0 / touchHints.size
+        val halfBucketDegrees = degreesPerBucket / 2.0
+        // The mapping should be as follows
+        // [315, 360] && [0, 45] -> 0
+        // [45, 135]             -> 1
+        var index = (((degrees + halfBucketDegrees) % 360) / degreesPerBucket).toInt()
+        index %= touchHints.size
+
+        // A rotation of 90 degrees corresponds to increasing the index by 1.
+        if (rotation == Surface.ROTATION_90) {
+            index = (index + 1) % touchHints.size
+        }
+
+        if (rotation == Surface.ROTATION_270) {
+            index = (index + 3) % touchHints.size
+        }
+
+        return touchHints[index]
+    }
+
     /** Cancel this request. */
     fun cancel() {
         try {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
index 2ed60e5..0b7bdde 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java
@@ -99,4 +99,10 @@
     public int getPaddingY() {
         return mEnrollProgressBarRadius;
     }
+
+    @Override
+    public void doAnnounceForAccessibility(String str) {
+        mView.announceForAccessibility(str);
+    }
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index 5055056..c3dd29e 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -302,6 +302,7 @@
             mExitAnimator.cancel();
         }
         reset();
+        String accessibilityAnnouncement;
 
         boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
                 && clipData.getDescription().getExtras()
@@ -310,6 +311,7 @@
             showTextPreview(
                     mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
                     mTextPreview);
+            accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
         } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
             ClipData.Item item = clipData.getItemAt(0);
             if (item.getTextLinks() != null) {
@@ -321,13 +323,18 @@
             } else {
                 showEditableText(item.getText(), false);
             }
+            accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied);
         } else if (clipData.getItemAt(0).getUri() != null) {
-            // How to handle non-image URIs?
-            showEditableImage(clipData.getItemAt(0).getUri(), isSensitive);
+            if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) {
+                accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied);
+            } else {
+                accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
+            }
         } else {
             showTextPreview(
                     mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
                     mTextPreview);
+            accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
         }
         Intent remoteCopyIntent = getRemoteCopyIntent(clipData);
         // Only show remote copy if it's available.
@@ -344,7 +351,12 @@
         } else {
             mRemoteCopyChip.setVisibility(View.GONE);
         }
-        withWindowAttached(() -> mView.post(this::animateIn));
+        withWindowAttached(() -> {
+            updateInsets(
+                    mWindowManager.getCurrentWindowMetrics().getWindowInsets());
+            mView.post(this::animateIn);
+            mView.announceForAccessibility(accessibilityAnnouncement);
+        });
         mTimeoutHandler.resetTimeout();
     }
 
@@ -476,33 +488,46 @@
         textView.setOnClickListener(listener);
     }
 
-    private void showEditableImage(Uri uri, boolean isSensitive) {
-        mEditChip.setAlpha(1f);
-        mActionContainerBackground.setVisibility(View.VISIBLE);
+    private boolean tryShowEditableImage(Uri uri, boolean isSensitive) {
         View.OnClickListener listener = v -> editImage(uri);
+        ContentResolver resolver = mContext.getContentResolver();
+        String mimeType = resolver.getType(uri);
+        boolean isEditableImage = mimeType != null && mimeType.startsWith("image");
         if (isSensitive) {
             showSinglePreview(mHiddenImagePreview);
-            mHiddenImagePreview.setOnClickListener(listener);
-        } else {
-            showSinglePreview(mImagePreview);
-            ContentResolver resolver = mContext.getContentResolver();
+            if (isEditableImage) {
+                mHiddenImagePreview.setOnClickListener(listener);
+            }
+        } else if (isEditableImage) { // if the MIMEtype is image, try to load
             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);
+                showSinglePreview(mImagePreview);
                 mImagePreview.setImageBitmap(thumbnail);
+                mImagePreview.setOnClickListener(listener);
             } catch (IOException e) {
                 Log.e(TAG, "Thumbnail loading failed", e);
                 showTextPreview(
                         mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
                         mTextPreview);
+                isEditableImage = false;
             }
-            mImagePreview.setOnClickListener(listener);
+        } else {
+            showTextPreview(
+                    mContext.getResources().getString(R.string.clipboard_overlay_text_copied),
+                    mTextPreview);
         }
-        mEditChip.setOnClickListener(listener);
-        mEditChip.setContentDescription(
-                mContext.getString(R.string.clipboard_edit_image_description));
+        if (isEditableImage) {
+            mEditChip.setVisibility(View.VISIBLE);
+            mEditChip.setAlpha(1f);
+            mActionContainerBackground.setVisibility(View.VISIBLE);
+            mEditChip.setOnClickListener(listener);
+            mEditChip.setContentDescription(
+                    mContext.getString(R.string.clipboard_edit_image_description));
+        }
+        return isEditableImage;
     }
 
     private Intent getRemoteCopyIntent(ClipData clipData) {
@@ -526,6 +551,9 @@
     }
 
     private void animateOut() {
+        if (mExitAnimator != null && mExitAnimator.isRunning()) {
+            return;
+        }
         Animator anim = getExitAnimation();
         anim.addListener(new AnimatorListenerAdapter() {
             private boolean mCancelled;
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
index a4f9f3a..6a9aaf8 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
@@ -472,7 +472,6 @@
         updateContentDescription()
 
         status.setTextColor(color)
-        chevronIcon.imageTintList = color
 
         control?.getCustomIcon()?.let {
             icon.setImageIcon(it)
@@ -495,10 +494,13 @@
                 icon.imageTintList = color
             }
         }
+
+        chevronIcon.imageTintList = icon.imageTintList
     }
 
     private fun setEnabled(enabled: Boolean) {
-        status.setEnabled(enabled)
-        icon.setEnabled(enabled)
+        status.isEnabled = enabled
+        icon.isEnabled = enabled
+        chevronIcon.isEnabled = enabled
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/decor/OverlayWindow.kt b/packages/SystemUI/src/com/android/systemui/decor/OverlayWindow.kt
index d775ad3..3c0748e 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/OverlayWindow.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/OverlayWindow.kt
@@ -16,11 +16,13 @@
 package com.android.systemui.decor
 
 import android.annotation.IdRes
+import android.annotation.NonNull
 import android.content.Context
 import android.view.Surface
 import android.view.View
 import android.view.ViewGroup
 import com.android.systemui.RegionInterceptingFrameLayout
+import java.io.PrintWriter
 
 class OverlayWindow(private val context: Context) {
 
@@ -100,4 +102,13 @@
             }
         }
     }
+
+    fun dump(@NonNull pw: PrintWriter, name: String) {
+        pw.println("  $name=")
+        pw.println("    rootView=$rootView")
+        for (i in 0 until rootView.childCount) {
+            val child = rootView.getChildAt(i)
+            pw.println("    child[$i]=$child")
+        }
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index e963c39..afa7d5e 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -151,7 +151,7 @@
     /***************************************/
     // 900 - media
     public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, true);
-    public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, true);
+    public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, false);
     public static final BooleanFlag MEDIA_NEARBY_DEVICES = new BooleanFlag(903, true);
     public static final BooleanFlag MEDIA_MUTE_AWAIT = new BooleanFlag(904, true);
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index 404e531..6dfc5e1 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.NotificationShadeWindowController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -142,7 +143,8 @@
     private val keyguardViewController: KeyguardViewController,
     private val featureFlags: FeatureFlags,
     private val biometricUnlockControllerLazy: Lazy<BiometricUnlockController>,
-    private val statusBarStateController: SysuiStatusBarStateController
+    private val statusBarStateController: SysuiStatusBarStateController,
+    private val notificationShadeWindowController: NotificationShadeWindowController
 ) : KeyguardStateController.Callback, ISysuiUnlockAnimationController.Stub() {
 
     interface KeyguardUnlockAnimationListener {
@@ -362,6 +364,9 @@
      */
     fun canPerformInWindowLauncherAnimations(): Boolean {
         return isNexusLauncherUnderneath() &&
+                // If the launcher is underneath, but we're about to launch an activity, don't do
+                // the animations since they won't be visible.
+                !notificationShadeWindowController.isLaunchingActivity &&
                 launcherUnlockController != null &&
                 !keyguardStateController.isDismissingFromSwipe &&
                 // Temporarily disable for foldables since foldable launcher has two first pages,
@@ -413,7 +418,6 @@
             (lockscreenSmartspace as BcSmartspaceDataPlugin.SmartspaceView?)?.selectedPage ?: -1
 
         try {
-
             // Let the launcher know to prepare for this animation.
             launcherUnlockController?.prepareForUnlock(
                 willUnlockWithSmartspaceTransition, /* willAnimateSmartspace */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
index e6b650b..546a409 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivity.java
@@ -17,23 +17,22 @@
 package com.android.systemui.keyguard;
 
 import static android.app.ActivityManager.TaskDescription;
-import static android.app.admin.DevicePolicyResources.Strings.SystemUi.WORK_LOCK_ACCESSIBILITY;
 
-import android.annotation.ColorInt;
 import android.annotation.UserIdInt;
 import android.app.Activity;
 import android.app.ActivityOptions;
 import android.app.KeyguardManager;
 import android.app.PendingIntent;
-import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.graphics.Color;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.UserHandle;
-import android.view.View;
+import android.os.UserManager;
+import android.widget.ImageView;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.R;
@@ -52,12 +51,6 @@
 public class WorkLockActivity extends Activity {
     private static final String TAG = "WorkLockActivity";
 
-    /**
-     * Contains a {@link TaskDescription} for the activity being covered.
-     */
-    static final String EXTRA_TASK_DESCRIPTION =
-            "com.android.systemui.keyguard.extra.TASK_DESCRIPTION";
-
     private static final int REQUEST_CODE_CONFIRM_CREDENTIALS = 1;
 
     /**
@@ -65,12 +58,17 @@
      * @see KeyguardManager
      */
     private KeyguardManager mKgm;
+    private UserManager mUserManager;
+    private PackageManager mPackageManager;
     private final BroadcastDispatcher mBroadcastDispatcher;
 
     @Inject
-    public WorkLockActivity(BroadcastDispatcher broadcastDispatcher) {
+    public WorkLockActivity(BroadcastDispatcher broadcastDispatcher, UserManager userManager,
+            PackageManager packageManager) {
         super();
         mBroadcastDispatcher = broadcastDispatcher;
+        mUserManager = userManager;
+        mPackageManager = packageManager;
     }
 
     @Override
@@ -91,15 +89,28 @@
         // Draw captions overlaid on the content view, so the whole window is one solid color.
         setOverlayWithDecorCaptionEnabled(true);
 
-        // Blank out the activity. When it is on-screen it will look like a Recents thumbnail with
-        // redaction switched on.
-        final DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class);
-        String contentDescription = dpm.getResources().getString(
-                WORK_LOCK_ACCESSIBILITY, () -> getString(R.string.accessibility_desc_work_lock));
-        final View blankView = new View(this);
-        blankView.setContentDescription(contentDescription);
-        blankView.setBackgroundColor(getPrimaryColor());
-        setContentView(blankView);
+        // Add background protection that contains a badged icon of the app being opened.
+        setContentView(R.layout.auth_biometric_background);
+        Drawable badgedIcon = getBadgedIcon();
+        if (badgedIcon != null) {
+            ((ImageView) findViewById(R.id.icon)).setImageDrawable(badgedIcon);
+        }
+    }
+
+    @VisibleForTesting
+    protected Drawable getBadgedIcon() {
+        String packageName = getIntent().getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+        if (!packageName.isEmpty()) {
+            try {
+                return mUserManager.getBadgedIconForUser(mPackageManager.getApplicationIcon(
+                        mPackageManager.getApplicationInfoAsUser(packageName,
+                                PackageManager.ApplicationInfoFlags.of(0), getTargetUserId())),
+                        UserHandle.of(getTargetUserId()));
+            } catch (PackageManager.NameNotFoundException e) {
+                // Unable to set the badged icon, show the background protection without an icon.
+            }
+        }
+        return null;
     }
 
     /**
@@ -208,19 +219,4 @@
     final int getTargetUserId() {
         return getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
     }
-
-    @VisibleForTesting
-    @ColorInt
-    final int getPrimaryColor() {
-        final TaskDescription taskDescription = (TaskDescription)
-                getIntent().getExtra(EXTRA_TASK_DESCRIPTION);
-        if (taskDescription != null && Color.alpha(taskDescription.getPrimaryColor()) == 255) {
-            return taskDescription.getPrimaryColor();
-        } else {
-            // No task description. Use an organization color set by the policy controller.
-            final DevicePolicyManager devicePolicyManager = (DevicePolicyManager)
-                    getSystemService(Context.DEVICE_POLICY_SERVICE);
-            return devicePolicyManager.getOrganizationColorForUser(getTargetUserId());
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
index 7585110..16817ed 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/WorkLockActivityController.java
@@ -52,22 +52,17 @@
         tscl.registerTaskStackListener(mLockListener);
     }
 
-    private void startWorkChallengeInTask(int taskId, int userId) {
-        ActivityManager.TaskDescription taskDescription = null;
-        try {
-            taskDescription = mIatm.getTaskDescription(taskId);
-        } catch (RemoteException e) {
-            Log.w(TAG, "Failed to get description for task=" + taskId);
-        }
+    private void startWorkChallengeInTask(ActivityManager.RunningTaskInfo info) {
+        String packageName = info.baseActivity != null ? info.baseActivity.getPackageName() : "";
         Intent intent = new Intent(KeyguardManager.ACTION_CONFIRM_DEVICE_CREDENTIAL_WITH_USER)
                 .setComponent(new ComponentName(mContext, WorkLockActivity.class))
-                .putExtra(Intent.EXTRA_USER_ID, userId)
-                .putExtra(WorkLockActivity.EXTRA_TASK_DESCRIPTION, taskDescription)
+                .putExtra(Intent.EXTRA_USER_ID, info.userId)
+                .putExtra(Intent.EXTRA_PACKAGE_NAME, packageName)
                 .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
                         | Intent.FLAG_ACTIVITY_CLEAR_TOP);
 
         final ActivityOptions options = ActivityOptions.makeBasic();
-        options.setLaunchTaskId(taskId);
+        options.setLaunchTaskId(info.taskId);
         options.setTaskOverlay(true, false /* canResume */);
 
         final int result = startActivityAsUser(intent, options.toBundle(), UserHandle.USER_CURRENT);
@@ -77,9 +72,9 @@
             // Starting the activity inside the task failed. We can't be sure why, so to be
             // safe just remove the whole task if it still exists.
             try {
-                mIatm.removeTask(taskId);
+                mIatm.removeTask(info.taskId);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed to get description for task=" + taskId);
+                Log.w(TAG, "Failed to get description for task=" + info.taskId);
             }
         }
     }
@@ -112,8 +107,8 @@
 
     private final TaskStackChangeListener mLockListener = new TaskStackChangeListener() {
         @Override
-        public void onTaskProfileLocked(int taskId, int userId) {
-            startWorkChallengeInTask(taskId, userId);
+        public void onTaskProfileLocked(ActivityManager.RunningTaskInfo info) {
+            startWorkChallengeInTask(info);
         }
     };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
index 25effec..db446c3 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
@@ -19,13 +19,14 @@
 import android.os.Trace
 import android.util.Log
 import com.android.systemui.log.dagger.LogModule
+import com.android.systemui.util.collection.RingBuffer
 import java.io.PrintWriter
 import java.text.SimpleDateFormat
-import java.util.ArrayDeque
 import java.util.Locale
 import java.util.concurrent.ArrayBlockingQueue
 import java.util.concurrent.BlockingQueue
 import kotlin.concurrent.thread
+import kotlin.math.max
 
 /**
  * A simple ring buffer of recyclable log messages
@@ -63,29 +64,22 @@
  *
  * Buffers are provided by [LogModule]. Instances should be created using a [LogBufferFactory].
  *
- * @param name The name of this buffer
- * @param maxLogs The maximum number of messages to keep in memory at any one time, including the
- * unused pool. Must be >= [poolSize].
- * @param poolSize The maximum amount that the size of the buffer is allowed to flex in response to
- * sequential calls to [document] that aren't immediately followed by a matching call to [push].
+ * @param name The name of this buffer, printed when the buffer is dumped and in some other
+ * situations.
+ * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start
+ * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches
+ * the maximum, it behaves like a ring buffer.
  */
 class LogBuffer @JvmOverloads constructor(
     private val name: String,
-    private val maxLogs: Int,
-    private val poolSize: Int,
+    private val maxSize: Int,
     private val logcatEchoTracker: LogcatEchoTracker,
     private val systrace: Boolean = true
 ) {
-    init {
-        if (maxLogs < poolSize) {
-            throw IllegalArgumentException("maxLogs must be greater than or equal to poolSize, " +
-                    "but maxLogs=$maxLogs < $poolSize=poolSize")
-        }
-    }
+    private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() }
 
-    private val buffer: ArrayDeque<LogMessageImpl> = ArrayDeque()
-    private val echoMessageQueue: BlockingQueue<LogMessageImpl>? =
-            if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(poolSize) else null
+    private val echoMessageQueue: BlockingQueue<LogMessage>? =
+            if (logcatEchoTracker.logInBackgroundThread) ArrayBlockingQueue(10) else null
 
     init {
         if (logcatEchoTracker.logInBackgroundThread && echoMessageQueue != null) {
@@ -104,6 +98,9 @@
     var frozen = false
         private set
 
+    private val mutable
+        get() = !frozen && maxSize > 0
+
     /**
      * Logs a message to the log buffer
      *
@@ -138,34 +135,19 @@
         initializer: LogMessage.() -> Unit,
         noinline printer: LogMessage.() -> String
     ) {
-        if (!frozen) {
-            val message = obtain(tag, level, printer)
-            initializer(message)
-            push(message)
-        }
-    }
-
-    /**
-     * Same as [log], but doesn't push the message to the buffer. Useful if you need to supply a
-     * "reason" for doing something (the thing you supply the reason to will presumably call [push]
-     * on that message at some point).
-     */
-    inline fun document(
-        tag: String,
-        level: LogLevel,
-        initializer: LogMessage.() -> Unit,
-        noinline printer: LogMessage.() -> String
-    ): LogMessage {
         val message = obtain(tag, level, printer)
         initializer(message)
-        return message
+        commit(message)
     }
 
     /**
-     * Obtains an instance of [LogMessageImpl], usually from the object pool. If the pool has been
-     * exhausted, creates a new instance.
+     * You should call [log] instead of this method.
      *
-     * In general, you should call [log] or [document] instead of this method.
+     * Obtains the next [LogMessage] from the ring buffer. If the buffer is not yet at max size,
+     * grows the buffer by one.
+     *
+     * After calling [obtain], the message will now be at the end of the buffer. The caller must
+     * store any relevant data on the message and then call [commit].
      */
     @Synchronized
     fun obtain(
@@ -173,28 +155,26 @@
         level: LogLevel,
         printer: (LogMessage) -> String
     ): LogMessageImpl {
-        val message = when {
-            frozen -> LogMessageImpl.create()
-            buffer.size > maxLogs - poolSize -> buffer.removeFirst()
-            else -> LogMessageImpl.create()
+        if (!mutable) {
+            return FROZEN_MESSAGE
         }
+        val message = buffer.advance()
         message.reset(tag, level, System.currentTimeMillis(), printer)
         return message
     }
 
     /**
-     * Pushes a message into buffer, possibly evicting an older message if the buffer is full.
+     * You should call [log] instead of this method.
+     *
+     * After acquiring a message via [obtain], call this method to signal to the buffer that you
+     * have finished filling in its data fields. The message will be echoed to logcat if
+     * necessary.
      */
     @Synchronized
-    fun push(message: LogMessage) {
-        if (frozen) {
+    fun commit(message: LogMessage) {
+        if (!mutable) {
             return
         }
-        if (buffer.size == maxLogs) {
-            Log.e(TAG, "LogBuffer $name has exceeded its pool size")
-            buffer.removeFirst()
-        }
-        buffer.add(message as LogMessageImpl)
         // Log in the background thread only if echoMessageQueue exists and has capacity (checking
         // capacity avoids the possibility of blocking this thread)
         if (echoMessageQueue != null && echoMessageQueue.remainingCapacity() > 0) {
@@ -210,7 +190,7 @@
     }
 
     /** Sends message to echo after determining whether to use Logcat and/or systrace. */
-    private fun echoToDesiredEndpoints(message: LogMessageImpl) {
+    private fun echoToDesiredEndpoints(message: LogMessage) {
         val includeInLogcat = logcatEchoTracker.isBufferLoggable(name, message.level) ||
                 logcatEchoTracker.isTagLoggable(message.tag, message.level)
         echo(message, toLogcat = includeInLogcat, toSystrace = systrace)
@@ -219,19 +199,17 @@
     /** Converts the entire buffer to a newline-delimited string */
     @Synchronized
     fun dump(pw: PrintWriter, tailLength: Int) {
-        val start = if (tailLength <= 0) { 0 } else { buffer.size - tailLength }
+        val iterationStart = if (tailLength <= 0) { 0 } else { max(0, buffer.size - tailLength) }
 
-        for ((i, message) in buffer.withIndex()) {
-            if (i >= start) {
-                dumpMessage(message, pw)
-            }
+        for (i in iterationStart until buffer.size) {
+            dumpMessage(buffer[i], pw)
         }
     }
 
     /**
-     * "Freezes" the contents of the buffer, making them immutable until [unfreeze] is called.
-     * Calls to [log], [document], [obtain], and [push] will not affect the buffer and will return
-     * dummy values if necessary.
+     * "Freezes" the contents of the buffer, making it immutable until [unfreeze] is called.
+     * Calls to [log], [obtain], and [commit] will not affect the buffer and will return dummy
+     * values if necessary.
      */
     @Synchronized
     fun freeze() {
@@ -293,3 +271,4 @@
 
 private const val TAG = "LogBuffer"
 private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
+private val FROZEN_MESSAGE = LogMessageImpl.create()
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
index cbfca25..5651399 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBufferFactory.kt
@@ -27,22 +27,21 @@
     private val logcatEchoTracker: LogcatEchoTracker
 ) {
     /* limit the size of maxPoolSize for low ram (Go) devices */
-    private fun poolLimit(maxPoolSize_requested: Int): Int {
-        if (ActivityManager.isLowRamDeviceStatic()) {
-            return minOf(maxPoolSize_requested, 20) /* low ram max log size*/
+    private fun adjustMaxSize(requestedMaxSize: Int): Int {
+        return if (ActivityManager.isLowRamDeviceStatic()) {
+            minOf(requestedMaxSize, 20) /* low ram max log size*/
         } else {
-            return maxPoolSize_requested
+            requestedMaxSize
         }
     }
 
     @JvmOverloads
     fun create(
         name: String,
-        maxPoolSize: Int,
-        flexSize: Int = 10,
+        maxSize: Int,
         systrace: Boolean = true
     ): LogBuffer {
-        val buffer = LogBuffer(name, poolLimit(maxPoolSize), flexSize, logcatEchoTracker, systrace)
+        val buffer = LogBuffer(name, adjustMaxSize(maxSize), logcatEchoTracker, systrace)
         dumpManager.registerBuffer(name, buffer)
         return buffer
     }
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt b/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
index 91734cc..40b0cdc 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogcatEchoTrackerDebug.kt
@@ -60,7 +60,7 @@
                 Settings.Global.getUriFor(BUFFER_PATH),
                 true,
                 object : ContentObserver(Handler(mainLooper)) {
-                    override fun onChange(selfChange: Boolean, uri: Uri) {
+                    override fun onChange(selfChange: Boolean, uri: Uri?) {
                         super.onChange(selfChange, uri)
                         cachedBufferLevels.clear()
                     }
@@ -70,7 +70,7 @@
                 Settings.Global.getUriFor(TAG_PATH),
                 true,
                 object : ContentObserver(Handler(mainLooper)) {
-                    override fun onChange(selfChange: Boolean, uri: Uri) {
+                    override fun onChange(selfChange: Boolean, uri: Uri?) {
                         super.onChange(selfChange, uri)
                         cachedTagLevels.clear()
                     }
@@ -110,7 +110,7 @@
     }
 
     private fun parseProp(propValue: String?): LogLevel {
-        return when (propValue?.toLowerCase()) {
+        return when (propValue?.lowercase()) {
             "verbose" -> LogLevel.VERBOSE
             "v" -> LogLevel.VERBOSE
             "debug" -> LogLevel.DEBUG
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index f72f1bb..eff025f 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -49,8 +49,7 @@
     @SysUISingleton
     @NotificationLog
     public static LogBuffer provideNotificationsLogBuffer(LogBufferFactory factory) {
-        return factory.create("NotifLog", 1000 /* maxPoolSize */,
-                10 /* flexSize */, false /* systrace */);
+        return factory.create("NotifLog", 1000 /* maxSize */, false /* systrace */);
     }
 
     /** Provides a logging buffer for all logs related to the data layer of notifications. */
@@ -74,8 +73,7 @@
     @SysUISingleton
     @NotificationSectionLog
     public static LogBuffer provideNotificationSectionLogBuffer(LogBufferFactory factory) {
-        return factory.create("NotifSectionLog", 1000 /* maxPoolSize */,
-                10 /* flexSize */, false /* systrace */);
+        return factory.create("NotifSectionLog", 1000 /* maxSize */, false /* systrace */);
     }
 
     /** Provides a logging buffer for all logs related to the data layer of notifications. */
@@ -91,8 +89,7 @@
     @SysUISingleton
     @QSLog
     public static LogBuffer provideQuickSettingsLogBuffer(LogBufferFactory factory) {
-        return factory.create("QSLog", 500 /* maxPoolSize */,
-                10 /* flexSize */, false /* systrace */);
+        return factory.create("QSLog", 500 /* maxSize */, false /* systrace */);
     }
 
     /** Provides a logging buffer for {@link com.android.systemui.broadcast.BroadcastDispatcher} */
@@ -100,8 +97,8 @@
     @SysUISingleton
     @BroadcastDispatcherLog
     public static LogBuffer provideBroadcastDispatcherLogBuffer(LogBufferFactory factory) {
-        return factory.create("BroadcastDispatcherLog", 500 /* maxPoolSize */,
-                10 /* flexSize */, false /* systrace */);
+        return factory.create("BroadcastDispatcherLog", 500 /* maxSize */,
+                false /* systrace */);
     }
 
     /** Provides a logging buffer for all logs related to Toasts shown by SystemUI. */
@@ -139,8 +136,8 @@
     @SysUISingleton
     @QSFragmentDisableLog
     public static LogBuffer provideQSFragmentDisableLogBuffer(LogBufferFactory factory) {
-        return factory.create("QSFragmentDisableFlagsLog", 10 /* maxPoolSize */,
-                10 /* flexSize */, false /* systrace */);
+        return factory.create("QSFragmentDisableFlagsLog", 10 /* maxSize */,
+                false /* systrace */);
     }
 
     /**
@@ -230,6 +227,18 @@
         return factory.create("MediaBrowser", 100);
     }
 
+    /**
+     * Provides a buffer for updates to the media carousel.
+     *
+     * See {@link com.android.systemui.media.MediaCarouselController}.
+     */
+    @Provides
+    @SysUISingleton
+    @MediaCarouselControllerLog
+    public static LogBuffer provideMediaCarouselControllerBuffer(LogBufferFactory factory) {
+        return factory.create("MediaCarouselCtlrLog", 20);
+    }
+
     /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */
     @Provides
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
new file mode 100644
index 0000000..b03655a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaCarouselControllerLog.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/**
+ * A {@link LogBuffer} for {@link com.android.systemui.media.MediaCarouselController}
+ */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface MediaCarouselControllerLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index 3483bc3..0f86871 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.util.animation.requiresRemeasuring
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.traceSection
 import java.io.PrintWriter
 import java.util.TreeMap
 import javax.inject.Inject
@@ -59,7 +60,8 @@
     falsingCollector: FalsingCollector,
     falsingManager: FalsingManager,
     dumpManager: DumpManager,
-    private val logger: MediaUiEventLogger
+    private val logger: MediaUiEventLogger,
+    private val debugLogger: MediaCarouselControllerLogger
 ) : Dumpable {
     /**
      * The current width of the carousel
@@ -424,7 +426,7 @@
         oldKey: String?,
         data: MediaData,
         isSsReactivated: Boolean
-    ): Boolean {
+    ): Boolean = traceSection("MediaCarouselController#addOrUpdatePlayer") {
         MediaPlayerData.moveIfExists(oldKey, key)
         val existingPlayer = MediaPlayerData.getMediaPlayer(key)
         val curVisibleMediaKey = MediaPlayerData.playerKeys()
@@ -439,12 +441,16 @@
             newPlayer.mediaViewHolder?.player?.setLayoutParams(lp)
             newPlayer.bindPlayer(data, key)
             newPlayer.setListening(currentlyExpanded)
-            MediaPlayerData.addMediaPlayer(key, data, newPlayer, systemClock, isSsReactivated)
+            MediaPlayerData.addMediaPlayer(
+                key, data, newPlayer, systemClock, isSsReactivated, debugLogger
+            )
             updatePlayerToState(newPlayer, noAnimation = true)
             reorderAllPlayers(curVisibleMediaKey)
         } else {
             existingPlayer.bindPlayer(data, key)
-            MediaPlayerData.addMediaPlayer(key, data, existingPlayer, systemClock, isSsReactivated)
+            MediaPlayerData.addMediaPlayer(
+                key, data, existingPlayer, systemClock, isSsReactivated, debugLogger
+            )
             if (isReorderingAllowed || shouldScrollToActivePlayer) {
                 reorderAllPlayers(curVisibleMediaKey)
             } else {
@@ -466,7 +472,7 @@
         key: String,
         data: SmartspaceMediaData,
         shouldPrioritize: Boolean
-    ) {
+    ) = traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
         if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
         if (MediaPlayerData.getMediaPlayer(key) != null) {
             Log.w(TAG, "Skip adding smartspace target in carousel")
@@ -475,7 +481,8 @@
 
         val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey()
         existingSmartspaceMediaKey?.let {
-            MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey)
+            val removedPlayer = MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey)
+            removedPlayer?.run { debugLogger.logPotentialMemoryLeak(existingSmartspaceMediaKey) }
         }
 
         val newRecs = mediaControlPanelFactory.get()
@@ -488,7 +495,9 @@
         newRecs.bindRecommendation(data)
         val curVisibleMediaKey = MediaPlayerData.playerKeys()
                 .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)
-        MediaPlayerData.addMediaRecommendation(key, data, newRecs, shouldPrioritize, systemClock)
+        MediaPlayerData.addMediaRecommendation(
+            key, data, newRecs, shouldPrioritize, systemClock, debugLogger
+        )
         updatePlayerToState(newRecs, noAnimation = true)
         reorderAllPlayers(curVisibleMediaKey)
         updatePageIndicator()
@@ -690,7 +699,7 @@
         animate: Boolean,
         duration: Long = 200,
         startDelay: Long = 0
-    ) {
+    ) = traceSection("MediaCarouselController#onDesiredLocationChanged") {
         desiredHostState?.let {
             if (this.desiredLocation != desiredLocation) {
                 // Only log an event when location changes
@@ -845,7 +854,8 @@
                     uid,
                     interactedSubcardRank,
                     interactedSubcardCardinality,
-                    receivedLatencyMillis
+                    receivedLatencyMillis,
+                    null // Media cards cannot have subcards.
             )
             /* ktlint-disable max-line-length */
             if (DEBUG) {
@@ -882,7 +892,8 @@
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.apply {
             println("keysNeedRemoval: $keysNeedRemoval")
-            println("playerKeys: ${MediaPlayerData.playerKeys()}")
+            println("dataKeys: ${MediaPlayerData.dataKeys()}")
+            println("playerSortKeys: ${MediaPlayerData.playerKeys()}")
             println("smartspaceMediaData: ${MediaPlayerData.smartspaceMediaData}")
             println("shouldPrioritizeSs: ${MediaPlayerData.shouldPrioritizeSs}")
             println("current size: $currentCarouselWidth x $currentCarouselHeight")
@@ -945,9 +956,13 @@
         data: MediaData,
         player: MediaControlPanel,
         clock: SystemClock,
-        isSsReactivated: Boolean
+        isSsReactivated: Boolean,
+        debugLogger: MediaCarouselControllerLogger? = null
     ) {
-        removeMediaPlayer(key)
+        val removedPlayer = removeMediaPlayer(key)
+        if (removedPlayer != null && removedPlayer != player) {
+            debugLogger?.logPotentialMemoryLeak(key)
+        }
         val sortKey = MediaSortKey(isSsMediaRec = false,
                 data, clock.currentTimeMillis(), isSsReactivated = isSsReactivated)
         mediaData.put(key, sortKey)
@@ -959,10 +974,14 @@
         data: SmartspaceMediaData,
         player: MediaControlPanel,
         shouldPrioritize: Boolean,
-        clock: SystemClock
+        clock: SystemClock,
+        debugLogger: MediaCarouselControllerLogger? = null
     ) {
         shouldPrioritizeSs = shouldPrioritize
-        removeMediaPlayer(key)
+        val removedPlayer = removeMediaPlayer(key)
+        if (removedPlayer != null && removedPlayer != player) {
+            debugLogger?.logPotentialMemoryLeak(key)
+        }
         val sortKey = MediaSortKey(isSsMediaRec = true,
             EMPTY.copy(isPlaying = false), clock.currentTimeMillis(), isSsReactivated = true)
         mediaData.put(key, sortKey)
@@ -970,13 +989,18 @@
         smartspaceMediaData = data
     }
 
-    fun moveIfExists(oldKey: String?, newKey: String) {
+    fun moveIfExists(
+        oldKey: String?,
+        newKey: String,
+        debugLogger: MediaCarouselControllerLogger? = null
+    ) {
         if (oldKey == null || oldKey == newKey) {
             return
         }
 
         mediaData.remove(oldKey)?.let {
-            removeMediaPlayer(newKey)
+            val removedPlayer = removeMediaPlayer(newKey)
+            removedPlayer?.run { debugLogger?.logPotentialMemoryLeak(newKey) }
             mediaData.put(newKey, it)
         }
     }
@@ -1004,6 +1028,8 @@
 
     fun mediaData() = mediaData.entries.map { e -> Triple(e.key, e.value.data, e.value.isSsMediaRec) }
 
+    fun dataKeys() = mediaData.keys
+
     fun players() = mediaPlayers.values
 
     fun playerKeys() = mediaPlayers.keys
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
new file mode 100644
index 0000000..04ebd5a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselControllerLogger.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.dagger.MediaCarouselControllerLog
+import javax.inject.Inject
+
+/** A debug logger for [MediaCarouselController]. */
+@SysUISingleton
+class MediaCarouselControllerLogger @Inject constructor(
+    @MediaCarouselControllerLog private val buffer: LogBuffer
+) {
+    /**
+     * Log that there might be a potential memory leak for the [MediaControlPanel] and/or
+     * [MediaViewController] related to [key].
+     */
+    fun logPotentialMemoryLeak(key: String) = buffer.log(
+        TAG,
+        LogLevel.DEBUG,
+        { str1 = key },
+        {
+            "Potential memory leak: " +
+                    "Removing control panel for $str1 from map without calling #onDestroy"
+        }
+    )
+}
+
+private const val TAG = "MediaCarouselCtlrLog"
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index b960142..972e93b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -28,7 +28,6 @@
 import android.app.smartspace.SmartspaceAction;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
@@ -45,6 +44,7 @@
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
 import android.os.Process;
+import android.os.Trace;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
@@ -75,6 +75,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.monet.ColorScheme;
+import com.android.systemui.monet.Style;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.shared.system.SysUiStatsLog;
@@ -396,6 +397,7 @@
         if (mMediaViewHolder == null) {
             return;
         }
+        Trace.beginSection("MediaControlPanel#bindPlayer<" + key + ">");
         mKey = key;
         mMediaData = data;
         MediaSession.Token token = data.getToken();
@@ -454,7 +456,7 @@
         bindActionButtons(data);
 
         boolean isSongUpdated = bindSongMetadata(data);
-        bindArtworkAndColors(data, isSongUpdated);
+        bindArtworkAndColors(data, key, isSongUpdated);
 
         // TODO: We don't need to refresh this state constantly, only if the state actually changed
         // to something which might impact the measurement
@@ -462,6 +464,7 @@
         if (!mMetadataAnimationHandler.isRunning()) {
             mMediaViewController.refreshState();
         }
+        Trace.endSection();
     }
 
     private void bindOutputSwitcherChip(MediaData data) {
@@ -606,7 +609,11 @@
         mRecommendationViewHolder.getRecommendations().setContentDescription(contentDescription);
     }
 
-    private void bindArtworkAndColors(MediaData data, boolean updateBackground) {
+    private void bindArtworkAndColors(MediaData data, String key, boolean updateBackground) {
+        final int traceCookie = data.hashCode();
+        final String traceName = "MediaControlPanel#bindArtworkAndColors<" + key + ">";
+        Trace.beginAsyncSection(traceName, traceCookie);
+
         final int reqId = mArtworkNextBindRequestId++;
         if (updateBackground) {
             mIsArtworkBound = false;
@@ -627,7 +634,7 @@
             if (artworkIcon != null) {
                 WallpaperColors wallpaperColors = WallpaperColors
                         .fromBitmap(artworkIcon.getBitmap());
-                mutableColorScheme = new ColorScheme(wallpaperColors, true);
+                mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
                 artwork = getScaledBackground(artworkIcon, width, height);
                 isArtworkBound = true;
             } else {
@@ -637,7 +644,8 @@
                 try {
                     Drawable icon = mContext.getPackageManager()
                             .getApplicationIcon(data.getPackageName());
-                    mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon), true);
+                    mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon), true,
+                            Style.CONTENT);
                 } catch (PackageManager.NameNotFoundException e) {
                     Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
                 }
@@ -646,7 +654,10 @@
             final ColorScheme colorScheme = mutableColorScheme;
             mMainExecutor.execute(() -> {
                 // Cancel the request if a later one arrived first
-                if (reqId < mArtworkBoundId) return;
+                if (reqId < mArtworkBoundId) {
+                    Trace.endAsyncSection(traceName, traceCookie);
+                    return;
+                }
                 mArtworkBoundId = reqId;
 
                 // Bind the album view to the artwork or a transition drawable
@@ -696,6 +707,7 @@
                         appIconView.setImageResource(R.drawable.ic_music_note);
                     }
                 }
+                Trace.endAsyncSection(traceName, traceCookie);
             });
         });
     }
@@ -988,6 +1000,9 @@
             return;
         }
 
+        Trace.beginSection(
+                "MediaControlPanel#bindRecommendation<" + data.getPackageName() + ">");
+
         mRecommendationData = data;
         mSmartspaceId = SmallHash.hash(data.getTargetId());
         mPackageName = data.getPackageName();
@@ -1001,12 +1016,14 @@
             mUid = applicationInfo.uid;
         } catch (PackageManager.NameNotFoundException e) {
             Log.w(TAG, "Fail to get media recommendation's app info", e);
+            Trace.endSection();
             return;
         }
 
         CharSequence appName = data.getAppName(mContext);
         if (appName == null) {
             Log.w(TAG, "Fail to get media recommendation's app name");
+            Trace.endSection();
             return;
         }
 
@@ -1121,6 +1138,7 @@
         if (mMetadataAnimationHandler == null || !mMetadataAnimationHandler.isRunning()) {
             mMediaViewController.refreshState();
         }
+        Trace.endSection();
     }
 
     private void fetchAndUpdateRecommendationColors(Drawable appIcon) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 0a44556..8bf2c6e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -65,6 +65,7 @@
 import com.android.systemui.util.Utils
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.traceSection
 import java.io.IOException
 import java.io.PrintWriter
 import java.util.concurrent.Executor
@@ -1031,7 +1032,11 @@
         )
     }
 
-    fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+    fun onMediaDataLoaded(
+        key: String,
+        oldKey: String?,
+        data: MediaData
+    ) = traceSection("MediaDataManager#onMediaDataLoaded") {
         Assert.isMainThread()
         if (mediaEntries.containsKey(key)) {
             // Otherwise this was removed already
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index 30771c7..ed5c193 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -45,6 +45,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.LargeScreenUtils
 import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.traceSection
 import javax.inject.Inject
 
 /**
@@ -582,7 +583,7 @@
     private fun updateDesiredLocation(
         forceNoAnimation: Boolean = false,
         forceStateUpdate: Boolean = false
-    ) {
+    ) = traceSection("MediaHierarchyManager#updateDesiredLocation") {
         val desiredLocation = calculateLocation()
         if (desiredLocation != this.desiredLocation || forceStateUpdate) {
             if (this.desiredLocation >= 0 && desiredLocation != this.desiredLocation) {
@@ -616,7 +617,10 @@
         }
     }
 
-    private fun performTransitionToNewLocation(isNewView: Boolean, animate: Boolean) {
+    private fun performTransitionToNewLocation(
+        isNewView: Boolean,
+        animate: Boolean
+    ) = traceSection("MediaHierarchyManager#performTransitionToNewLocation") {
         if (previousLocation < 0 || isNewView) {
             cancelAnimationAndApplyDesiredState()
             return
@@ -899,7 +903,7 @@
         alpha: Float,
         immediately: Boolean = false,
         clipBounds: Rect = EMPTY_RECT
-    ) {
+    ) = traceSection("MediaHierarchyManager#applyState") {
         currentBounds.set(bounds)
         currentClipping = clipBounds
         carouselAlpha = if (isCurrentlyFading()) alpha else 1.0f
@@ -922,7 +926,9 @@
         }
     }
 
-    private fun updateHostAttachment() {
+    private fun updateHostAttachment() = traceSection(
+        "MediaHierarchyManager#updateHostAttachment"
+    ) {
         var newLocation = resolveLocationForFading()
         var canUseOverlay = !isCurrentlyFading()
         if (isCrossFadeAnimatorRunning) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
index ba7c167..aea2934 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.util.animation.MeasurementOutput
+import com.android.systemui.util.traceSection
 import javax.inject.Inject
 
 /**
@@ -46,7 +47,10 @@
      * Notify that a media state for a given location has changed. Should only be called from
      * Media hosts themselves.
      */
-    fun updateHostState(@MediaLocation location: Int, hostState: MediaHostState) {
+    fun updateHostState(
+        @MediaLocation location: Int,
+        hostState: MediaHostState
+    ) = traceSection("MediaHostStatesManager#updateHostState") {
         val currentState = mediaHostStates.get(location)
         if (!hostState.equals(currentState)) {
             val newState = hostState.copy()
@@ -71,7 +75,7 @@
     fun updateCarouselDimensions(
         @MediaLocation location: Int,
         hostState: MediaHostState
-    ): MeasurementOutput {
+    ): MeasurementOutput = traceSection("MediaHostStatesManager#updateCarouselDimensions") {
         val result = MeasurementOutput(0, 0)
         for (controller in controllers) {
             val measurement = controller.getMeasurementsForState(hostState)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
index fc8d38d..d4c4f216 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
@@ -22,8 +22,11 @@
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
@@ -42,7 +45,9 @@
 class MediaTimeoutListener @Inject constructor(
     private val mediaControllerFactory: MediaControllerFactory,
     @Main private val mainExecutor: DelayableExecutor,
-    private val logger: MediaTimeoutLogger
+    private val logger: MediaTimeoutLogger,
+    statusBarStateController: SysuiStatusBarStateController,
+    private val systemClock: SystemClock
 ) : MediaDataManager.Listener {
 
     private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
@@ -62,6 +67,24 @@
      */
     lateinit var stateCallback: (String, PlaybackState) -> Unit
 
+    init {
+        statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
+            override fun onDozingChanged(isDozing: Boolean) {
+                if (!isDozing) {
+                    // Check whether any timeouts should have expired
+                    mediaListeners.forEach { (key, listener) ->
+                        if (listener.cancellation != null &&
+                                listener.expiration <= systemClock.elapsedRealtime()) {
+                            // We dozed too long - timeout now, and cancel the pending one
+                            listener.expireMediaTimeout(key, "timeout happened while dozing")
+                            listener.doTimeout()
+                        }
+                    }
+                }
+            }
+        })
+    }
+
     override fun onMediaDataLoaded(
         key: String,
         oldKey: String?,
@@ -131,6 +154,7 @@
         var lastState: PlaybackState? = null
         var resumption: Boolean? = null
         var destroyed = false
+        var expiration = Long.MAX_VALUE
 
         var mediaData: MediaData = data
             set(value) {
@@ -150,7 +174,8 @@
 
         // Resume controls may have null token
         private var mediaController: MediaController? = null
-        private var cancellation: Runnable? = null
+        var cancellation: Runnable? = null
+            private set
 
         fun Int.isPlaying() = isPlayingState(this)
         fun isPlaying() = lastState?.state?.isPlaying() ?: false
@@ -216,12 +241,9 @@
                 } else {
                     PAUSED_MEDIA_TIMEOUT
                 }
+                expiration = systemClock.elapsedRealtime() + timeout
                 cancellation = mainExecutor.executeDelayed({
-                    cancellation = null
-                    logger.logTimeout(key)
-                    timedOut = true
-                    // this event is async, so it's safe even when `dispatchEvents` is false
-                    timeoutCallback(key, timedOut)
+                    doTimeout()
                 }, timeout)
             } else {
                 expireMediaTimeout(key, "playback started - $state, $key")
@@ -232,11 +254,21 @@
             }
         }
 
-        private fun expireMediaTimeout(mediaKey: String, reason: String) {
+        fun doTimeout() {
+            cancellation = null
+            logger.logTimeout(key)
+            timedOut = true
+            expiration = Long.MAX_VALUE
+            // this event is async, so it's safe even when `dispatchEvents` is false
+            timeoutCallback(key, timedOut)
+        }
+
+        fun expireMediaTimeout(mediaKey: String, reason: String) {
             cancellation?.apply {
                 logger.logTimeoutCancelled(mediaKey, reason)
                 run()
             }
+            expiration = Long.MAX_VALUE
             cancellation = null
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
index 7eccb3b..ae62355 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.animation.TransitionLayoutController
 import com.android.systemui.util.animation.TransitionViewState
+import com.android.systemui.util.traceSection
 import javax.inject.Inject
 
 /**
@@ -371,7 +372,10 @@
      * Attach a view to this controller. This may perform measurements if it's not available yet
      * and should therefore be done carefully.
      */
-    fun attach(transitionLayout: TransitionLayout, type: TYPE) {
+    fun attach(
+        transitionLayout: TransitionLayout,
+        type: TYPE
+    ) = traceSection("MediaViewController#attach") {
         updateMediaViewControllerType(type)
         logger.logMediaLocation("attach", currentStartLocation, currentEndLocation)
         this.transitionLayout = transitionLayout
@@ -392,7 +396,9 @@
      * and all widgets know their location. Calling this method may create a measurement if we
      * don't have a cached value available already.
      */
-    fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? {
+    fun getMeasurementsForState(
+        hostState: MediaHostState
+    ): MeasurementOutput? = traceSection("MediaViewController#getMeasurementsForState") {
         val viewState = obtainViewState(hostState) ?: return null
         measurement.measuredWidth = viewState.width
         measurement.measuredHeight = viewState.height
@@ -408,7 +414,7 @@
         @MediaLocation endLocation: Int,
         transitionProgress: Float,
         applyImmediately: Boolean
-    ) {
+    ) = traceSection("MediaViewController#setCurrentState") {
         currentEndLocation = endLocation
         currentStartLocation = startLocation
         currentTransitionProgress = transitionProgress
@@ -540,7 +546,7 @@
     /**
      * Clear all existing measurements and refresh the state to match the view.
      */
-    fun refreshState() {
+    fun refreshState() = traceSection("MediaViewController#refreshState") {
         // Let's clear all of our measurements and recreate them!
         viewStates.clear()
         if (firstRefresh) {
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 8dcca3d..fa6db01 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -76,6 +76,7 @@
     private static final String PREF_NAME = "MediaOutputDialog";
     private static final String PREF_IS_LE_BROADCAST_FIRST_LAUNCH = "PrefIsLeBroadcastFirstLaunch";
     private static final boolean DEBUG = true;
+    private static final int HANDLE_BROADCAST_FAILED_DELAY = 3000;
 
     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
     private final RecyclerView.LayoutManager mLayoutManager;
@@ -119,7 +120,7 @@
                         Log.d(TAG, "onBroadcastStarted(), reason = " + reason
                                 + ", broadcastId = " + broadcastId);
                     }
-                    mMainThreadHandler.post(() -> startLeBroadcastDialog());
+                    mMainThreadHandler.post(() -> handleLeBroadcastStarted());
                 }
 
                 @Override
@@ -127,7 +128,8 @@
                     if (DEBUG) {
                         Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
                     }
-                    handleLeBroadcastStartFailed();
+                    mMainThreadHandler.postDelayed(() -> handleLeBroadcastStartFailed(),
+                            HANDLE_BROADCAST_FAILED_DELAY);
                 }
 
                 @Override
@@ -137,7 +139,7 @@
                         Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId
                                 + ", metadata = " + metadata);
                     }
-                    mMainThreadHandler.post(() -> refresh());
+                    mMainThreadHandler.post(() -> handleLeBroadcastMetadataChanged());
                 }
 
                 @Override
@@ -146,7 +148,7 @@
                         Log.d(TAG, "onBroadcastStopped(), reason = " + reason
                                 + ", broadcastId = " + broadcastId);
                     }
-                    mMainThreadHandler.post(() -> refresh());
+                    mMainThreadHandler.post(() -> handleLeBroadcastStopped());
                 }
 
                 @Override
@@ -154,7 +156,7 @@
                     if (DEBUG) {
                         Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
                     }
-                    mMainThreadHandler.post(() -> refresh());
+                    mMainThreadHandler.post(() -> handleLeBroadcastStopFailed());
                 }
 
                 @Override
@@ -163,7 +165,7 @@
                         Log.d(TAG, "onBroadcastUpdated(), reason = " + reason
                                 + ", broadcastId = " + broadcastId);
                     }
-                    mMainThreadHandler.post(() -> refresh());
+                    mMainThreadHandler.post(() -> handleLeBroadcastUpdated());
                 }
 
                 @Override
@@ -172,7 +174,7 @@
                         Log.d(TAG, "onBroadcastUpdateFailed(), reason = " + reason
                                 + ", broadcastId = " + broadcastId);
                     }
-                    mMainThreadHandler.post(() -> refresh());
+                    mMainThreadHandler.post(() -> handleLeBroadcastUpdateFailed());
                 }
 
                 @Override
@@ -225,9 +227,7 @@
         lp.setFitInsetsIgnoringVisibility(true);
         window.setAttributes(lp);
         window.setContentView(mDialogView);
-        // Sets window to a blank string to avoid talkback announce app label first when pop up,
-        // which doesn't make sense.
-        window.setTitle(EMPTY_TITLE);
+        window.setTitle(mContext.getString(R.string.media_output_dialog_accessibility_title));
 
         mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
         mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
@@ -384,10 +384,34 @@
                 Bitmap.createScaledBitmap(bitmap, size, size, false));
     }
 
-    protected void handleLeBroadcastStartFailed() {
+    public void handleLeBroadcastStarted() {
+        startLeBroadcastDialog();
+    }
+
+    public void handleLeBroadcastStartFailed() {
         mStopButton.setText(R.string.media_output_broadcast_start_failed);
         mStopButton.setEnabled(false);
-        mMainThreadHandler.postDelayed(() -> refresh(), 3000);
+        refresh();
+    }
+
+    public void handleLeBroadcastMetadataChanged() {
+        refresh();
+    }
+
+    public void handleLeBroadcastStopped() {
+        refresh();
+    }
+
+    public void handleLeBroadcastStopFailed() {
+        refresh();
+    }
+
+    public void handleLeBroadcastUpdated() {
+        refresh();
+    }
+
+    public void handleLeBroadcastUpdateFailed() {
+        refresh();
     }
 
     protected void startLeBroadcast() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
index dd4f1d6..8f06546 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -61,6 +61,10 @@
     private ImageView mBroadcastCodeEdit;
     private AlertDialog mAlertDialog;
     private TextView mBroadcastErrorMessage;
+    private int mRetryCount = 0;
+    private String mCurrentBroadcastName;
+    private String mCurrentBroadcastCode;
+    private boolean mIsStopbyUpdateBroadcastCode = false;
 
     static final int METADATA_BROADCAST_NAME = 0;
     static final int METADATA_BROADCAST_CODE = 1;
@@ -144,8 +148,6 @@
 
         //init UI component
         mBroadcastQrCodeView = getDialogView().requireViewById(R.id.qrcode_view);
-        //Set the QR code view
-        setQrCodeView();
 
         mBroadcastNotify = getDialogView().requireViewById(R.id.broadcast_info);
         mBroadcastNotify.setOnClickListener(v -> {
@@ -171,8 +173,16 @@
             launchBroadcastUpdatedDialog(true, mBroadcastCode.getText().toString());
         });
 
-        mBroadcastName.setText(getBroadcastMetadataInfo(METADATA_BROADCAST_NAME));
-        mBroadcastCode.setText(getBroadcastMetadataInfo(METADATA_BROADCAST_CODE));
+        refreshUi();
+    }
+
+    private void refreshUi() {
+        setQrCodeView();
+
+        mCurrentBroadcastName = getBroadcastMetadataInfo(METADATA_BROADCAST_NAME);
+        mCurrentBroadcastCode = getBroadcastMetadataInfo(METADATA_BROADCAST_CODE);
+        mBroadcastName.setText(mCurrentBroadcastName);
+        mBroadcastCode.setText(mCurrentBroadcastCode);
     }
 
     private void inflateBroadcastInfoArea() {
@@ -239,52 +249,99 @@
         }
 
         if (isBroadcastCode) {
-            handleBroadcastCodeUpdated(updatedString);
+            /* If the user wants to update the Broadcast Code, the Broadcast session should be
+             * stopped then used the new Broadcast code to start the Broadcast.
+             */
+            mIsStopbyUpdateBroadcastCode = true;
+            mMediaOutputController.setBroadcastCode(updatedString);
+            if (!mMediaOutputController.stopBluetoothLeBroadcast()) {
+                handleLeBroadcastStopFailed();
+                return;
+            }
         } else {
-            handleBroadcastNameUpdated(updatedString);
+            /* If the user wants to update the Broadcast Name, we don't need to stop the Broadcast
+             * session. Only use the new Broadcast name to update the broadcast session.
+             */
+            mMediaOutputController.setBroadcastName(updatedString);
+            if (!mMediaOutputController.updateBluetoothLeBroadcast()) {
+                handleLeBroadcastUpdateFailed();
+            }
         }
     }
 
-    private void handleBroadcastNameUpdated(String name) {
-        // TODO(b/230473995) Add the retry mechanism and error handling when update fails
-        String currentName = mMediaOutputController.getBroadcastName();
-        int retryCount = MAX_BROADCAST_INFO_UPDATE;
-        mMediaOutputController.setBroadcastName(name);
-        if (!mMediaOutputController.updateBluetoothLeBroadcast()) {
-            mMediaOutputController.setBroadcastName(currentName);
-            handleLeUpdateBroadcastFailed(retryCount);
+    @Override
+    public void handleLeBroadcastStarted() {
+        mRetryCount = 0;
+        if (mAlertDialog != null) {
+            mAlertDialog.dismiss();
+        }
+        refreshUi();
+    }
+
+    @Override
+    public void handleLeBroadcastStartFailed() {
+        mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode);
+        mRetryCount++;
+
+        handleUpdateFailedUi();
+    }
+
+    @Override
+    public void handleLeBroadcastMetadataChanged() {
+        refreshUi();
+    }
+
+    @Override
+    public void handleLeBroadcastUpdated() {
+        mRetryCount = 0;
+        if (mAlertDialog != null) {
+            mAlertDialog.dismiss();
+        }
+        refreshUi();
+    }
+
+    @Override
+    public void handleLeBroadcastUpdateFailed() {
+        //Change the value in shared preferences back to it original value
+        mMediaOutputController.setBroadcastName(mCurrentBroadcastName);
+        mRetryCount++;
+
+        handleUpdateFailedUi();
+    }
+
+    @Override
+    public void handleLeBroadcastStopped() {
+        if (mIsStopbyUpdateBroadcastCode) {
+            mIsStopbyUpdateBroadcastCode = false;
+            mRetryCount = 0;
+            if (!mMediaOutputController.startBluetoothLeBroadcast()) {
+                handleLeBroadcastStartFailed();
+                return;
+            }
+        } else {
+            dismiss();
         }
     }
 
-    private void handleBroadcastCodeUpdated(String newPassword) {
-        // TODO(b/230473995) Add the retry mechanism and error handling when update fails
-        String currentPassword = mMediaOutputController.getBroadcastCode();
-        int retryCount = MAX_BROADCAST_INFO_UPDATE;
-        if (!mMediaOutputController.stopBluetoothLeBroadcast()) {
-            mMediaOutputController.setBroadcastCode(currentPassword);
-            handleLeUpdateBroadcastFailed(retryCount);
-            return;
-        }
+    @Override
+    public void handleLeBroadcastStopFailed() {
+        //Change the value in shared preferences back to it original value
+        mMediaOutputController.setBroadcastCode(mCurrentBroadcastCode);
+        mRetryCount++;
 
-        mMediaOutputController.setBroadcastCode(newPassword);
-        if (!mMediaOutputController.startBluetoothLeBroadcast()) {
-            mMediaOutputController.setBroadcastCode(currentPassword);
-            handleLeUpdateBroadcastFailed(retryCount);
-            return;
-        }
-
-        mAlertDialog.dismiss();
+        handleUpdateFailedUi();
     }
 
-    private void handleLeUpdateBroadcastFailed(int retryCount) {
+    private void handleUpdateFailedUi() {
         final Button positiveBtn = mAlertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
         mBroadcastErrorMessage.setVisibility(View.VISIBLE);
-        if (retryCount < MAX_BROADCAST_INFO_UPDATE) {
+        if (mRetryCount < MAX_BROADCAST_INFO_UPDATE) {
             if (positiveBtn != null) {
                 positiveBtn.setEnabled(true);
             }
             mBroadcastErrorMessage.setText(R.string.media_output_broadcast_update_error);
         } else {
+            mRetryCount = 0;
             mBroadcastErrorMessage.setText(R.string.media_output_broadcast_last_update_error);
         }
     }
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 58b6ad3..9329b1b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -573,6 +573,7 @@
     }
 
     boolean addDeviceToPlayMedia(MediaDevice device) {
+        mMetricLogger.logInteractionExpansion(device);
         return mLocalMediaManager.addDeviceToPlayMedia(device);
     }
 
@@ -613,6 +614,7 @@
     }
 
     void releaseSession() {
+        mMetricLogger.logInteractionStopCasting();
         mLocalMediaManager.releaseSession();
     }
 
@@ -627,6 +629,7 @@
     }
 
     void adjustVolume(MediaDevice device, int volume) {
+        mMetricLogger.logInteractionAdjustVolume(device);
         ThreadUtils.postOnBackgroundThread(() -> {
             device.requestSetVolume(volume);
         });
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
index ac0295e..5d7af52 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputMetricLogger.java
@@ -96,6 +96,49 @@
     }
 
     /**
+     * Do the metric logging of volume adjustment.
+     * @param source the device been adjusted
+     */
+    public void logInteractionAdjustVolume(MediaDevice source) {
+        if (DEBUG) {
+            Log.d(TAG, "logInteraction - AdjustVolume");
+        }
+
+        SysUiStatsLog.write(
+                SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT,
+                SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__ADJUST_VOLUME,
+                getInteractionDeviceType(source));
+    }
+
+    /**
+     * Do the metric logging of stop casting.
+     */
+    public void logInteractionStopCasting() {
+        if (DEBUG) {
+            Log.d(TAG, "logInteraction - Stop casting");
+        }
+
+        SysUiStatsLog.write(
+                SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT,
+                SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__STOP_CASTING,
+                SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__UNKNOWN_TYPE);
+    }
+
+    /**
+     * Do the metric logging of device expansion.
+     */
+    public void logInteractionExpansion(MediaDevice source) {
+        if (DEBUG) {
+            Log.d(TAG, "logInteraction - Expansion");
+        }
+
+        SysUiStatsLog.write(
+                SysUiStatsLog.MEDIAOUTPUT_OP_INTERACTION_REPORT,
+                SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__INTERACTION_TYPE__EXPANSION,
+                getInteractionDeviceType(source));
+    }
+
+    /**
      * Do the metric logging of content switching failure.
      * @param deviceList media device list for device count updating
      * @param reason the reason of content switching failure
@@ -185,6 +228,27 @@
         }
     }
 
+    private int getInteractionDeviceType(MediaDevice device) {
+        switch (device.getDeviceType()) {
+            case MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE:
+                return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__BUILTIN_SPEAKER;
+            case MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE:
+                return SysUiStatsLog
+                        .MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__WIRED_3POINT5_MM_AUDIO;
+            case MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE:
+                return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__USB_C_AUDIO;
+            case MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE:
+                return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__BLUETOOTH;
+            case MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE:
+                return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__REMOTE_SINGLE;
+            case MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE:
+                return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__REMOTE_GROUP;
+            default:
+                return SysUiStatsLog.MEDIA_OUTPUT_OP_INTERACTION_REPORTED__TARGET__UNKNOWN_TYPE;
+        }
+    }
+
+
     private int getLoggingSwitchOpSubResult(int reason) {
         switch (reason) {
             case REASON_REJECTED:
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
index d27b716..622f5a2 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
@@ -273,9 +273,8 @@
                 mLongClicked = false;
                 setPressed(true);
 
-                // Use raw X and Y to detect gestures in case a parent changes the x and y values
-                mTouchDownX = (int) ev.getRawX();
-                mTouchDownY = (int) ev.getRawY();
+                mTouchDownX = (int) ev.getX();
+                mTouchDownY = (int) ev.getY();
                 if (mCode != KEYCODE_UNKNOWN) {
                     sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
                 } else {
@@ -289,8 +288,8 @@
                 postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
                 break;
             case MotionEvent.ACTION_MOVE:
-                x = (int)ev.getRawX();
-                y = (int)ev.getRawY();
+                x = (int) ev.getX();
+                y = (int) ev.getY();
 
                 float slop = QuickStepContract.getQuickStepTouchSlopPx(getContext());
                 if (Math.abs(x - mTouchDownX) > slop || Math.abs(y - mTouchDownY) > slop) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationHandle.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationHandle.java
index 33e6aa4..913b6528 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationHandle.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationHandle.java
@@ -36,8 +36,8 @@
     protected final Paint mPaint = new Paint();
     private @ColorInt final int mLightColor;
     private @ColorInt final int mDarkColor;
-    protected final int mRadius;
-    protected final int mBottom;
+    protected final float mRadius;
+    protected final float mBottom;
     private boolean mRequiresInvalidate;
 
     public NavigationHandle(Context context) {
@@ -47,8 +47,8 @@
     public NavigationHandle(Context context, AttributeSet attr) {
         super(context, attr);
         final Resources res = context.getResources();
-        mRadius = res.getDimensionPixelSize(R.dimen.navigation_handle_radius);
-        mBottom = res.getDimensionPixelSize(R.dimen.navigation_handle_bottom);
+        mRadius = res.getDimension(R.dimen.navigation_handle_radius);
+        mBottom = res.getDimension(R.dimen.navigation_handle_bottom);
 
         final int dualToneDarkTheme = Utils.getThemeAttr(context, R.attr.darkIconTheme);
         final int dualToneLightTheme = Utils.getThemeAttr(context, R.attr.lightIconTheme);
@@ -75,9 +75,9 @@
 
         // Draw that bar
         int navHeight = getHeight();
-        int height = mRadius * 2;
+        float height = mRadius * 2;
         int width = getWidth();
-        int y = (navHeight - mBottom - height);
+        float y = (navHeight - mBottom - height);
         canvas.drawRoundRect(0, y, width, y + height, mRadius, mRadius, mPaint);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/QuickswitchOrientedNavHandle.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/QuickswitchOrientedNavHandle.java
index 71c8a2c..88622aa 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/QuickswitchOrientedNavHandle.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/QuickswitchOrientedNavHandle.java
@@ -44,33 +44,33 @@
     }
 
     public RectF computeHomeHandleBounds() {
-        int left;
-        int top;
-        int bottom;
-        int right;
-        int radiusOffset = mRadius * 2;
+        float left;
+        float top;
+        float bottom;
+        float right;
+        float radiusOffset = mRadius * 2;
         int topStart = getLocationOnScreen()[1];
 
         switch (mDeltaRotation) {
             default:
             case Surface.ROTATION_0:
             case Surface.ROTATION_180:
-                int height = mRadius * 2;
-                left = getWidth() / 2 - mWidth / 2;
+                float height = mRadius * 2;
+                left = getWidth() / 2f - mWidth / 2f;
                 top = (getHeight() - mBottom - height);
-                right = getWidth() / 2 + mWidth / 2;
+                right = getWidth() / 2f + mWidth / 2f;
                 bottom = top + height;
                 break;
             case Surface.ROTATION_90:
                 left = mBottom;
                 right = left + radiusOffset;
-                top = getHeight() / 2 - (mWidth / 2) - (topStart / 2);
+                top = getHeight() / 2f - (mWidth / 2f) - (topStart / 2f);
                 bottom = top + mWidth;
                 break;
             case Surface.ROTATION_270:
                 right = getWidth() - mBottom;
                 left = right - radiusOffset;
-                top = getHeight() / 2 - (mWidth / 2) - (topStart / 2);
+                top = getHeight() / 2f - (mWidth / 2f) - (topStart / 2f);
                 bottom = top + mWidth;
                 break;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
index dbdb9d2..cd6eb99 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
@@ -58,7 +58,8 @@
     internal companion object {
         val OPS_MIC_CAMERA = intArrayOf(AppOpsManager.OP_CAMERA,
                 AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_RECORD_AUDIO,
-                AppOpsManager.OP_PHONE_CALL_MICROPHONE)
+                AppOpsManager.OP_PHONE_CALL_MICROPHONE,
+                AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO)
         val OPS_LOCATION = intArrayOf(
                 AppOpsManager.OP_COARSE_LOCATION,
                 AppOpsManager.OP_FINE_LOCATION)
@@ -315,6 +316,7 @@
             AppOpsManager.OP_COARSE_LOCATION,
             AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
             AppOpsManager.OP_PHONE_CALL_MICROPHONE,
+            AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
             AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
             else -> return null
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java
index 8000bdc..2c20feb 100644
--- a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java
+++ b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java
@@ -29,7 +29,6 @@
 import android.util.Log;
 
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
-import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.settings.UserTracker;
@@ -119,7 +118,6 @@
         mSecureSettings = secureSettings;
         mDeviceConfigProxy = proxy;
         mUserTracker = userTracker;
-
         mConfigEnableLockScreenButton = mContext.getResources().getBoolean(
             android.R.bool.config_enableQrCodeScannerOnLockScreen);
     }
@@ -258,16 +256,20 @@
         }
     }
 
+    private String getDefaultScannerActivity() {
+        return mContext.getResources().getString(
+            com.android.internal.R.string.config_defaultQrCodeComponent);
+    }
+
     private void updateQRCodeScannerActivityDetails() {
         String qrCodeScannerActivity = mDeviceConfigProxy.getString(
                 DeviceConfig.NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER, "");
 
         // "" means either the flags is not available or is set to "", and in both the cases we
-        // want to use R.string.def_qr_code_component
+        // want to use R.string.config_defaultQrCodeComponent
         if (Objects.equals(qrCodeScannerActivity, "")) {
-            qrCodeScannerActivity =
-                    mContext.getResources().getString(R.string.def_qr_code_component);
+            qrCodeScannerActivity = getDefaultScannerActivity();
         }
 
         String prevQrCodeScannerActivity = mQRCodeScannerActivity;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 3e8cdf3..e5d7b40 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -23,10 +23,12 @@
 import android.content.Intent
 import android.content.IntentFilter
 import android.content.pm.PackageManager
+import android.content.pm.UserInfo
 import android.graphics.drawable.Drawable
 import android.os.IBinder
 import android.os.PowerExemptionManager
 import android.os.RemoteException
+import android.os.UserHandle
 import android.provider.DeviceConfig.NAMESPACE_SYSTEMUI
 import android.text.format.DateUtils
 import android.util.ArrayMap
@@ -51,6 +53,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.shared.system.SysUiStatsLog
+import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.util.DeviceConfigProxy
 import com.android.systemui.util.indentIfPossible
@@ -69,6 +72,7 @@
     private val systemClock: SystemClock,
     private val activityManager: IActivityManager,
     private val packageManager: PackageManager,
+    private val userTracker: UserTracker,
     private val deviceConfigProxy: DeviceConfigProxy,
     private val dialogLaunchAnimator: DialogLaunchAnimator,
     private val broadcastDispatcher: BroadcastDispatcher,
@@ -82,7 +86,8 @@
     var changesSinceDialog = false
         private set
 
-    private var isAvailable = false
+    var isAvailable = false
+        private set
 
     private val lock = Any()
 
@@ -90,6 +95,12 @@
     var initialized = false
 
     @GuardedBy("lock")
+    private var lastNumberOfVisiblePackages = 0
+
+    @GuardedBy("lock")
+    private var currentProfileIds = mutableSetOf<Int>()
+
+    @GuardedBy("lock")
     private val runningServiceTokens = mutableMapOf<UserPackage, StartTimeAndTokens>()
 
     @GuardedBy("lock")
@@ -101,6 +112,19 @@
     @GuardedBy("lock")
     private var runningApps: ArrayMap<UserPackage, RunningApp> = ArrayMap()
 
+    private val userTrackerCallback = object : UserTracker.Callback {
+        override fun onUserChanged(newUser: Int, userContext: Context) {}
+
+        override fun onProfilesChanged(profiles: List<UserInfo>) {
+            synchronized(lock) {
+                currentProfileIds.clear()
+                currentProfileIds.addAll(profiles.map { it.id })
+                lastNumberOfVisiblePackages = 0
+                updateNumberOfVisibleRunningPackagesLocked()
+            }
+        }
+    }
+
     interface OnNumberOfPackagesChangedListener {
         fun onNumberOfPackagesChanged(numPackages: Int)
     }
@@ -120,6 +144,10 @@
                 e.rethrowFromSystemServer()
             }
 
+            userTracker.addCallback(userTrackerCallback, backgroundExecutor)
+
+            currentProfileIds.addAll(userTracker.userProfiles.map { it.id })
+
             deviceConfigProxy.addOnPropertiesChangedListener(NAMESPACE_SYSTEMUI,
                     backgroundExecutor) {
                 isAvailable = it.getBoolean(TASK_MANAGER_ENABLED, isAvailable)
@@ -153,10 +181,9 @@
         isForeground: Boolean
     ) {
         synchronized(lock) {
-            val numPackagesBefore = getNumRunningPackagesLocked()
             val userPackageKey = UserPackage(userId, packageName)
             if (isForeground) {
-                runningServiceTokens.getOrPut(userPackageKey, { StartTimeAndTokens(systemClock) })
+                runningServiceTokens.getOrPut(userPackageKey) { StartTimeAndTokens(systemClock) }
                         .addToken(token)
             } else {
                 if (runningServiceTokens[userPackageKey]?.also {
@@ -165,14 +192,7 @@
                 }
             }
 
-            val numPackagesAfter = getNumRunningPackagesLocked()
-
-            if (numPackagesAfter != numPackagesBefore) {
-                changesSinceDialog = true
-                onNumberOfPackagesChangedListeners.forEach {
-                    backgroundExecutor.execute { it.onNumberOfPackagesChanged(numPackagesAfter) }
-                }
-            }
+            updateNumberOfVisibleRunningPackagesLocked()
 
             updateAppItemsLocked()
         }
@@ -209,18 +229,30 @@
         }
     }
 
-    fun isAvailable(): Boolean {
-        return isAvailable
-    }
-
     fun getNumRunningPackages(): Int {
         synchronized(lock) {
-            return getNumRunningPackagesLocked()
+            return getNumVisiblePackagesLocked()
         }
     }
 
-    private fun getNumRunningPackagesLocked() =
-            runningServiceTokens.keys.count { it.uiControl != UIControl.HIDE_ENTRY }
+    private fun getNumVisiblePackagesLocked(): Int {
+        return runningServiceTokens.keys.count {
+            it.uiControl != UIControl.HIDE_ENTRY && currentProfileIds.contains(it.userId)
+        }
+    }
+
+    private fun updateNumberOfVisibleRunningPackagesLocked() {
+        val num = getNumVisiblePackagesLocked()
+        if (num != lastNumberOfVisiblePackages) {
+            lastNumberOfVisiblePackages = num
+            changesSinceDialog = true
+            onNumberOfPackagesChangedListeners.forEach {
+                backgroundExecutor.execute {
+                    it.onNumberOfPackagesChanged(num)
+                }
+            }
+        }
+    }
 
     fun shouldUpdateFooterVisibility() = dialog == null
 
@@ -289,7 +321,9 @@
             val ai = packageManager.getApplicationInfoAsUser(it.packageName, 0, it.userId)
             runningApps[it] = RunningApp(it.userId, it.packageName,
                     runningServiceTokens[it]!!.startTime, it.uiControl,
-                    ai.loadLabel(packageManager), ai.loadIcon(packageManager))
+                    packageManager.getApplicationLabel(ai),
+                    packageManager.getUserBadgedIcon(
+                            packageManager.getApplicationIcon(ai), UserHandle.of(it.userId)))
             logEvent(stopped = false, it.packageName, it.userId, runningApps[it]!!.timeStarted)
         }
 
@@ -404,6 +438,7 @@
         val packageName: String
     ) {
         val uid by lazy { packageManager.getPackageUidAsUser(packageName, userId) }
+        var backgroundRestrictionExemptionReason = PowerExemptionManager.REASON_DENIED
 
         private var uiControlInitialized = false
         var uiControl: UIControl = UIControl.NORMAL
@@ -416,7 +451,9 @@
             private set
 
         fun updateUiControl() {
-            uiControl = when (activityManager.getBackgroundRestrictionExemptionReason(uid)) {
+            backgroundRestrictionExemptionReason =
+                    activityManager.getBackgroundRestrictionExemptionReason(uid)
+            uiControl = when (backgroundRestrictionExemptionReason) {
                 PowerExemptionManager.REASON_SYSTEM_UID,
                 PowerExemptionManager.REASON_DEVICE_DEMO_MODE -> UIControl.HIDE_ENTRY
 
@@ -448,7 +485,7 @@
             pw.indentIfPossible {
                 pw.println("userId=$userId")
                 pw.println("packageName=$packageName")
-                pw.println("uiControl=$uiControl")
+                pw.println("uiControl=$uiControl (reason=$backgroundRestrictionExemptionReason)")
             }
             pw.println("]")
         }
@@ -525,7 +562,7 @@
                 pw.println("userId=$userId")
                 pw.println("packageName=$packageName")
                 pw.println("timeStarted=$timeStarted (time since start =" +
-                        " ${systemClock.elapsedRealtime() - timeStarted}ms)\"")
+                        " ${systemClock.elapsedRealtime() - timeStarted}ms)")
                 pw.println("uiControl=$uiControl")
                 pw.println("appLabel=$appLabel")
                 pw.println("icon=$icon")
@@ -542,6 +579,7 @@
     override fun dump(printwriter: PrintWriter, args: Array<out String>) {
         val pw = IndentingPrintWriter(printwriter)
         synchronized(lock) {
+            pw.println("current user profiles = $currentProfileIds")
             pw.println("changesSinceDialog=$changesSinceDialog")
             pw.println("Running service tokens: [")
             pw.indentIfPossible {
@@ -560,8 +598,10 @@
             pw.indentIfPossible {
                 runningApps.forEach { (userPackage, runningApp) ->
                     pw.println("{")
-                    userPackage.dump(pw)
-                    runningApp.dump(pw, systemClock)
+                    pw.indentIfPossible {
+                        userPackage.dump(pw)
+                        runningApp.dump(pw, systemClock)
+                    }
                     pw.println("}")
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
index dd99db4..584de6e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooter.java
@@ -22,6 +22,9 @@
 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_NETWORK;
 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TITLE;
 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MANAGEMENT_TWO_NAMED_VPN;
+import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_CA_CERT_SUBTITLE;
+import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_NETWORK_SUBTITLE;
+import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_MONITORING_VPN_SUBTITLE;
 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_NAMED_MANAGEMENT;
 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_PERSONAL_PROFILE_NAMED_VPN;
 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_DIALOG_VIEW_POLICIES;
@@ -92,6 +95,7 @@
 import com.android.systemui.util.ViewController;
 
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -106,7 +110,7 @@
 
     private final TextView mFooterText;
     private final ImageView mPrimaryFooterIcon;
-    private final Context mContext;
+    private Context mContext;
     private final DevicePolicyManager mDpm;
     private final Callback mCallback = new Callback();
     private final SecurityController mSecurityController;
@@ -141,6 +145,63 @@
         }
     };
 
+    private Supplier<String> mManagementTitleSupplier = () ->
+            mContext == null ? null : mContext.getString(R.string.monitoring_title_device_owned);
+
+    private Supplier<String> mManagementMessageSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.quick_settings_disclosure_management);
+
+    private Supplier<String> mManagementMonitoringStringSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.quick_settings_disclosure_management_monitoring);
+
+    private Supplier<String> mManagementMultipleVpnStringSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.quick_settings_disclosure_management_vpns);
+
+    private Supplier<String> mWorkProfileMonitoringStringSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.quick_settings_disclosure_managed_profile_monitoring);
+
+    private Supplier<String> mWorkProfileNetworkStringSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.quick_settings_disclosure_managed_profile_network_activity);
+
+    private Supplier<String> mMonitoringSubtitleCaCertStringSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.monitoring_subtitle_ca_certificate);
+
+    private Supplier<String> mMonitoringSubtitleNetworkStringSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.monitoring_subtitle_network_logging);
+
+    private Supplier<String> mMonitoringSubtitleVpnStringSupplier = () ->
+            mContext == null ? null : mContext.getString(R.string.monitoring_subtitle_vpn);
+
+    private Supplier<String> mViewPoliciesButtonStringSupplier = () ->
+            mContext == null ? null : mContext.getString(R.string.monitoring_button_view_policies);
+
+    private Supplier<String> mManagementDialogStringSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.monitoring_description_management);
+
+    private Supplier<String> mManagementDialogCaCertStringSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.monitoring_description_management_ca_certificate);
+
+    private Supplier<String> mWorkProfileDialogCaCertStringSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.monitoring_description_managed_profile_ca_certificate);
+
+    private Supplier<String> mManagementDialogNetworkStringSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.monitoring_description_management_network_logging);
+
+    private Supplier<String> mWorkProfileDialogNetworkStringSupplier = () ->
+            mContext == null ? null : mContext.getString(
+                    R.string.monitoring_description_managed_profile_network_logging);
+
     @Inject
     QSSecurityFooter(@Named(QS_SECURITY_FOOTER_VIEW) View rootView,
             UserTracker userTracker, @Main Handler mainHandler,
@@ -337,9 +398,7 @@
     private String getManagedDeviceMonitoringText(CharSequence organizationName) {
         if (organizationName == null) {
             return mDpm.getResources().getString(
-                    QS_MSG_MANAGEMENT_MONITORING,
-                    () -> mContext.getString(
-                            R.string.quick_settings_disclosure_management_monitoring));
+                    QS_MSG_MANAGEMENT_MONITORING, mManagementMonitoringStringSupplier);
         }
         return mDpm.getResources().getString(
                 QS_MSG_NAMED_MANAGEMENT_MONITORING,
@@ -354,9 +413,7 @@
         if (vpnName != null && vpnNameWorkProfile != null) {
             if (organizationName == null) {
                 return mDpm.getResources().getString(
-                        QS_MSG_MANAGEMENT_MULTIPLE_VPNS,
-                        () -> mContext.getString(
-                                R.string.quick_settings_disclosure_management_vpns));
+                        QS_MSG_MANAGEMENT_MULTIPLE_VPNS, mManagementMultipleVpnStringSupplier);
             }
             return mDpm.getResources().getString(
                     QS_MSG_NAMED_MANAGEMENT_MULTIPLE_VPNS,
@@ -386,10 +443,7 @@
 
     private String getMangedDeviceGeneralText(CharSequence organizationName) {
         if (organizationName == null) {
-            return mDpm.getResources().getString(
-                    QS_MSG_MANAGEMENT,
-                    () -> mContext.getString(
-                            R.string.quick_settings_disclosure_management));
+            return mDpm.getResources().getString(QS_MSG_MANAGEMENT, mManagementMessageSupplier);
         }
         if (isFinancedDevice()) {
             return mContext.getString(
@@ -431,9 +485,7 @@
         if (hasCACertsInWorkProfile && isWorkProfileOn) {
             if (workProfileOrganizationName == null) {
                 return mDpm.getResources().getString(
-                        QS_MSG_WORK_PROFILE_MONITORING,
-                        () -> mContext.getString(
-                                R.string.quick_settings_disclosure_managed_profile_monitoring));
+                        QS_MSG_WORK_PROFILE_MONITORING, mWorkProfileMonitoringStringSupplier);
             }
             return mDpm.getResources().getString(
                     QS_MSG_NAMED_WORK_PROFILE_MONITORING,
@@ -478,10 +530,9 @@
 
     private String getManagedProfileNetworkActivityText() {
         return mDpm.getResources().getString(
-                QS_MSG_WORK_PROFILE_NETWORK,
-                () -> mContext.getString(
-                        R.string.quick_settings_disclosure_managed_profile_network_activity));
+                QS_MSG_WORK_PROFILE_NETWORK, mWorkProfileNetworkStringSupplier);
     }
+
     @Override
     public void onClick(DialogInterface dialog, int which) {
         if (which == DialogInterface.BUTTON_NEGATIVE) {
@@ -569,6 +620,12 @@
             caCertsWarning.setText(caCertsMessage);
             // Make "Open trusted credentials"-link clickable
             caCertsWarning.setMovementMethod(new LinkMovementMethod());
+
+            TextView caCertsSubtitle = (TextView) dialogView.findViewById(R.id.ca_certs_subtitle);
+            String caCertsSubtitleMessage = mDpm.getResources().getString(
+                    QS_DIALOG_MONITORING_CA_CERT_SUBTITLE, mMonitoringSubtitleCaCertStringSupplier);
+            caCertsSubtitle.setText(caCertsSubtitleMessage);
+
         }
 
         // network logging section
@@ -581,6 +638,13 @@
             TextView networkLoggingWarning =
                     (TextView) dialogView.findViewById(R.id.network_logging_warning);
             networkLoggingWarning.setText(networkLoggingMessage);
+
+            TextView networkLoggingSubtitle = (TextView) dialogView.findViewById(
+                    R.id.network_logging_subtitle);
+            String networkLoggingSubtitleMessage = mDpm.getResources().getString(
+                    QS_DIALOG_MONITORING_NETWORK_SUBTITLE,
+                    mMonitoringSubtitleNetworkStringSupplier);
+            networkLoggingSubtitle.setText(networkLoggingSubtitleMessage);
         }
 
         // vpn section
@@ -594,6 +658,11 @@
             vpnWarning.setText(vpnMessage);
             // Make "Open VPN Settings"-link clickable
             vpnWarning.setMovementMethod(new LinkMovementMethod());
+
+            TextView vpnSubtitle = (TextView) dialogView.findViewById(R.id.vpn_subtitle);
+            String vpnSubtitleMessage = mDpm.getResources().getString(
+                    QS_DIALOG_MONITORING_VPN_SUBTITLE, mMonitoringSubtitleVpnStringSupplier);
+            vpnSubtitle.setText(vpnSubtitleMessage);
         }
 
         // Note: if a new section is added, should update configSubtitleVisibility to include
@@ -657,8 +726,7 @@
     @VisibleForTesting
     String getSettingsButton() {
         return mDpm.getResources().getString(
-                QS_DIALOG_VIEW_POLICIES,
-                () -> mContext.getString(R.string.monitoring_button_view_policies));
+                QS_DIALOG_VIEW_POLICIES, mViewPoliciesButtonStringSupplier);
     }
 
     private String getPositiveButton() {
@@ -692,9 +760,7 @@
                         organizationName);
             }
         }
-        return mDpm.getResources().getString(
-                QS_DIALOG_MANAGEMENT,
-                () -> mContext.getString(R.string.monitoring_description_management));
+        return mDpm.getResources().getString(QS_DIALOG_MANAGEMENT, mManagementDialogStringSupplier);
     }
 
     @Nullable
@@ -703,15 +769,11 @@
         if (!(hasCACerts || hasCACertsInWorkProfile)) return null;
         if (isDeviceManaged) {
             return mDpm.getResources().getString(
-                    QS_DIALOG_MANAGEMENT_CA_CERT,
-                    () -> mContext.getString(
-                            R.string.monitoring_description_management_ca_certificate));
+                    QS_DIALOG_MANAGEMENT_CA_CERT, mManagementDialogCaCertStringSupplier);
         }
         if (hasCACertsInWorkProfile) {
             return mDpm.getResources().getString(
-                    QS_DIALOG_WORK_PROFILE_CA_CERT,
-                    () -> mContext.getString(
-                            R.string.monitoring_description_managed_profile_ca_certificate));
+                    QS_DIALOG_WORK_PROFILE_CA_CERT, mWorkProfileDialogCaCertStringSupplier);
         }
         return mContext.getString(R.string.monitoring_description_ca_certificate);
     }
@@ -722,14 +784,10 @@
         if (!isNetworkLoggingEnabled) return null;
         if (isDeviceManaged) {
             return mDpm.getResources().getString(
-                    QS_DIALOG_MANAGEMENT_NETWORK,
-                    () -> mContext.getString(
-                            R.string.monitoring_description_management_network_logging));
+                    QS_DIALOG_MANAGEMENT_NETWORK, mManagementDialogNetworkStringSupplier);
         } else {
             return mDpm.getResources().getString(
-                    QS_DIALOG_WORK_PROFILE_NETWORK,
-                    () -> mContext.getString(
-                            R.string.monitoring_description_managed_profile_network_logging));
+                    QS_DIALOG_WORK_PROFILE_NETWORK, mWorkProfileDialogNetworkStringSupplier);
         }
     }
 
@@ -799,7 +857,7 @@
         } else {
             return mDpm.getResources().getString(
                     QS_DIALOG_MANAGEMENT_TITLE,
-                    () -> mContext.getString(R.string.monitoring_title_device_owned));
+                    mManagementTitleSupplier);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
index d7aa8b2..7c8e77b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HotspotTile.java
@@ -30,6 +30,7 @@
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils;
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -158,7 +159,9 @@
         state.expandedAccessibilityClassName = Switch.class.getName();
         state.contentDescription = state.label;
 
-        final boolean isTileUnavailable = isDataSaverEnabled;
+        final boolean isWifiTetheringAllowed =
+                WifiEnterpriseRestrictionUtils.isWifiTetheringAllowed(mHost.getUserContext());
+        final boolean isTileUnavailable = isDataSaverEnabled || !isWifiTetheringAllowed;
         final boolean isTileActive = (state.value || state.isTransient);
 
         if (isTileUnavailable) {
@@ -167,15 +170,17 @@
             state.state = isTileActive ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
         }
 
-        state.secondaryLabel = getSecondaryLabel(
-                isTileActive, isTransient, isDataSaverEnabled, numConnectedDevices);
+        state.secondaryLabel = getSecondaryLabel(isTileActive, isTransient, isDataSaverEnabled,
+                numConnectedDevices, isWifiTetheringAllowed);
         state.stateDescription = state.secondaryLabel;
     }
 
     @Nullable
     private String getSecondaryLabel(boolean isActive, boolean isTransient,
-            boolean isDataSaverEnabled, int numConnectedDevices) {
-        if (isTransient) {
+            boolean isDataSaverEnabled, int numConnectedDevices, boolean isWifiTetheringAllowed) {
+        if (!isWifiTetheringAllowed) {
+            return mContext.getString(R.string.wifitrackerlib_admin_restricted_network);
+        } else if (isTransient) {
             return mContext.getString(R.string.quick_settings_hotspot_secondary_label_transient);
         } else if (isDataSaverEnabled) {
             return mContext.getString(
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 4728c67..489f881 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -78,7 +78,6 @@
 import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
-import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.widget.Toast;
 import android.window.WindowContext;
@@ -484,6 +483,7 @@
                 setWindowFocusable(false);
             }
         });
+        mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis());
 
         mScreenshotView.setOnKeyListener((v, keyCode, event) -> {
             if (keyCode == KeyEvent.KEYCODE_BACK) {
@@ -558,14 +558,9 @@
 
     private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
             Insets screenInsets, ComponentName topComponent, boolean showFlash) {
-        if (mAccessibilityManager.isEnabled()) {
-            AccessibilityEvent event =
-                    new AccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
-            event.setContentDescription(
-                    mContext.getResources().getString(R.string.screenshot_saving_title));
-            mAccessibilityManager.sendAccessibilityEvent(event);
-        }
-
+        withWindowAttached(() ->
+                mScreenshotView.announceForAccessibility(
+                        mContext.getResources().getString(R.string.screenshot_saving_title)));
 
         if (mScreenshotView.isAttachedToWindow()) {
             // if we didn't already dismiss for another reason
@@ -632,6 +627,7 @@
                                 }
                             }
                         }
+
                         @Override
                         public void requestCompatCameraControl(boolean showControl,
                                 boolean transformationApplied,
@@ -717,6 +713,7 @@
             Log.e(TAG, "requestScrollCapture failed", e);
         }
     }
+
     ListenableFuture<ScrollCaptureController.LongScreenshot> mLongScreenshotFuture;
 
     private void runBatchScrollCapture(ScrollCaptureResponse response) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index ccfcaa6..79939c8 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -18,6 +18,7 @@
 
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_TAKE_SCREENSHOT;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT;
@@ -83,6 +84,7 @@
 
 import androidx.constraintlayout.widget.ConstraintLayout;
 
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
@@ -166,6 +168,9 @@
     private final ArrayList<OverlayActionChip> mSmartChips = new ArrayList<>();
     private PendingInteraction mPendingInteraction;
 
+    private final InteractionJankMonitor mInteractionJankMonitor;
+    private long mDefaultTimeoutOfTimeoutHandler;
+
     private enum PendingInteraction {
         PREVIEW,
         EDIT,
@@ -189,6 +194,7 @@
             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         mResources = mContext.getResources();
+        mInteractionJankMonitor = getInteractionJankMonitorInstance();
 
         mFixedSize = mResources.getDimensionPixelSize(R.dimen.overlay_x_scale);
 
@@ -229,6 +235,14 @@
         });
     }
 
+    private InteractionJankMonitor getInteractionJankMonitorInstance() {
+        return InteractionJankMonitor.getInstance();
+    }
+
+    void setDefaultTimeoutMillis(long timeout) {
+        mDefaultTimeoutOfTimeoutHandler = timeout;
+    }
+
     public void hideScrollChip() {
         mScrollChip.setVisibility(View.GONE);
     }
@@ -395,6 +409,9 @@
 
             @Override
             public void onDismissComplete() {
+                if (mInteractionJankMonitor.isInstrumenting(CUJ_TAKE_SCREENSHOT)) {
+                    mInteractionJankMonitor.end(CUJ_TAKE_SCREENSHOT);
+                }
                 mCallbacks.onDismiss();
             }
         });
@@ -595,6 +612,20 @@
 
         dropInAnimation.addListener(new AnimatorListenerAdapter() {
             @Override
+            public void onAnimationCancel(Animator animation) {
+                mInteractionJankMonitor.cancel(CUJ_TAKE_SCREENSHOT);
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                InteractionJankMonitor.Configuration.Builder builder =
+                        InteractionJankMonitor.Configuration.Builder.withView(
+                                CUJ_TAKE_SCREENSHOT, mScreenshotPreview)
+                                .setTag("DropIn");
+                mInteractionJankMonitor.begin(builder);
+            }
+
+            @Override
             public void onAnimationEnd(Animator animation) {
                 if (DEBUG_ANIM) {
                     Log.d(TAG, "drop-in animation ended");
@@ -620,7 +651,7 @@
                 mScreenshotPreview.setX(finalPos.x - mScreenshotPreview.getWidth() / 2f);
                 mScreenshotPreview.setY(finalPos.y - mScreenshotPreview.getHeight() / 2f);
                 requestLayout();
-
+                mInteractionJankMonitor.end(CUJ_TAKE_SCREENSHOT);
                 createScreenshotActionsShadeAnimation().start();
             }
         });
@@ -691,6 +722,28 @@
         mActionsContainer.setVisibility(View.VISIBLE);
         mActionsContainerBackground.setVisibility(View.VISIBLE);
 
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mInteractionJankMonitor.cancel(CUJ_TAKE_SCREENSHOT);
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mInteractionJankMonitor.end(CUJ_TAKE_SCREENSHOT);
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                InteractionJankMonitor.Configuration.Builder builder =
+                        InteractionJankMonitor.Configuration.Builder.withView(
+                                CUJ_TAKE_SCREENSHOT, mScreenshotStatic)
+                                .setTag("Actions")
+                                .setTimeout(mDefaultTimeoutOfTimeoutHandler);
+                mInteractionJankMonitor.begin(builder);
+            }
+        });
+
         animator.addUpdateListener(animation -> {
             float t = animation.getAnimatedFraction();
             float containerAlpha = t < alphaFraction ? t / alphaFraction : 1;
@@ -767,7 +820,7 @@
                             animateDismissal();
                         });
                 actionChip.setAlpha(1);
-                mActionsView.addView(actionChip);
+                mActionsView.addView(actionChip, mActionsView.getChildCount() - 1);
                 mSmartChips.add(actionChip);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index 7f3758e..2621f6d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.screenshot;
 
+import static android.app.admin.DevicePolicyResources.Strings.SystemUi.SCREENSHOT_BLOCKED_BY_ADMIN;
 import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
 
 import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE;
@@ -54,7 +55,9 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.util.ScreenshotHelper;
 import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Background;
 
+import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
@@ -70,6 +73,7 @@
     private final ScreenshotNotificationsController mNotificationsController;
     private final Handler mHandler;
     private final Context mContext;
+    private final @Background Executor mBgExecutor;
 
     private final BroadcastReceiver mCloseSystemDialogs = new BroadcastReceiver() {
         @Override
@@ -97,7 +101,8 @@
     @Inject
     public TakeScreenshotService(ScreenshotController screenshotController, UserManager userManager,
             DevicePolicyManager devicePolicyManager, UiEventLogger uiEventLogger,
-            ScreenshotNotificationsController notificationsController, Context context) {
+            ScreenshotNotificationsController notificationsController, Context context,
+            @Background Executor bgExecutor) {
         if (DEBUG_SERVICE) {
             Log.d(TAG, "new " + this);
         }
@@ -108,6 +113,7 @@
         mUiEventLogger = uiEventLogger;
         mNotificationsController = notificationsController;
         mContext = context;
+        mBgExecutor = bgExecutor;
     }
 
     @Override
@@ -189,12 +195,18 @@
             requestCallback.reportError();
             return true;
         }
-        if(mDevicePolicyManager.getScreenCaptureDisabled(null, UserHandle.USER_ALL)) {
-            Log.w(TAG, "Skipping screenshot because an IT admin has disabled "
-                    + "screenshots on the device");
-            Toast.makeText(mContext, R.string.screenshot_blocked_by_admin,
-                    Toast.LENGTH_SHORT).show();
-            requestCallback.reportError();
+
+        if (mDevicePolicyManager.getScreenCaptureDisabled(null, UserHandle.USER_ALL)) {
+            mBgExecutor.execute(() -> {
+                Log.w(TAG, "Skipping screenshot because an IT admin has disabled "
+                        + "screenshots on the device");
+                String blockedByAdminText = mDevicePolicyManager.getResources().getString(
+                        SCREENSHOT_BLOCKED_BY_ADMIN,
+                        () -> mContext.getString(R.string.screenshot_blocked_by_admin));
+                mHandler.post(() ->
+                        Toast.makeText(mContext, blockedByAdminText, Toast.LENGTH_SHORT).show());
+                requestCallback.reportError();
+            });
             return true;
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java b/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java
index 9156601..71c2cb4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TimeoutHandler.java
@@ -72,6 +72,10 @@
         mDefaultTimeout = timeout;
     }
 
+    int getDefaultTimeoutMillis() {
+        return mDefaultTimeout;
+    }
+
     /**
      * Cancel the current timeout, if any. To reset the delayed runnable use resetTimeout instead.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
index 7530681..f68e042 100644
--- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
+++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java
@@ -58,6 +58,7 @@
     private Drawable mDrawable;
     private PorterDuffColorFilter mColorFilter;
     private int mTintColor;
+    private boolean mBlendWithMainColor = true;
     private Runnable mChangeRunnable;
     private Executor mChangeRunnableExecutor;
     private Executor mExecutor;
@@ -192,6 +193,19 @@
     }
 
     /**
+     * The call to {@link #setTint} will blend with the main color, with the amount
+     * determined by the alpha of the tint. Set to false to avoid this blend.
+     */
+    public void setBlendWithMainColor(boolean blend) {
+        mBlendWithMainColor = blend;
+    }
+
+    /** @return true if blending tint color with main color */
+    public boolean shouldBlendWithMainColor() {
+        return mBlendWithMainColor;
+    }
+
+    /**
      * Tints this view, optionally animating it.
      * @param color The color.
      * @param animated If we should animate.
@@ -211,8 +225,11 @@
             // Optimization to blend colors and avoid a color filter
             ScrimDrawable drawable = (ScrimDrawable) mDrawable;
             float tintAmount = Color.alpha(mTintColor) / 255f;
-            int mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor,
-                    tintAmount);
+
+            int mainTinted = mTintColor;
+            if (mBlendWithMainColor) {
+                mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor, tintAmount);
+            }
             drawable.setColor(mainTinted, animated);
         } else {
             boolean hasAlpha = Color.alpha(mTintColor) != 0;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java
index 4d933d9..e44d334 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java
@@ -29,7 +29,6 @@
 import com.android.systemui.R;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry.OnSensitivityChangedListener;
 
 import java.util.ArrayList;
 
@@ -49,6 +48,7 @@
     private TextView mTextView;
     private NotificationEntry mShowingEntry;
     private Runnable mOnDrawingRectChangedListener;
+    private boolean mRedactSensitiveContent;
 
     public HeadsUpStatusBarView(Context context) {
         this(context, null);
@@ -111,29 +111,28 @@
     }
 
     public void setEntry(NotificationEntry entry) {
-        if (mShowingEntry != null) {
-            mShowingEntry.removeOnSensitivityChangedListener(mOnSensitivityChangedListener);
-        }
         mShowingEntry = entry;
-
         if (mShowingEntry != null) {
             CharSequence text = entry.headsUpStatusBarText;
-            if (entry.isSensitive()) {
+            if (mRedactSensitiveContent && entry.hasSensitiveContents()) {
                 text = entry.headsUpStatusBarTextPublic;
             }
             mTextView.setText(text);
-            mShowingEntry.addOnSensitivityChangedListener(mOnSensitivityChangedListener);
         }
     }
 
-    private final OnSensitivityChangedListener mOnSensitivityChangedListener = entry -> {
-        if (entry != mShowingEntry) {
-            throw new IllegalStateException("Got a sensitivity change for " + entry
-                    + " but mShowingEntry is " + mShowingEntry);
+    public void setRedactSensitiveContent(boolean redactSensitiveContent) {
+        if (mRedactSensitiveContent == redactSensitiveContent) {
+            return;
         }
-        // Update the text
-        setEntry(entry);
-    };
+        mRedactSensitiveContent = redactSensitiveContent;
+        if (mShowingEntry != null && mShowingEntry.hasSensitiveContents()) {
+            mTextView.setText(
+                    mRedactSensitiveContent
+                            ? mShowingEntry.headsUpStatusBarTextPublic
+                            : mShowingEntry.headsUpStatusBarText);
+        }
+    }
 
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index c1ea6bf..cbe722b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -386,7 +386,7 @@
             }
             if (view is ExpandableNotificationRow) {
                 // Only drag down on sensitive views, otherwise the ExpandHelper will take this
-                return view.entry.isSensitive
+                return lockScreenUserManager.notifNeedsRedactionInPublic(view.entry)
             }
         }
         return false
@@ -552,7 +552,8 @@
             logger.logShadeDisabledOnGoToLockedShade()
             return
         }
-        var userId: Int = lockScreenUserManager.getCurrentUserId()
+        val currentUser = lockScreenUserManager.currentUserId
+        var userId: Int = currentUser
         var entry: NotificationEntry? = null
         if (expandView is ExpandableNotificationRow) {
             entry = expandView.entry
@@ -562,12 +563,18 @@
             entry.setGroupExpansionChanging(true)
             userId = entry.sbn.userId
         }
-        var fullShadeNeedsBouncer = (!lockScreenUserManager.userAllowsPrivateNotificationsInPublic(
-                lockScreenUserManager.getCurrentUserId()) ||
-                !lockScreenUserManager.shouldShowLockscreenNotifications() ||
-                falsingCollector.shouldEnforceBouncer())
-        if (keyguardBypassController.bypassEnabled) {
-            fullShadeNeedsBouncer = false
+        val fullShadeNeedsBouncer = when {
+            // No bouncer necessary if we're bypassing
+            keyguardBypassController.bypassEnabled -> false
+            // Redacted notificationss are present, bouncer should be shown before un-redacting in
+            // the full shade
+            lockScreenUserManager.sensitiveNotifsNeedRedactionInPublic(currentUser) -> true
+            // Notifications are hidden in public, bouncer should be shown before showing them in
+            // the full shade
+            !lockScreenUserManager.shouldShowLockscreenNotifications() -> true
+            // Bouncer is being enforced, so we need to show it
+            falsingCollector.shouldEnforceBouncer() -> true
+            else -> false
         }
         if (lockScreenUserManager.isLockscreenPublicMode(userId) && fullShadeNeedsBouncer) {
             statusBarStateController.setLeaveOpenOnKeyguardHide(true)
@@ -911,4 +918,4 @@
         host.getLocationOnScreen(temp2)
         return expandCallback.getChildAtRawPosition(x + temp2[0], y + temp2[1])
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
index 9a1a144..512230e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
@@ -71,17 +71,22 @@
     boolean shouldHideNotifications(String key);
     boolean shouldShowOnKeyguard(NotificationEntry entry);
 
+    void addOnNeedsRedactionInPublicChangedListener(Runnable listener);
+
+    void removeOnNeedsRedactionInPublicChangedListener(Runnable listener);
+
     boolean isAnyProfilePublicMode();
 
     void updatePublicMode();
 
-    boolean needsRedaction(NotificationEntry entry);
+    /** Does this notification require redaction if it is displayed when the device is public? */
+    boolean notifNeedsRedactionInPublic(NotificationEntry entry);
 
     /**
-     * Has the given user chosen to allow their private (full) notifications to be shown even
-     * when the lockscreen is in "public" (secure & locked) mode?
+     * Do all sensitive notifications belonging to the given user require redaction when they are
+     * displayed in public?
      */
-    boolean userAllowsPrivateNotificationsInPublic(int currentUserId);
+    boolean sensitiveNotifsNeedRedactionInPublic(int userId);
 
     /**
      * Has the given user chosen to allow notifications to be shown even when the lockscreen is in
@@ -93,6 +98,7 @@
     interface UserChangedListener {
         default void onUserChanged(int userId) {}
         default void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) {}
+        default void onUserRemoved(int userId) {}
     }
 
     /** Used to hide notifications on the lockscreen */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 56e09f0..0394724 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -24,7 +24,6 @@
 
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
-import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
@@ -45,7 +44,6 @@
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
@@ -60,6 +58,7 @@
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.ListenerSet;
 import com.android.systemui.util.settings.SecureSettings;
 
 import java.io.PrintWriter;
@@ -85,13 +84,12 @@
     private final DeviceProvisionedController mDeviceProvisionedController;
     private final KeyguardStateController mKeyguardStateController;
     private final SecureSettings mSecureSettings;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final Lazy<OverviewProxyService> mOverviewProxyService;
     private final Object mLock = new Object();
-
-    // Lazy
-    private NotificationEntryManager mEntryManager;
-
     private final Lazy<NotificationVisibilityProvider> mVisibilityProviderLazy;
     private final Lazy<CommonNotifCollection> mCommonNotifCollectionLazy;
+    private final Lazy<NotificationEntryManager> mEntryManagerLazy;
     private final DevicePolicyManager mDevicePolicyManager;
     private final SparseBooleanArray mLockscreenPublicMode = new SparseBooleanArray();
     private final SparseBooleanArray mUsersWithSeparateWorkChallenge = new SparseBooleanArray();
@@ -103,13 +101,14 @@
     private final List<UserChangedListener> mListeners = new ArrayList<>();
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final NotificationClickNotifier mClickNotifier;
-
-    private boolean mShowLockscreenNotifications;
-    private boolean mAllowLockscreenRemoteInput;
-    private LockPatternUtils mLockPatternUtils;
-    protected KeyguardManager mKeyguardManager;
-    private int mState = StatusBarState.SHADE;
-    private List<KeyguardNotificationSuppressor> mKeyguardSuppressors = new ArrayList<>();
+    private final LockPatternUtils mLockPatternUtils;
+    private final List<KeyguardNotificationSuppressor> mKeyguardSuppressors = new ArrayList<>();
+    protected final Context mContext;
+    private final Handler mMainHandler;
+    protected final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>();
+    protected final SparseArray<UserInfo> mCurrentManagedProfiles = new SparseArray<>();
+    private final ListenerSet<Runnable> mOnSensitiveContentRedactionChangeListeners =
+            new ListenerSet<>();
 
     protected final BroadcastReceiver mAllUsersReceiver = new BroadcastReceiver() {
         @Override
@@ -120,7 +119,11 @@
                     isCurrentProfile(getSendingUserId())) {
                 mUsersAllowingPrivateNotifications.clear();
                 updateLockscreenNotificationSetting();
-                getEntryManager().updateNotifications("ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED");
+                for (Runnable listener : mOnSensitiveContentRedactionChangeListeners) {
+                    listener.run();
+                }
+                mEntryManagerLazy.get()
+                        .updateNotifications("ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED");
             }
         }
     };
@@ -142,13 +145,22 @@
                     // The filtering needs to happen before the update call below in order to
                     // make sure
                     // the presenter has the updated notifications from the new user
-                    getEntryManager().reapplyFilterAndSort("user switched");
+                    mEntryManagerLazy.get().reapplyFilterAndSort("user switched");
                     mPresenter.onUserSwitched(mCurrentUserId);
 
                     for (UserChangedListener listener : mListeners) {
                         listener.onUserChanged(mCurrentUserId);
                     }
                     break;
+                case Intent.ACTION_USER_REMOVED:
+                    int removedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                    if (removedUserId != -1) {
+                        for (UserChangedListener listener : mListeners) {
+                            listener.onUserRemoved(removedUserId);
+                        }
+                    }
+                    updateCurrentProfilesCache();
+                    break;
                 case Intent.ACTION_USER_ADDED:
                 case Intent.ACTION_MANAGED_PROFILE_AVAILABLE:
                 case Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE:
@@ -156,7 +168,7 @@
                     break;
                 case Intent.ACTION_USER_UNLOCKED:
                     // Start the overview connection to the launcher service
-                    Dependency.get(OverviewProxyService.class).startConnectionToCurrentUser();
+                    mOverviewProxyService.get().startConnectionToCurrentUser();
                     break;
                 case NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION:
                     final IntentSender intentSender = intent.getParcelableExtra(
@@ -179,28 +191,26 @@
         }
     };
 
-    protected final Context mContext;
-    private final Handler mMainHandler;
-    protected final SparseArray<UserInfo> mCurrentProfiles = new SparseArray<>();
-    protected final SparseArray<UserInfo> mCurrentManagedProfiles = new SparseArray<>();
-
-    protected int mCurrentUserId = 0;
+    // Late-init
     protected NotificationPresenter mPresenter;
     protected ContentObserver mLockscreenSettingsObserver;
     protected ContentObserver mSettingsObserver;
-    private boolean mHideSilentNotificationsOnLockscreen;
+    protected KeyguardManager mKeyguardManager;
 
-    private NotificationEntryManager getEntryManager() {
-        if (mEntryManager == null) {
-            mEntryManager = Dependency.get(NotificationEntryManager.class);
-        }
-        return mEntryManager;
-    }
+    protected int mCurrentUserId = 0;
+    private int mState = StatusBarState.SHADE;
+    private boolean mHideSilentNotificationsOnLockscreen;
+    private boolean mShowLockscreenNotifications;
+    private boolean mAllowLockscreenRemoteInput;
 
     @Inject
-    public NotificationLockscreenUserManagerImpl(Context context,
+    public NotificationLockscreenUserManagerImpl(
+            Context context,
             BroadcastDispatcher broadcastDispatcher,
             DevicePolicyManager devicePolicyManager,
+            KeyguardUpdateMonitor keyguardUpdateMonitor,
+            Lazy<NotificationEntryManager> notificationEntryManagerLazy,
+            Lazy<OverviewProxyService> overviewProxyServiceLazy,
             UserManager userManager,
             Lazy<NotificationVisibilityProvider> visibilityProviderLazy,
             Lazy<CommonNotifCollection> commonNotifCollectionLazy,
@@ -216,9 +226,11 @@
         mMainHandler = mainHandler;
         mDevicePolicyManager = devicePolicyManager;
         mUserManager = userManager;
+        mOverviewProxyService = overviewProxyServiceLazy;
         mCurrentUserId = ActivityManager.getCurrentUser();
         mVisibilityProviderLazy = visibilityProviderLazy;
         mCommonNotifCollectionLazy = commonNotifCollectionLazy;
+        mEntryManagerLazy = notificationEntryManagerLazy;
         mClickNotifier = clickNotifier;
         statusBarStateController.addCallback(this);
         mLockPatternUtils = new LockPatternUtils(context);
@@ -227,10 +239,12 @@
         mDeviceProvisionedController = deviceProvisionedController;
         mSecureSettings = secureSettings;
         mKeyguardStateController = keyguardStateController;
+        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
 
         dumpManager.registerDumpable(this);
     }
 
+    @Override
     public void setUpWithPresenter(NotificationPresenter presenter) {
         mPresenter = presenter;
 
@@ -243,7 +257,10 @@
                 mUsersAllowingNotifications.clear();
                 // ... and refresh all the notifications
                 updateLockscreenNotificationSetting();
-                getEntryManager().updateNotifications("LOCK_SCREEN_SHOW_NOTIFICATIONS,"
+                for (Runnable listener : mOnSensitiveContentRedactionChangeListeners) {
+                    listener.run();
+                }
+                mEntryManagerLazy.get().updateNotifications("LOCK_SCREEN_SHOW_NOTIFICATIONS,"
                         + " or LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS change");
             }
         };
@@ -253,7 +270,7 @@
             public void onChange(boolean selfChange) {
                 updateLockscreenNotificationSetting();
                 if (mDeviceProvisionedController.isDeviceProvisioned()) {
-                    getEntryManager().updateNotifications("LOCK_SCREEN_ALLOW_REMOTE_INPUT"
+                    mEntryManagerLazy.get().updateNotifications("LOCK_SCREEN_ALLOW_REMOTE_INPUT"
                             + " or ZEN_MODE change");
                 }
             }
@@ -295,6 +312,7 @@
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_USER_SWITCHED);
         filter.addAction(Intent.ACTION_USER_ADDED);
+        filter.addAction(Intent.ACTION_USER_REMOVED);
         filter.addAction(Intent.ACTION_USER_UNLOCKED);
         filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
         filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
@@ -312,14 +330,17 @@
         mSettingsObserver.onChange(false);  // set up
     }
 
+    @Override
     public boolean shouldShowLockscreenNotifications() {
         return mShowLockscreenNotifications;
     }
 
+    @Override
     public boolean shouldAllowLockscreenRemoteInput() {
         return mAllowLockscreenRemoteInput;
     }
 
+    @Override
     public boolean isCurrentProfile(int userId) {
         synchronized (mLock) {
             return userId == UserHandle.USER_ALL || mCurrentProfiles.get(userId) != null;
@@ -334,7 +355,7 @@
         if (userId == UserHandle.USER_ALL) {
             userId = mCurrentUserId;
         }
-        boolean inLockdown = Dependency.get(KeyguardUpdateMonitor.class).isUserInLockdown(userId);
+        boolean inLockdown = mKeyguardUpdateMonitor.isUserInLockdown(userId);
         mUsersInLockdownLatestResult.put(userId, inLockdown);
         return inLockdown;
     }
@@ -343,6 +364,7 @@
      * Returns true if we're on a secure lockscreen and the user wants to hide notification data.
      * If so, notifications should be hidden.
      */
+    @Override
     public boolean shouldHideNotifications(int userId) {
         boolean hide = isLockscreenPublicMode(userId) && !userAllowsNotificationsInPublic(userId)
                 || (userId != mCurrentUserId && shouldHideNotifications(mCurrentUserId))
@@ -355,6 +377,7 @@
      * Returns true if we're on a secure lockscreen and the user wants to hide notifications via
      * package-specific override.
      */
+    @Override
     public boolean shouldHideNotifications(String key) {
         if (mCommonNotifCollectionLazy.get() == null) {
             Log.wtf(TAG, "mCommonNotifCollectionLazy was null!", new Throwable());
@@ -365,6 +388,7 @@
                 && visibleEntry.getRanking().getLockscreenVisibilityOverride() == VISIBILITY_SECRET;
     }
 
+    @Override
     public boolean shouldShowOnKeyguard(NotificationEntry entry) {
         if (mCommonNotifCollectionLazy.get() == null) {
             Log.wtf(TAG, "mCommonNotifCollectionLazy was null!", new Throwable());
@@ -387,14 +411,6 @@
         return mShowLockscreenNotifications && exceedsPriorityThreshold;
     }
 
-    private void setShowLockscreenNotifications(boolean show) {
-        mShowLockscreenNotifications = show;
-    }
-
-    private void setLockscreenAllowRemoteInput(boolean allowLockscreenRemoteInput) {
-        mAllowLockscreenRemoteInput = allowLockscreenRemoteInput;
-    }
-
     protected void updateLockscreenNotificationSetting() {
         final boolean show = mSecureSettings.getIntForUser(
                 Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
@@ -408,7 +424,7 @@
         mHideSilentNotificationsOnLockscreen = mSecureSettings.getIntForUser(
                 Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 1, mCurrentUserId) == 0;
 
-        setShowLockscreenNotifications(show && allowedByDpm);
+        mShowLockscreenNotifications = show && allowedByDpm;
 
         if (ENABLE_LOCK_SCREEN_ALLOW_REMOTE_INPUT) {
             final boolean remoteInput = mSecureSettings.getIntForUser(
@@ -418,9 +434,9 @@
             final boolean remoteInputDpm =
                     (dpmFlags & DevicePolicyManager.KEYGUARD_DISABLE_REMOTE_INPUT) == 0;
 
-            setLockscreenAllowRemoteInput(remoteInput && remoteInputDpm);
+            mAllowLockscreenRemoteInput = remoteInput && remoteInputDpm;
         } else {
-            setLockscreenAllowRemoteInput(false);
+            mAllowLockscreenRemoteInput = false;
         }
     }
 
@@ -428,7 +444,7 @@
      * Has the given user chosen to allow their private (full) notifications to be shown even
      * when the lockscreen is in "public" (secure & locked) mode?
      */
-    public boolean userAllowsPrivateNotificationsInPublic(int userHandle) {
+    protected boolean userAllowsPrivateNotificationsInPublic(int userHandle) {
         if (userHandle == UserHandle.USER_ALL) {
             return true;
         }
@@ -473,10 +489,12 @@
     /**
      * Save the current "public" (locked and secure) state of the lockscreen.
      */
+    @Override
     public void setLockscreenPublicMode(boolean publicMode, int userId) {
         mLockscreenPublicMode.put(userId, publicMode);
     }
 
+    @Override
     public boolean isLockscreenPublicMode(int userId) {
         if (userId == UserHandle.USER_ALL) {
             return mLockscreenPublicMode.get(mCurrentUserId, false);
@@ -493,6 +511,7 @@
      * Has the given user chosen to allow notifications to be shown even when the lockscreen is in
      * "public" (secure & locked) mode?
      */
+    @Override
     public boolean userAllowsNotificationsInPublic(int userHandle) {
         if (isCurrentProfile(userHandle) && userHandle != mCurrentUserId) {
             return true;
@@ -513,36 +532,37 @@
     }
 
     /** @return true if the entry needs redaction when on the lockscreen. */
-    public boolean needsRedaction(NotificationEntry ent) {
+    @Override
+    public boolean notifNeedsRedactionInPublic(NotificationEntry ent) {
         int userId = ent.getSbn().getUserId();
+        return ent.hasSensitiveContents() && sensitiveNotifsNeedRedactionInPublic(userId);
+    }
 
+    @Override
+    public boolean sensitiveNotifsNeedRedactionInPublic(int userId) {
         boolean isCurrentUserRedactingNotifs =
                 !userAllowsPrivateNotificationsInPublic(mCurrentUserId);
+        if (userId == mCurrentUserId) {
+            return isCurrentUserRedactingNotifs;
+        }
+
         boolean isNotifForManagedProfile = mCurrentManagedProfiles.contains(userId);
         boolean isNotifUserRedacted = !userAllowsPrivateNotificationsInPublic(userId);
 
         // redact notifications if the current user is redacting notifications; however if the
         // notification is associated with a managed profile, we rely on the managed profile
         // setting to determine whether to redact it
-        boolean isNotifRedacted = (!isNotifForManagedProfile && isCurrentUserRedactingNotifs)
-                || isNotifUserRedacted;
-
-        boolean notificationRequestsRedaction =
-                ent.getSbn().getNotification().visibility == Notification.VISIBILITY_PRIVATE;
-        boolean userForcesRedaction = packageHasVisibilityOverride(ent.getSbn().getKey());
-
-        return userForcesRedaction || notificationRequestsRedaction && isNotifRedacted;
+        return (!isNotifForManagedProfile && isCurrentUserRedactingNotifs) || isNotifUserRedacted;
     }
 
-    private boolean packageHasVisibilityOverride(String key) {
-        if (mCommonNotifCollectionLazy.get() == null) {
-            Log.wtf(TAG, "mEntryManager was null!", new Throwable());
-            return true;
-        }
-        NotificationEntry entry = mCommonNotifCollectionLazy.get().getEntry(key);
-        return entry != null
-                && entry.getRanking().getLockscreenVisibilityOverride() 
-                == Notification.VISIBILITY_PRIVATE;
+    @Override
+    public void addOnNeedsRedactionInPublicChangedListener(Runnable listener) {
+        mOnSensitiveContentRedactionChangeListeners.addIfAbsent(listener);
+    }
+
+    @Override
+    public void removeOnNeedsRedactionInPublicChangedListener(Runnable listener) {
+        mOnSensitiveContentRedactionChangeListeners.remove(listener);
     }
 
     private void updateCurrentProfilesCache() {
@@ -562,12 +582,16 @@
             for (UserChangedListener listener : mListeners) {
                 listener.onCurrentProfilesChanged(mCurrentProfiles);
             }
+            for (Runnable listener : mOnSensitiveContentRedactionChangeListeners) {
+                listener.run();
+            }
         });
     }
 
     /**
      * If any of the profiles are in public mode.
      */
+    @Override
     public boolean isAnyProfilePublicMode() {
         synchronized (mLock) {
             for (int i = mCurrentProfiles.size() - 1; i >= 0; i--) {
@@ -596,10 +620,12 @@
     /**
      * Returns the current user id. This can change if the user is switched.
      */
+    @Override
     public int getCurrentUserId() {
         return mCurrentUserId;
     }
 
+    @Override
     public SparseArray<UserInfo> getCurrentProfiles() {
         return mCurrentProfiles;
     }
@@ -640,7 +666,8 @@
             setLockscreenPublicMode(isProfilePublic, userId);
             mUsersWithSeparateWorkChallenge.put(userId, needsSeparateChallenge);
         }
-        getEntryManager().updateNotifications("NotificationLockscreenUserManager.updatePublicMode");
+        mEntryManagerLazy.get()
+                .updateNotifications("NotificationLockscreenUserManager.updatePublicMode");
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 734bc48..0d60401 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -326,9 +326,9 @@
                     || child.isPinned();
             boolean isLastChild = child == lastChild;
             final float viewStart = child.getTranslationY();
-
-            final float inShelfAmount = updateShelfTransformation(i, child, scrollingFast,
-                    expandingAnimated, isLastChild);
+            final float shelfClipStart = getTranslationY() - mPaddingBetweenElements;
+            final float inShelfAmount = getAmountInShelf(i, child, scrollingFast,
+                    expandingAnimated, isLastChild, shelfClipStart);
 
             // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount
             if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) {
@@ -609,10 +609,18 @@
     }
 
     /**
-     * @return the amount how much this notification is in the shelf
+     * @param i Index of the view in the host layout.
+     * @param view The current ExpandableView.
+     * @param scrollingFast Whether we are scrolling fast.
+     * @param expandingAnimated Whether we are expanding a notification.
+     * @param isLastChild Whether this is the last view.
+     * @param shelfClipStart The point at which notifications start getting clipped by the shelf.
+     * @return The amount how much this notification is in the shelf.
+     *         0f is not in shelf. 1f is completely in shelf.
      */
-    private float updateShelfTransformation(int i, ExpandableView view, boolean scrollingFast,
-            boolean expandingAnimated, boolean isLastChild) {
+    @VisibleForTesting
+    public float getAmountInShelf(int i, ExpandableView view, boolean scrollingFast,
+            boolean expandingAnimated, boolean isLastChild, float shelfClipStart) {
 
         // Let's calculate how much the view is in the shelf
         float viewStart = view.getTranslationY();
@@ -635,29 +643,33 @@
         float viewEnd = viewStart + fullHeight;
         float fullTransitionAmount = 0.0f;
         float iconTransitionAmount = 0.0f;
-        float shelfStart = getTranslationY() - mPaddingBetweenElements;
+
+        // Don't animate shelf icons during shade expansion.
         if (mAmbientState.isExpansionChanging() && !mAmbientState.isOnKeyguard()) {
             // TODO(b/172289889) handle icon placement for notification that is clipped by the shelf
             if (mIndexOfFirstViewInShelf != -1 && i >= mIndexOfFirstViewInShelf) {
                 fullTransitionAmount = 1f;
                 iconTransitionAmount = 1f;
             }
-        } else if (viewEnd >= shelfStart
+
+        } else if (viewEnd >= shelfClipStart
                 && (!mAmbientState.isUnlockHintRunning() || view.isInShelf())
                 && (mAmbientState.isShadeExpanded()
                 || (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) {
 
-            if (viewStart < shelfStart) {
-                float fullAmount = (shelfStart - viewStart) / fullHeight;
+            if (viewStart < shelfClipStart && Math.abs(viewStart - shelfClipStart) > 0.001f) {
+                // Partially clipped by shelf.
+                float fullAmount = (shelfClipStart - viewStart) / fullHeight;
                 fullAmount = Math.min(1.0f, fullAmount);
                 fullTransitionAmount = 1.0f - fullAmount;
                 if (isLastChild) {
                     // Reduce icon transform distance to completely fade in shelf icon
                     // by the time the notification icon fades out, and vice versa
-                    iconTransitionAmount = (shelfStart - viewStart)
+                    iconTransitionAmount = (shelfClipStart - viewStart)
                             / (iconTransformStart - viewStart);
                 } else {
-                    iconTransitionAmount = (shelfStart - iconTransformStart) / transformDistance;
+                    iconTransitionAmount = (shelfClipStart - iconTransformStart)
+                            / transformDistance;
                 }
                 iconTransitionAmount = MathUtils.constrain(iconTransitionAmount, 0.0f, 1.0f);
                 iconTransitionAmount = 1.0f - iconTransitionAmount;
@@ -772,6 +784,9 @@
     }
 
     private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) {
+        if (mShelfIcons == null) {
+            return null;
+        }
         return mShelfIcons.getIconState(icon);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
index 054543c..30cb09d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
@@ -53,6 +53,7 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
 import java.util.Stack;
@@ -207,12 +208,9 @@
                     || !mLockscreenUserManager.needsSeparateWorkChallenge(userId))) {
                 userPublic = false;
             }
-            boolean needsRedaction = mLockscreenUserManager.needsRedaction(ent);
+            boolean needsRedaction = mLockscreenUserManager.notifNeedsRedactionInPublic(ent);
             boolean sensitive = userPublic && needsRedaction;
-            boolean deviceSensitive = devicePublic
-                    && !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(
-                    currentUserId);
-            ent.setSensitive(sensitive, deviceSensitive);
+            ent.getRow().setSensitive(sensitive);
             ent.getRow().setNeedsRedaction(needsRedaction);
             mLowPriorityInflationHelper.recheckLowPriorityViewAndInflate(ent, ent.getRow());
             boolean isChildInGroup = mGroupManager.isChildInGroup(ent);
@@ -365,6 +363,8 @@
         boolean hasClearableAlertingNotifs = false;
         boolean hasNonClearableSilentNotifs = false;
         boolean hasClearableSilentNotifs = false;
+        HashSet<Integer> clearableAlertingSensitiveNotifUsers = new HashSet<>();
+        HashSet<Integer> clearableSilentSensitiveNotifUsers = new HashSet<>();
         final int childCount = mListContainer.getContainerChildCount();
         int visibleTopLevelEntries = 0;
         for (int i = 0; i < childCount; i++) {
@@ -376,10 +376,11 @@
                 continue;
             }
             final ExpandableNotificationRow row = (ExpandableNotificationRow) child;
-            boolean isSilent = row.getEntry().getBucket() == BUCKET_SILENT;
+            NotificationEntry entry = row.getEntry();
+            boolean isSilent = entry.getBucket() == BUCKET_SILENT;
             // NOTE: NotificationEntry.isClearable() will internally check group children to ensure
             //  the group itself definitively clearable.
-            boolean isClearable = row.getEntry().isClearable();
+            boolean isClearable = entry.isClearable();
             visibleTopLevelEntries++;
             if (isSilent) {
                 if (isClearable) {
@@ -394,13 +395,24 @@
                     hasNonClearableAlertingNotifs = true;
                 }
             }
+            if (isClearable && entry.hasSensitiveContents()) {
+                int userId = entry.getSbn().getUserId();
+                if (isSilent) {
+                    clearableSilentSensitiveNotifUsers.add(userId);
+                } else {
+                    clearableAlertingSensitiveNotifUsers.add(userId);
+                }
+            }
         }
         mStackController.setNotifStats(new NotifStats(
                 visibleTopLevelEntries /* numActiveNotifs */,
                 hasNonClearableAlertingNotifs /* hasNonClearableAlertingNotifs */,
                 hasClearableAlertingNotifs /* hasClearableAlertingNotifs */,
                 hasNonClearableSilentNotifs /* hasNonClearableSilentNotifs */,
-                hasClearableSilentNotifs /* hasClearableSilentNotifs */
+                hasClearableSilentNotifs /* hasClearableSilentNotifs */,
+                clearableAlertingSensitiveNotifUsers /* clearableAlertingSensitiveNotifUsers */,
+                clearableSilentSensitiveNotifUsers /* clearableSilentSensitiveNotifUsers */
+
         ));
         Trace.endSection();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 7800b4c..8405aea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -22,6 +22,7 @@
 import android.animation.ValueAnimator
 import android.content.Context
 import android.graphics.Rect
+import android.view.ContextThemeWrapper
 import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.View
@@ -46,6 +47,7 @@
 ) : SystemStatusAnimationCallback {
 
     private lateinit var animationWindowView: FrameLayout
+    private lateinit var themedContext: ContextThemeWrapper
 
     private var currentAnimatedView: BackgroundAnimatableView? = null
 
@@ -76,7 +78,7 @@
 
         // Initialize the animated view
         val insets = contentInsetsProvider.getStatusBarContentInsetsForCurrentRotation()
-        currentAnimatedView = viewCreator(context).also {
+        currentAnimatedView = viewCreator(themedContext).also {
             animationWindowView.addView(
                     it.view,
                     layoutParamsDefault(
@@ -221,7 +223,8 @@
 
     private fun init() {
         initialized = true
-        animationWindowView = LayoutInflater.from(context)
+        themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
+        animationWindowView = LayoutInflater.from(themedContext)
                 .inflate(R.layout.system_event_animation_window, null) as FrameLayout
         val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
         lp.gravity = Gravity.END or Gravity.CENTER_VERTICAL
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
index a0ccd57..d16e9e5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
@@ -80,7 +80,7 @@
 
     @VisibleForTesting
     boolean isDynamicPrivacyEnabled() {
-        return !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(
+        return mLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic(
                 mLockscreenUserManager.getCurrentUserId());
     }
 
@@ -95,6 +95,10 @@
         mListeners.add(listener);
     }
 
+    public void removeListener(Listener listener) {
+        mListeners.remove(listener);
+    }
+
     /**
      * Is the notification shade currently in a locked down mode where it's fully showing but the
      * contents aren't revealed yet?
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
index 129fa5a..0c341cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.notification;
 
 import android.content.Intent;
-import android.service.notification.StatusBarNotification;
 import android.view.View;
 
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -29,7 +28,7 @@
  */
 public interface NotificationActivityStarter {
     /** Called when the user clicks on the surface of a notification. */
-    void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row);
+    void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row);
 
     /** Called when the user clicks on a button in the notification guts which fires an intent. */
     void startNotificationGutsIntent(Intent intent, int appUid,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
index 392145a..c3ce593 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
@@ -104,7 +104,7 @@
             mBubblesOptional.get().collapseStack();
         }
 
-        mNotificationActivityStarter.onNotificationClicked(entry.getSbn(), row);
+        mNotificationActivityStarter.onNotificationClicked(entry, row);
     }
 
     private boolean isMenuVisible(ExpandableNotificationRow row) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
index fbf033b..ad3dfed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt
@@ -27,7 +27,7 @@
 ) {
     fun logOnClick(entry: NotificationEntry) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry.key
+            str1 = entry.logKey
             str2 = entry.ranking.channel.id
         }, {
             "CLICK $str1 (channel=$str2)"
@@ -36,7 +36,7 @@
 
     fun logMenuVisible(entry: NotificationEntry) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry.key
+            str1 = entry.logKey
         }, {
             "Ignoring click on $str1; menu is visible"
         })
@@ -44,7 +44,7 @@
 
     fun logParentMenuVisible(entry: NotificationEntry) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry.key
+            str1 = entry.logKey
         }, {
             "Ignoring click on $str1; parent menu is visible"
         })
@@ -52,7 +52,7 @@
 
     fun logChildrenExpanded(entry: NotificationEntry) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry.key
+            str1 = entry.logKey
         }, {
             "Ignoring click on $str1; children are expanded"
         })
@@ -60,7 +60,7 @@
 
     fun logGutsExposed(entry: NotificationEntry) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = entry.key
+            str1 = entry.logKey
         }, {
             "Ignoring click on $str1; guts are exposed"
         })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
index c3cc97b..7cfb157 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
@@ -24,7 +24,8 @@
 
 import com.android.internal.util.ContrastColorUtil;
 import com.android.systemui.R;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.ListEntry;
+import com.android.systemui.util.Compile;
 
 /**
  * A util class for various reusable functions
@@ -74,12 +75,18 @@
         return (int) (dimensionPixelSize * factor);
     }
 
+    private static final boolean INCLUDE_HASH_CODE_IN_LIST_ENTRY_LOG_KEY = false;
+
     /** Get the notification key, reformatted for logging, for the (optional) entry */
-    public static String logKey(NotificationEntry entry) {
+    public static String logKey(ListEntry entry) {
         if (entry == null) {
             return "null";
         }
-        return logKey(entry.getKey());
+        if (Compile.IS_DEBUG && INCLUDE_HASH_CODE_IN_LIST_ENTRY_LOG_KEY) {
+            return logKey(entry.getKey()) + "@" + Integer.toHexString(entry.hashCode());
+        } else {
+            return logKey(entry.getKey());
+        }
     }
 
     /** Removes newlines from the notification key to prettify apps that have these in the tag */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.kt
new file mode 100644
index 0000000..432bac4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar.notification
+
+import android.service.notification.StatusBarNotification
+import com.android.systemui.statusbar.notification.collection.ListEntry
+
+/** Get the notification key, reformatted for logging, for the (optional) entry  */
+val ListEntry?.logKey: String?
+    get() = this?.let { NotificationUtils.logKey(it) }
+
+/** Get the notification key, reformatted for logging, for the (optional) sbn  */
+val StatusBarNotification?.logKey: String?
+    get() = this?.key?.let { NotificationUtils.logKey(it) }
+
+/** Removes newlines from the notification key to prettify apps that have these in the tag  */
+fun logKey(key: String?): String? = NotificationUtils.logKey(key)
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
index 9f9fba4..f84a6df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification;
 
+import android.util.MathUtils;
 import android.util.Pools;
 import android.view.View;
 import android.view.animation.Interpolator;
@@ -98,7 +99,10 @@
         if (sameAs(otherState)) {
             ensureVisible();
         } else {
-            CrossFadeHelper.fadeIn(mTransformedView, transformationAmount, true /* remap */);
+            CrossFadeHelper.fadeIn(
+                    mTransformedView,
+                    MathUtils.constrainedMap(0, 1, 0.45f, 1, transformationAmount),
+                    false /* remap */);
         }
         transformViewFullyFrom(otherState, transformationAmount);
     }
@@ -294,7 +298,10 @@
             }
             return false;
         } else {
-            CrossFadeHelper.fadeOut(mTransformedView, transformationAmount);
+            CrossFadeHelper.fadeOut(
+                    mTransformedView,
+                    MathUtils.constrainedMap(0, 1, 0, 0.55f, transformationAmount),
+                    false /* remap */);
         }
         transformViewFullyTo(otherState, transformationAmount);
         return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index bcd8e59..6085096 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -18,9 +18,12 @@
 
 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL;
+import static android.service.notification.NotificationListenerService.REASON_ASSISTANT_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
 import static android.service.notification.NotificationListenerService.REASON_CANCEL_ALL;
 import static android.service.notification.NotificationListenerService.REASON_CHANNEL_BANNED;
+import static android.service.notification.NotificationListenerService.REASON_CHANNEL_REMOVED;
+import static android.service.notification.NotificationListenerService.REASON_CLEAR_DATA;
 import static android.service.notification.NotificationListenerService.REASON_CLICK;
 import static android.service.notification.NotificationListenerService.REASON_ERROR;
 import static android.service.notification.NotificationListenerService.REASON_GROUP_OPTIMIZATION;
@@ -36,9 +39,11 @@
 import static android.service.notification.NotificationListenerService.REASON_UNAUTOBUNDLED;
 import static android.service.notification.NotificationListenerService.REASON_USER_STOPPED;
 
+import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.DISMISSED;
 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED;
 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
+import static com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLoggerKt.cancellationReasonDebugString;
 
 import static java.util.Objects.requireNonNull;
 
@@ -99,6 +104,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -143,6 +149,7 @@
     private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>();
     private final Collection<NotificationEntry> mReadOnlyNotificationSet =
             Collections.unmodifiableCollection(mNotificationSet.values());
+    private final HashMap<String, FutureDismissal> mFutureDismissals = new HashMap<>();
 
     @Nullable private CollectionReadyForBuildListener mBuildListener;
     private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>();
@@ -511,6 +518,7 @@
             cancelDismissInterception(entry);
             mEventQueue.add(new EntryRemovedEvent(entry, entry.mCancellationReason));
             mEventQueue.add(new CleanUpEntryEvent(entry));
+            handleFutureDismissal(entry);
             return true;
         } else {
             return false;
@@ -519,31 +527,32 @@
 
     /**
      * Get the group summary entry
-     * @param group
+     * @param groupKey
      * @return
      */
     @Nullable
-    public NotificationEntry getGroupSummary(String group) {
+    public NotificationEntry getGroupSummary(String groupKey) {
         return mNotificationSet
                 .values()
                 .stream()
-                .filter(it -> Objects.equals(it.getSbn().getGroup(), group))
+                .filter(it -> Objects.equals(it.getSbn().getGroupKey(), groupKey))
                 .filter(it -> it.getSbn().getNotification().isGroupSummary())
                 .findFirst().orElse(null);
     }
 
     /**
-     * Checks if the entry is the only child in the logical group
-     * @param entry
-     * @return
+     * Checks if the entry is the only child in the logical group;
+     * it need not have a summary to qualify
+     *
+     * @param entry the entry to check
      */
     public boolean isOnlyChildInGroup(NotificationEntry entry) {
-        String group = entry.getSbn().getGroup();
+        String groupKey = entry.getSbn().getGroupKey();
         return mNotificationSet.get(entry.getKey()) == entry
                 && mNotificationSet
                 .values()
                 .stream()
-                .filter(it -> Objects.equals(it.getSbn().getGroup(), group))
+                .filter(it -> Objects.equals(it.getSbn().getGroupKey(), groupKey))
                 .filter(it -> !it.getSbn().getNotification().isGroupSummary())
                 .count() == 1;
     }
@@ -916,10 +925,139 @@
         dispatchEventsAndRebuildList();
     }
 
+    /**
+     * A method to alert the collection that an async operation is happening, at the end of which a
+     * dismissal request will be made.  This method has the additional guarantee that if a parent
+     * notification exists for a single child, then that notification will also be dismissed.
+     *
+     * The runnable returned must be run at the end of the async operation to enact the cancellation
+     *
+     * @param entry the notification we want to dismiss
+     * @param cancellationReason the reason for the cancellation
+     * @param statsCreator the callback for generating the stats for an entry
+     * @return the runnable to be run when the dismissal is ready to happen
+     */
+    public Runnable registerFutureDismissal(NotificationEntry entry, int cancellationReason,
+            DismissedByUserStatsCreator statsCreator) {
+        FutureDismissal dismissal = mFutureDismissals.get(entry.getKey());
+        if (dismissal != null) {
+            mLogger.logFutureDismissalReused(dismissal);
+            return dismissal;
+        }
+        dismissal = new FutureDismissal(entry, cancellationReason, statsCreator);
+        mFutureDismissals.put(entry.getKey(), dismissal);
+        mLogger.logFutureDismissalRegistered(dismissal);
+        return dismissal;
+    }
+
+    private void handleFutureDismissal(NotificationEntry entry) {
+        final FutureDismissal futureDismissal = mFutureDismissals.remove(entry.getKey());
+        if (futureDismissal != null) {
+            futureDismissal.onSystemServerCancel(entry.mCancellationReason);
+        }
+    }
+
+    /** A single method interface that callers can pass in when registering future dismissals */
+    public interface DismissedByUserStatsCreator {
+        DismissedByUserStats createDismissedByUserStats(NotificationEntry entry);
+    }
+
+    /** A class which tracks the double dismissal events coming in from both the system server and
+     * the ui */
+    public class FutureDismissal implements Runnable {
+        private final NotificationEntry mEntry;
+        private final DismissedByUserStatsCreator mStatsCreator;
+        @Nullable
+        private final NotificationEntry mSummaryToDismiss;
+        private final String mLabel;
+
+        private boolean mDidRun;
+        private boolean mDidSystemServerCancel;
+
+        private FutureDismissal(NotificationEntry entry, @CancellationReason int cancellationReason,
+                DismissedByUserStatsCreator statsCreator) {
+            mEntry = entry;
+            mStatsCreator = statsCreator;
+            mSummaryToDismiss = fetchSummaryToDismiss(entry);
+            mLabel = "<FutureDismissal@" + Integer.toHexString(hashCode())
+                    + " entry=" + logKey(mEntry)
+                    + " reason=" + cancellationReasonDebugString(cancellationReason)
+                    + " summary=" + logKey(mSummaryToDismiss)
+                    + ">";
+        }
+
+        @Nullable
+        private NotificationEntry fetchSummaryToDismiss(NotificationEntry entry) {
+            if (isOnlyChildInGroup(entry)) {
+                String group = entry.getSbn().getGroupKey();
+                NotificationEntry summary = getGroupSummary(group);
+                if (summary != null && summary.isDismissable()) return summary;
+            }
+            return null;
+        }
+
+        /** called when the entry has been removed from the collection */
+        public void onSystemServerCancel(@CancellationReason int cancellationReason) {
+            Assert.isMainThread();
+            if (mDidSystemServerCancel) {
+                mLogger.logFutureDismissalDoubleCancelledByServer(this);
+                return;
+            }
+            mLogger.logFutureDismissalGotSystemServerCancel(this, cancellationReason);
+            mDidSystemServerCancel = true;
+            // TODO: Internally dismiss the summary now instead of waiting for onUiCancel
+        }
+
+        private void onUiCancel() {
+            mFutureDismissals.remove(mEntry.getKey());
+            final NotificationEntry currentEntry = getEntry(mEntry.getKey());
+            // generate stats for the entry before dismissing summary, which could affect state
+            final DismissedByUserStats stats = mStatsCreator.createDismissedByUserStats(mEntry);
+            // dismiss the summary (if it exists)
+            if (mSummaryToDismiss != null) {
+                final NotificationEntry currentSummary = getEntry(mSummaryToDismiss.getKey());
+                if (currentSummary == mSummaryToDismiss) {
+                    mLogger.logFutureDismissalDismissing(this, "summary");
+                    dismissNotification(mSummaryToDismiss,
+                            mStatsCreator.createDismissedByUserStats(mSummaryToDismiss));
+                } else {
+                    mLogger.logFutureDismissalMismatchedEntry(this, "summary", currentSummary);
+                }
+            }
+            // dismiss this entry (if it is still around)
+            if (mDidSystemServerCancel) {
+                mLogger.logFutureDismissalAlreadyCancelledByServer(this);
+            } else if (currentEntry == mEntry) {
+                mLogger.logFutureDismissalDismissing(this, "entry");
+                dismissNotification(mEntry, stats);
+            } else {
+                mLogger.logFutureDismissalMismatchedEntry(this, "entry", currentEntry);
+            }
+        }
+
+        /** called when the dismissal should be completed */
+        @Override
+        public void run() {
+            Assert.isMainThread();
+            if (mDidRun) {
+                mLogger.logFutureDismissalDoubleRun(this);
+                return;
+            }
+            mDidRun = true;
+            onUiCancel();
+        }
+
+        /** provides a debug label for this instance */
+        public String getLabel() {
+            return mLabel;
+        }
+    }
+
     @IntDef(prefix = { "REASON_" }, value = {
             REASON_NOT_CANCELED,
             REASON_UNKNOWN,
             REASON_CLICK,
+            REASON_CANCEL,
             REASON_CANCEL_ALL,
             REASON_ERROR,
             REASON_PACKAGE_CHANGED,
@@ -937,6 +1075,9 @@
             REASON_CHANNEL_BANNED,
             REASON_SNOOZED,
             REASON_TIMEOUT,
+            REASON_CHANNEL_REMOVED,
+            REASON_CLEAR_DATA,
+            REASON_ASSISTANT_CANCEL,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CancellationReason {}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 4fc347a..e3c39dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -169,9 +169,6 @@
      */
     private boolean hasSentReply;
 
-    private boolean mSensitive = true;
-    private List<OnSensitivityChangedListener> mOnSensitivityChangedListeners = new ArrayList<>();
-
     private boolean mAutoHeadsUp;
     private boolean mPulseSupressed;
     private int mBucket = BUCKET_ALERTING;
@@ -867,33 +864,29 @@
     }
 
     /**
-     * Set this notification to be sensitive.
-     *
-     * @param sensitive true if the content of this notification is sensitive right now
-     * @param deviceSensitive true if the device in general is sensitive right now
+     * Returns the visibility of this notification on the lockscreen, taking into account both the
+     * notification's defined visibility, as well as the visibility override as determined by the
+     * device policy.
      */
-    public void setSensitive(boolean sensitive, boolean deviceSensitive) {
-        getRow().setSensitive(sensitive, deviceSensitive);
-        if (sensitive != mSensitive) {
-            mSensitive = sensitive;
-            for (int i = 0; i < mOnSensitivityChangedListeners.size(); i++) {
-                mOnSensitivityChangedListeners.get(i).onSensitivityChanged(this);
-            }
+    public int getLockscreenVisibility() {
+        int setting = mRanking.getLockscreenVisibilityOverride();
+        if (setting == Ranking.VISIBILITY_NO_OVERRIDE) {
+            setting = mSbn.getNotification().visibility;
         }
+        return setting;
     }
 
-    public boolean isSensitive() {
-        return mSensitive;
-    }
-
-    /** Add a listener to be notified when the entry's sensitivity changes. */
-    public void addOnSensitivityChangedListener(OnSensitivityChangedListener listener) {
-        mOnSensitivityChangedListeners.add(listener);
-    }
-
-    /** Remove a listener that was registered above. */
-    public void removeOnSensitivityChangedListener(OnSensitivityChangedListener listener) {
-        mOnSensitivityChangedListeners.remove(listener);
+    /**
+     * Does this notification contain sensitive content? If the user's settings specify, then this
+     * content would need to be redacted when the device this public.
+     *
+     * NOTE: If the notification's visibility setting is VISIBILITY_SECRET, then this will return
+     * false; SECRET notifications are omitted entirely when the device is public, so effectively
+     * the contents of the notification are not sensitive whenever the notification is actually
+     * visible.
+     */
+    public boolean hasSensitiveContents() {
+        return getLockscreenVisibility() == Notification.VISIBILITY_PRIVATE;
     }
 
     public boolean isPulseSuppressed() {
@@ -954,12 +947,6 @@
         }
     }
 
-    /** Listener interface for {@link #addOnSensitivityChangedListener} */
-    public interface OnSensitivityChangedListener {
-        /** Called when the sensitivity changes */
-        void onSensitivityChanged(@NonNull NotificationEntry entry);
-    }
-
     /** @see #getDismissState() */
     public enum DismissState {
         /** User has not dismissed this notif or its parent */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index 1b5e52d..df2fe4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -93,6 +93,7 @@
     private final NotificationInteractionTracker mInteractionTracker;
     private final DumpManager mDumpManager;
     // used exclusivly by ShadeListBuilder#notifySectionEntriesUpdated
+    // TODO replace temp with collection pool for readability
     private final ArrayList<ListEntry> mTempSectionMembers = new ArrayList<>();
     private final boolean mAlwaysLogList;
 
@@ -230,13 +231,7 @@
         mPipelineState.requireState(STATE_IDLE);
 
         mNotifSections.clear();
-        NotifSectioner lastSection = null;
         for (NotifSectioner sectioner : sectioners) {
-            if (lastSection != null && lastSection.getBucket() > sectioner.getBucket()) {
-                throw new IllegalArgumentException("setSectioners with non contiguous sections "
-                        + lastSection.getName() + " - " + lastSection.getBucket() + " & "
-                        + sectioner.getName() + " - " + sectioner.getBucket());
-            }
             final NotifSection section = new NotifSection(sectioner, mNotifSections.size());
             final NotifComparator sectionComparator = section.getComparator();
             mNotifSections.add(section);
@@ -244,10 +239,23 @@
             if (sectionComparator != null) {
                 sectionComparator.setInvalidationListener(this::onNotifComparatorInvalidated);
             }
-            lastSection = sectioner;
         }
 
         mNotifSections.add(new NotifSection(DEFAULT_SECTIONER, mNotifSections.size()));
+
+        // validate sections
+        final ArraySet<Integer> seenBuckets = new ArraySet<>();
+        int lastBucket = mNotifSections.size() > 0
+                ? mNotifSections.get(0).getBucket()
+                : 0;
+        for (NotifSection section : mNotifSections) {
+            if (lastBucket != section.getBucket() && seenBuckets.contains(section.getBucket())) {
+                throw new IllegalStateException("setSectioners with non contiguous sections "
+                        + section.getLabel() + " has an already seen bucket");
+            }
+            lastBucket = section.getBucket();
+            seenBuckets.add(lastBucket);
+        }
     }
 
     void setNotifStabilityManager(@NonNull NotifStabilityManager notifStabilityManager) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinator.kt
deleted file mode 100644
index b54163d..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinator.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.collection.coordinator
-
-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.notifcollection.NotifLifetimeExtender
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
-import com.android.systemui.statusbar.phone.NotifActivityLaunchEvents
-import dagger.Binds
-import dagger.Module
-import javax.inject.Inject
-
-/** Extends the lifetime of notifications while their activity launch animation is playing. */
-interface ActivityLaunchAnimCoordinator : Coordinator
-
-/** Provides an [ActivityLaunchAnimCoordinator] to [CoordinatorScope]. */
-@Module(includes = [PrivateActivityStarterCoordinatorModule::class])
-object ActivityLaunchAnimCoordinatorModule
-
-@Module
-private interface PrivateActivityStarterCoordinatorModule {
-    @Binds
-    fun bindCoordinator(impl: ActivityLaunchAnimCoordinatorImpl): ActivityLaunchAnimCoordinator
-}
-
-/**
- * Listens for [NotifActivityLaunchEvents], and then extends the lifetimes of any notifs while their
- * launch animation is playing.
- */
-@CoordinatorScope
-private class ActivityLaunchAnimCoordinatorImpl @Inject constructor(
-    private val activityLaunchEvents: NotifActivityLaunchEvents
-) : ActivityLaunchAnimCoordinator {
-    // Tracks notification launches, and whether or not their lifetimes are extended.
-    private val notifsLaunchingActivities = mutableMapOf<String, Boolean>()
-
-    private var onEndLifetimeExtensionCallback: OnEndLifetimeExtensionCallback? = null
-
-    override fun attach(pipeline: NotifPipeline) {
-        activityLaunchEvents.registerListener(activityStartEventListener)
-        pipeline.addNotificationLifetimeExtender(extender)
-    }
-
-    private val activityStartEventListener = object : NotifActivityLaunchEvents.Listener {
-        override fun onStartLaunchNotifActivity(entry: NotificationEntry) {
-            notifsLaunchingActivities[entry.key] = false
-        }
-
-        override fun onFinishLaunchNotifActivity(entry: NotificationEntry) {
-            if (notifsLaunchingActivities.remove(entry.key) == true) {
-                // If we were extending the lifetime of this notification, stop.
-                onEndLifetimeExtensionCallback?.onEndLifetimeExtension(extender, entry)
-            }
-        }
-    }
-
-    private val extender = object : NotifLifetimeExtender {
-        override fun getName(): String = "ActivityStarterCoordinator"
-
-        override fun setCallback(callback: OnEndLifetimeExtensionCallback) {
-            onEndLifetimeExtensionCallback = callback
-        }
-
-        override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
-            if (entry.key in notifsLaunchingActivities) {
-                // Track that we're now extending this notif
-                notifsLaunchingActivities[entry.key] = true
-                return true
-            }
-            return false
-        }
-
-        override fun cancelLifetimeExtension(entry: NotificationEntry) {
-            if (entry.key in notifsLaunchingActivities) {
-                notifsLaunchingActivities[entry.key] = false
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 3516625..0b3a0dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -56,8 +56,6 @@
     smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator,
     viewConfigCoordinator: ViewConfigCoordinator,
     visualStabilityCoordinator: VisualStabilityCoordinator,
-    sensitiveContentCoordinator: SensitiveContentCoordinator,
-    activityLaunchAnimCoordinator: ActivityLaunchAnimCoordinator
 ) : NotifCoordinators {
 
     private val mCoordinators: MutableList<Coordinator> = ArrayList()
@@ -94,8 +92,6 @@
         mCoordinators.add(shadeEventCoordinator)
         mCoordinators.add(viewConfigCoordinator)
         mCoordinators.add(visualStabilityCoordinator)
-        mCoordinators.add(sensitiveContentCoordinator)
-        mCoordinators.add(activityLaunchAnimCoordinator)
         if (notifPipelineFlags.isSmartspaceDedupingEnabled()) {
             mCoordinators.add(smartspaceDedupingCoordinator)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
index 57fd197..56484010 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -29,13 +28,11 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
 import com.android.systemui.statusbar.notification.collection.render.NodeController;
-import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
 import com.android.systemui.statusbar.notification.dagger.AlertingHeader;
 import com.android.systemui.statusbar.notification.dagger.SilentHeader;
 import com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt;
 
 import java.util.Collections;
-import java.util.List;
 
 import javax.inject.Inject;
 
@@ -53,10 +50,10 @@
     private final HighPriorityProvider mHighPriorityProvider;
     private final SectionClassifier mSectionClassifier;
     private final NodeController mSilentNodeController;
-    private final SectionHeaderController mSilentHeaderController;
     private final NodeController mAlertingHeaderController;
-    private boolean mHasSilentEntries;
-    private boolean mHasMinimizedEntries;
+    private final AlertingNotifSectioner mAlertingNotifSectioner = new AlertingNotifSectioner();
+    private final SilentNotifSectioner mSilentNotifSectioner = new SilentNotifSectioner();
+    private final MinimizedNotifSectioner mMinimizedNotifSectioner = new MinimizedNotifSectioner();
 
     @Inject
     public RankingCoordinator(
@@ -64,14 +61,12 @@
             HighPriorityProvider highPriorityProvider,
             SectionClassifier sectionClassifier,
             @AlertingHeader NodeController alertingHeaderController,
-            @SilentHeader SectionHeaderController silentHeaderController,
             @SilentHeader NodeController silentNodeController) {
         mStatusBarStateController = statusBarStateController;
         mHighPriorityProvider = highPriorityProvider;
         mSectionClassifier = sectionClassifier;
         mAlertingHeaderController = alertingHeaderController;
         mSilentNodeController = silentNodeController;
-        mSilentHeaderController = silentHeaderController;
     }
 
     @Override
@@ -95,82 +90,6 @@
         return mMinimizedNotifSectioner;
     }
 
-    private final NotifSectioner mAlertingNotifSectioner = new NotifSectioner("Alerting",
-            NotificationPriorityBucketKt.BUCKET_ALERTING) {
-        @Override
-        public boolean isInSection(ListEntry entry) {
-            return mHighPriorityProvider.isHighPriority(entry);
-        }
-
-        @Nullable
-        @Override
-        public NodeController getHeaderNodeController() {
-            // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mAlertingHeaderController
-            if (SHOW_ALL_SECTIONS) {
-                return mAlertingHeaderController;
-            }
-            return null;
-        }
-    };
-
-    private final NotifSectioner mSilentNotifSectioner = new NotifSectioner("Silent",
-            NotificationPriorityBucketKt.BUCKET_SILENT) {
-        @Override
-        public boolean isInSection(ListEntry entry) {
-            return !mHighPriorityProvider.isHighPriority(entry)
-                    && !entry.getRepresentativeEntry().isAmbient();
-        }
-
-        @Nullable
-        @Override
-        public NodeController getHeaderNodeController() {
-            return mSilentNodeController;
-        }
-
-        @Nullable
-        @Override
-        public void onEntriesUpdated(@NonNull List<ListEntry> entries) {
-            mHasSilentEntries = false;
-            for (int i = 0; i < entries.size(); i++) {
-                if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) {
-                    mHasSilentEntries = true;
-                    break;
-                }
-            }
-            mSilentHeaderController.setClearSectionEnabled(
-                    mHasSilentEntries | mHasMinimizedEntries);
-        }
-    };
-
-    private final NotifSectioner mMinimizedNotifSectioner = new NotifSectioner("Minimized",
-            NotificationPriorityBucketKt.BUCKET_SILENT) {
-        @Override
-        public boolean isInSection(ListEntry entry) {
-            return !mHighPriorityProvider.isHighPriority(entry)
-                    && entry.getRepresentativeEntry().isAmbient();
-        }
-
-        @Nullable
-        @Override
-        public NodeController getHeaderNodeController() {
-            return mSilentNodeController;
-        }
-
-        @Nullable
-        @Override
-        public void onEntriesUpdated(@NonNull List<ListEntry> entries) {
-            mHasMinimizedEntries = false;
-            for (int i = 0; i < entries.size(); i++) {
-                if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) {
-                    mHasMinimizedEntries = true;
-                    break;
-                }
-            }
-            mSilentHeaderController.setClearSectionEnabled(
-                    mHasSilentEntries | mHasMinimizedEntries);
-        }
-    };
-
     /**
      * Checks whether to filter out the given notification based the notification's Ranking object.
      * NotifListBuilder invalidates the notification list each time the ranking is updated,
@@ -202,4 +121,64 @@
                     mDndVisualEffectsFilter.invalidateList();
                 }
             };
+
+    private class AlertingNotifSectioner extends NotifSectioner {
+
+        AlertingNotifSectioner() {
+            super("Alerting", NotificationPriorityBucketKt.BUCKET_ALERTING);
+        }
+
+        @Override
+        public boolean isInSection(ListEntry entry) {
+            return mHighPriorityProvider.isHighPriority(entry);
+        }
+
+        @Nullable
+        @Override
+        public NodeController getHeaderNodeController() {
+            // TODO: remove SHOW_ALL_SECTIONS, this redundant method, and mAlertingHeaderController
+            if (SHOW_ALL_SECTIONS) {
+                return mAlertingHeaderController;
+            }
+            return null;
+        }
+    }
+
+    private class SilentNotifSectioner extends NotifSectioner {
+
+        SilentNotifSectioner() {
+            super("Silent", NotificationPriorityBucketKt.BUCKET_SILENT);
+        }
+
+        @Override
+        public boolean isInSection(ListEntry entry) {
+            return !mHighPriorityProvider.isHighPriority(entry)
+                    && !entry.getRepresentativeEntry().isAmbient();
+        }
+
+        @Nullable
+        @Override
+        public NodeController getHeaderNodeController() {
+            return mSilentNodeController;
+        }
+    }
+
+    private class MinimizedNotifSectioner extends NotifSectioner {
+
+        MinimizedNotifSectioner() {
+            super("Minimized", NotificationPriorityBucketKt.BUCKET_SILENT);
+        }
+
+        @Override
+        public boolean isInSection(ListEntry entry) {
+            return !mHighPriorityProvider.isHighPriority(entry)
+                    && entry.getRepresentativeEntry().isAmbient();
+        }
+
+        @Nullable
+        @Override
+        public NodeController getHeaderNodeController() {
+            return mSilentNodeController;
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
deleted file mode 100644
index 3f8a39f..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.collection.coordinator
-
-import android.os.UserHandle
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
-import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.notification.DynamicPrivacyController
-import com.android.systemui.statusbar.notification.collection.GroupEntry
-import com.android.systemui.statusbar.notification.collection.ListEntry
-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.Invalidator
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import dagger.Binds
-import dagger.Module
-import javax.inject.Inject
-
-@Module(includes = [PrivateSensitiveContentCoordinatorModule::class])
-interface SensitiveContentCoordinatorModule
-
-@Module
-private interface PrivateSensitiveContentCoordinatorModule {
-    @Binds
-    fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator
-}
-
-/** Coordinates re-inflation and post-processing of sensitive notification content. */
-interface SensitiveContentCoordinator : Coordinator
-
-@CoordinatorScope
-private class SensitiveContentCoordinatorImpl @Inject constructor(
-    private val dynamicPrivacyController: DynamicPrivacyController,
-    private val lockscreenUserManager: NotificationLockscreenUserManager,
-    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    private val statusBarStateController: StatusBarStateController,
-    private val keyguardStateController: KeyguardStateController
-) : Invalidator("SensitiveContentInvalidator"),
-        SensitiveContentCoordinator,
-        DynamicPrivacyController.Listener,
-        OnBeforeRenderListListener {
-
-    override fun attach(pipeline: NotifPipeline) {
-        dynamicPrivacyController.addListener(this)
-        pipeline.addOnBeforeRenderListListener(this)
-        pipeline.addPreRenderInvalidator(this)
-    }
-
-    override fun onDynamicPrivacyChanged(): Unit = invalidateList()
-
-    override fun onBeforeRenderList(entries: List<ListEntry>) {
-        if (keyguardStateController.isKeyguardGoingAway() ||
-                statusBarStateController.getState() == StatusBarState.KEYGUARD &&
-                keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(
-                        KeyguardUpdateMonitor.getCurrentUser())) {
-            // don't update yet if:
-            // - the keyguard is currently going away
-            // - LS is about to be dismissed by a biometric that bypasses LS (avoid notif flash)
-
-            // TODO(b/206118999): merge this class with KeyguardCoordinator which ensures the
-            // dependent state changes invalidate the pipeline
-            return
-        }
-
-        val currentUserId = lockscreenUserManager.currentUserId
-        val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId)
-        val deviceSensitive = devicePublic &&
-                !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)
-        val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked
-        for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) {
-            val notifUserId = entry.sbn.user.identifier
-            val userLockscreen = devicePublic ||
-                    lockscreenUserManager.isLockscreenPublicMode(notifUserId)
-            val userPublic = when {
-                // if we're not on the lockscreen, we're definitely private
-                !userLockscreen -> false
-                // we are on the lockscreen, so unless we're dynamically unlocked, we're
-                // definitely public
-                !dynamicallyUnlocked -> true
-                // we're dynamically unlocked, but check if the notification needs
-                // a separate challenge if it's from a work profile
-                else -> when (notifUserId) {
-                    currentUserId -> false
-                    UserHandle.USER_ALL -> false
-                    else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId)
-                }
-            }
-            val needsRedaction = lockscreenUserManager.needsRedaction(entry)
-            val isSensitive = userPublic && needsRedaction
-            entry.setSensitive(isSensitive, deviceSensitive)
-        }
-    }
-}
-
-private fun extractAllRepresentativeEntries(
-    entries: List<ListEntry>
-): Sequence<NotificationEntry> =
-    entries.asSequence().flatMap(::extractAllRepresentativeEntries)
-
-private fun extractAllRepresentativeEntries(listEntry: ListEntry): Sequence<NotificationEntry> =
-    sequence {
-        listEntry.representativeEntry?.let { yield(it) }
-        if (listEntry is GroupEntry) {
-            yieldAll(extractAllRepresentativeEntries(listEntry.children))
-        }
-    }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index 1c96e8c..2374e9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -50,6 +50,8 @@
         var hasClearableAlertingNotifs = false
         var hasNonClearableSilentNotifs = false
         var hasClearableSilentNotifs = false
+        val clearableAlertingSensitiveNotifUsers = mutableSetOf<Int>()
+        val clearableSilentSensitiveNotifUsers = mutableSetOf<Int>()
         entries.forEach {
             val section = checkNotNull(it.section) { "Null section for ${it.key}" }
             val entry = checkNotNull(it.representativeEntry) { "Null notif entry for ${it.key}" }
@@ -63,13 +65,22 @@
                 !isSilent && isClearable -> hasClearableAlertingNotifs = true
                 !isSilent && !isClearable -> hasNonClearableAlertingNotifs = true
             }
+            if (isClearable && entry.hasSensitiveContents()) {
+                if (isSilent) {
+                    clearableSilentSensitiveNotifUsers.add(entry.sbn.userId)
+                } else {
+                    clearableAlertingSensitiveNotifUsers.add(entry.sbn.userId)
+                }
+            }
         }
         return NotifStats(
             numActiveNotifs = entries.size,
             hasNonClearableAlertingNotifs = hasNonClearableAlertingNotifs,
             hasClearableAlertingNotifs = hasClearableAlertingNotifs,
             hasNonClearableSilentNotifs = hasNonClearableSilentNotifs,
-            hasClearableSilentNotifs = hasClearableSilentNotifs
+            hasClearableSilentNotifs = hasClearableSilentNotifs,
+            clearableAlertingSensitiveNotifUsers = clearableAlertingSensitiveNotifUsers,
+            clearableSilentSensitiveNotifUsers = clearableSilentSensitiveNotifUsers
         )
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/dagger/CoordinatorsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/dagger/CoordinatorsModule.kt
index 274affd..839cf0d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/dagger/CoordinatorsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/dagger/CoordinatorsModule.kt
@@ -17,10 +17,8 @@
 package com.android.systemui.statusbar.notification.collection.coordinator.dagger
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.notification.collection.coordinator.ActivityLaunchAnimCoordinatorModule
 import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinators
 import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinatorsImpl
-import com.android.systemui.statusbar.notification.collection.coordinator.SensitiveContentCoordinatorModule
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -49,8 +47,6 @@
 }
 
 @Module(includes = [
-    ActivityLaunchAnimCoordinatorModule::class,
-    SensitiveContentCoordinatorModule::class,
 ])
 private abstract class InternalCoordinatorsModule {
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 4c7b2bb..6e96aad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.collection.inflation;
 
+import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
+
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.Nullable;
@@ -249,10 +251,13 @@
         RowContentBindParams params = mRowContentBindStage.getStageParams(entry);
         params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
         params.setUseLowPriority(isLowPriority);
-
-        // TODO: Replace this API with RowContentBindParams directly. Also move to a separate
-        // redaction controller.
-        row.setNeedsRedaction(mNotificationLockscreenUserManager.needsRedaction(entry));
+        boolean needsRedaction =
+                mNotificationLockscreenUserManager.notifNeedsRedactionInPublic(entry);
+        if (needsRedaction) {
+            params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC);
+        } else {
+            params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC);
+        }
 
         params.rebindAllContentViews();
         mRowContentBindStage.requestRebind(entry, en -> {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java
index 3bd91b5..7dd3672 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/OnUserInteractionCallbackImpl.java
@@ -18,17 +18,17 @@
 
 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
 
-import android.annotation.Nullable;
 import android.os.SystemClock;
-import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationStats;
 
+import androidx.annotation.NonNull;
+
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
+import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator;
 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
-import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -43,54 +43,33 @@
     private final HeadsUpManager mHeadsUpManager;
     private final StatusBarStateController mStatusBarStateController;
     private final VisualStabilityCoordinator mVisualStabilityCoordinator;
-    private final GroupMembershipManager mGroupMembershipManager;
 
     public OnUserInteractionCallbackImpl(
             NotificationVisibilityProvider visibilityProvider,
             NotifCollection notifCollection,
             HeadsUpManager headsUpManager,
             StatusBarStateController statusBarStateController,
-            VisualStabilityCoordinator visualStabilityCoordinator,
-            GroupMembershipManager groupMembershipManager
+            VisualStabilityCoordinator visualStabilityCoordinator
     ) {
         mVisibilityProvider = visibilityProvider;
         mNotifCollection = notifCollection;
         mHeadsUpManager = headsUpManager;
         mStatusBarStateController = statusBarStateController;
         mVisualStabilityCoordinator = visualStabilityCoordinator;
-        mGroupMembershipManager = groupMembershipManager;
     }
 
-    /**
-     * Callback triggered when a user:
-     * 1. Manually dismisses a notification {@see ExpandableNotificationRow}.
-     * 2. Clicks on a notification with flag {@link android.app.Notification#FLAG_AUTO_CANCEL}.
-     * {@see StatusBarNotificationActivityStarter}
-     */
-    @Override
-    public void onDismiss(
-            NotificationEntry entry,
-            @NotificationListenerService.NotificationCancelReason int cancellationReason,
-            @Nullable NotificationEntry groupSummaryToDismiss
-    ) {
+    @NonNull
+    private DismissedByUserStats getDismissedByUserStats(NotificationEntry entry) {
         int dismissalSurface = NotificationStats.DISMISSAL_SHADE;
         if (mHeadsUpManager.isAlerting(entry.getKey())) {
             dismissalSurface = NotificationStats.DISMISSAL_PEEK;
         } else if (mStatusBarStateController.isDozing()) {
             dismissalSurface = NotificationStats.DISMISSAL_AOD;
         }
-
-        if (groupSummaryToDismiss != null) {
-            onDismiss(groupSummaryToDismiss, cancellationReason, null);
-        }
-
-        mNotifCollection.dismissNotification(
-                entry,
-                new DismissedByUserStats(
-                    dismissalSurface,
-                    DISMISS_SENTIMENT_NEUTRAL,
-                    mVisibilityProvider.obtain(entry, true))
-        );
+        return new DismissedByUserStats(
+                dismissalSurface,
+                DISMISS_SENTIMENT_NEUTRAL,
+                mVisibilityProvider.obtain(entry, true));
     }
 
     @Override
@@ -100,19 +79,11 @@
                 SystemClock.uptimeMillis());
     }
 
-    /**
-     * @param entry that is being dismissed
-     * @return the group summary to dismiss along with this entry if this is the last entry in
-     * the group. Else, returns null.
-     */
+    @NonNull
     @Override
-    @Nullable
-    public NotificationEntry getGroupSummaryToDismiss(NotificationEntry entry) {
-        String group = entry.getSbn().getGroup();
-        if (mNotifCollection.isOnlyChildInGroup(entry)) {
-            NotificationEntry summary = mNotifCollection.getGroupSummary(group);
-            if (summary != null && summary.isDismissable()) return summary;
-        }
-        return null;
+    public Runnable registerFutureDismissal(@NonNull NotificationEntry entry,
+            @CancellationReason int cancellationReason) {
+        return mNotifCollection.registerFutureDismissal(
+                entry, cancellationReason, this::getDismissedByUserStats);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/OnUserInteractionCallbackImplLegacy.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/OnUserInteractionCallbackImplLegacy.java
index 8daf8be..103b14b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/OnUserInteractionCallbackImplLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/OnUserInteractionCallbackImplLegacy.java
@@ -18,12 +18,15 @@
 
 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
 
-import android.annotation.Nullable;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationStats;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
+import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
@@ -68,8 +71,7 @@
      *                              along with this dismissal. If null, does not additionally
      *                              dismiss any notifications.
      */
-    @Override
-    public void onDismiss(
+    private void onDismiss(
             NotificationEntry entry,
             @NotificationListenerService.NotificationCancelReason int cancellationReason,
             @Nullable NotificationEntry groupSummaryToDismiss
@@ -106,14 +108,21 @@
      * @return the group summary to dismiss along with this entry if this is the last entry in
      * the group. Else, returns null.
      */
-    @Override
     @Nullable
-    public NotificationEntry getGroupSummaryToDismiss(NotificationEntry entry) {
+    private NotificationEntry getGroupSummaryToDismiss(NotificationEntry entry) {
         if (mGroupMembershipManager.isOnlyChildInGroup(entry)) {
             NotificationEntry groupSummary = mGroupMembershipManager.getLogicalGroupSummary(entry);
             return groupSummary.isDismissable() ? groupSummary : null;
         }
         return null;
     }
+
+    @Override
+    @NonNull
+    public Runnable registerFutureDismissal(@NonNull NotificationEntry entry,
+            @CancellationReason int cancellationReason) {
+        NotificationEntry groupSummaryToDismiss = getGroupSummaryToDismiss(entry);
+        return () -> onDismiss(entry, cancellationReason, groupSummaryToDismiss);
+    }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
index 7302de5..7e79367 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
@@ -28,7 +28,9 @@
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.statusbar.notification.collection.NotifCollection
 import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason
+import com.android.systemui.statusbar.notification.collection.NotifCollection.FutureDismissal
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
 fun cancellationReasonDebugString(@CancellationReason reason: Int) =
@@ -36,6 +38,7 @@
         -1 -> "REASON_NOT_CANCELED" // NotifCollection.REASON_NOT_CANCELED
         NotifCollection.REASON_UNKNOWN -> "REASON_UNKNOWN"
         NotificationListenerService.REASON_CLICK -> "REASON_CLICK"
+        NotificationListenerService.REASON_CANCEL -> "REASON_CANCEL"
         NotificationListenerService.REASON_CANCEL_ALL -> "REASON_CANCEL_ALL"
         NotificationListenerService.REASON_ERROR -> "REASON_ERROR"
         NotificationListenerService.REASON_PACKAGE_CHANGED -> "REASON_PACKAGE_CHANGED"
@@ -53,6 +56,9 @@
         NotificationListenerService.REASON_CHANNEL_BANNED -> "REASON_CHANNEL_BANNED"
         NotificationListenerService.REASON_SNOOZED -> "REASON_SNOOZED"
         NotificationListenerService.REASON_TIMEOUT -> "REASON_TIMEOUT"
+        NotificationListenerService.REASON_CHANNEL_REMOVED -> "REASON_CHANNEL_REMOVED"
+        NotificationListenerService.REASON_CLEAR_DATA -> "REASON_CLEAR_DATA"
+        NotificationListenerService.REASON_ASSISTANT_CANCEL -> "REASON_ASSISTANT_CANCEL"
         else -> "unknown"
     }
 
@@ -241,6 +247,81 @@
             "ERROR suppressed due to initialization forgiveness: $str1"
         })
     }
+
+    fun logFutureDismissalReused(dismissal: FutureDismissal) {
+        buffer.log(TAG, INFO, {
+            str1 = dismissal.label
+        }, {
+            "Reusing existing registration: $str1"
+        })
+    }
+
+    fun logFutureDismissalRegistered(dismissal: FutureDismissal) {
+        buffer.log(TAG, DEBUG, {
+            str1 = dismissal.label
+        }, {
+            "Registered: $str1"
+        })
+    }
+
+    fun logFutureDismissalDoubleCancelledByServer(dismissal: FutureDismissal) {
+        buffer.log(TAG, WARNING, {
+            str1 = dismissal.label
+        }, {
+            "System server double cancelled: $str1"
+        })
+    }
+
+    fun logFutureDismissalDoubleRun(dismissal: FutureDismissal) {
+        buffer.log(TAG, WARNING, {
+            str1 = dismissal.label
+        }, {
+            "Double run: $str1"
+        })
+    }
+
+    fun logFutureDismissalAlreadyCancelledByServer(dismissal: FutureDismissal) {
+        buffer.log(TAG, DEBUG, {
+            str1 = dismissal.label
+        }, {
+            "Ignoring: entry already cancelled by server: $str1"
+        })
+    }
+
+    fun logFutureDismissalGotSystemServerCancel(
+        dismissal: FutureDismissal,
+        @CancellationReason cancellationReason: Int
+    ) {
+        buffer.log(TAG, DEBUG, {
+            str1 = dismissal.label
+            int1 = cancellationReason
+        }, {
+            "SystemServer cancelled: $str1 reason=${cancellationReasonDebugString(int1)}"
+        })
+    }
+
+    fun logFutureDismissalDismissing(dismissal: FutureDismissal, type: String) {
+        buffer.log(TAG, DEBUG, {
+            str1 = dismissal.label
+            str2 = type
+        }, {
+            "Dismissing $str2 for: $str1"
+        })
+    }
+
+    fun logFutureDismissalMismatchedEntry(
+        dismissal: FutureDismissal,
+        type: String,
+        latestEntry: NotificationEntry?
+    ) {
+        buffer.log(TAG, WARNING, {
+            str1 = dismissal.label
+            str2 = type
+            str3 = latestEntry.logKey
+        }, {
+            "Mismatch: current $str2 is $str3 for: $str1"
+        })
+    }
 }
 
 private const val TAG = "NotifCollection"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt
index 6db544c..8be710c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt
@@ -57,7 +57,6 @@
 
         var currentSection: NotifSection? = null
         val prevSections = mutableSetOf<NotifSection?>()
-        var lastSection: NotifSection? = null
         val showHeaders = sectionHeaderVisibilityProvider.sectionHeadersVisible
         val sectionOrder = mutableListOf<NotifSection?>()
         val sectionHeaders = mutableMapOf<NotifSection?, NodeController?>()
@@ -65,15 +64,6 @@
 
         for (entry in notifList) {
             val section = entry.section!!
-
-            lastSection?.let {
-                if (it.bucket > section.bucket) {
-                    throw IllegalStateException("buildNodeSpec with non contiguous section " +
-                            "buckets ${it.sectioner.name} - ${it.bucket} & " +
-                            "${it.sectioner.name} - ${it.bucket}")
-                }
-            }
-            lastSection = section
             if (prevSections.contains(section)) {
                 throw java.lang.RuntimeException("Section ${section.label} has been duplicated")
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
index b6278d1..580d853 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
@@ -28,11 +28,21 @@
     val hasNonClearableAlertingNotifs: Boolean,
     val hasClearableAlertingNotifs: Boolean,
     val hasNonClearableSilentNotifs: Boolean,
-    val hasClearableSilentNotifs: Boolean
+    val hasClearableSilentNotifs: Boolean,
+    val clearableAlertingSensitiveNotifUsers: Set<Int>,
+    val clearableSilentSensitiveNotifUsers: Set<Int>
 ) {
     companion object {
         @JvmStatic
-        val empty = NotifStats(0, false, false, false, false)
+        val empty = NotifStats(
+            numActiveNotifs = 0,
+            hasNonClearableAlertingNotifs = false,
+            hasClearableAlertingNotifs = false,
+            hasNonClearableSilentNotifs = false,
+            hasClearableSilentNotifs = false,
+            clearableAlertingSensitiveNotifUsers = emptySet(),
+            clearableSilentSensitiveNotifUsers = emptySet(),
+        )
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt
index 2a9cfd0..f58918f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationSectionHeadersModule.kt
@@ -182,4 +182,4 @@
 
 @Scope
 @Retention(AnnotationRetention.BINARY)
-annotation class SectionHeaderScope
\ No newline at end of file
+annotation class SectionHeaderScope
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 d96590a..c9c7fe9 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
@@ -88,7 +88,6 @@
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.NotifActivityLaunchEventsModule;
 import com.android.systemui.statusbar.phone.NotifPanelEventsModule;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -111,7 +110,6 @@
 @Module(includes = {
         CoordinatorsModule.class,
         KeyguardNotificationVisibilityProviderModule.class,
-        NotifActivityLaunchEventsModule.class,
         NotifPanelEventsModule.class,
         NotifPipelineChoreographerModule.class,
         NotificationSectionHeadersModule.class,
@@ -350,8 +348,7 @@
                         notifCollection.get(),
                         headsUpManager,
                         statusBarStateController,
-                        visualStabilityCoordinator.get(),
-                        groupMembershipManagerLazy.get())
+                        visualStabilityCoordinator.get())
                 : new OnUserInteractionCallbackImplLegacy(
                         entryManager,
                         visibilityProvider.get(),
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 d896541..015e3d8 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
@@ -28,6 +28,7 @@
 import com.android.internal.statusbar.StatusBarIcon
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.InflationException
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -49,31 +50,29 @@
 class IconManager @Inject constructor(
     private val notifCollection: CommonNotifCollection,
     private val launcherApps: LauncherApps,
-    private val iconBuilder: IconBuilder
+    private val iconBuilder: IconBuilder,
+    private val notifLockscreenUserManager: NotificationLockscreenUserManager
 ) : ConversationIconManager {
     private var unimportantConversationKeys: Set<String> = emptySet()
 
     fun attach() {
         notifCollection.addCollectionListener(entryListener)
+        notifLockscreenUserManager.addOnNeedsRedactionInPublicChangedListener(sensitivityListener)
     }
 
     private val entryListener = object : NotifCollectionListener {
-        override fun onEntryInit(entry: NotificationEntry) {
-            entry.addOnSensitivityChangedListener(sensitivityListener)
-        }
-
-        override fun onEntryCleanUp(entry: NotificationEntry) {
-            entry.removeOnSensitivityChangedListener(sensitivityListener)
-        }
-
         override fun onRankingApplied() {
             // rankings affect whether a conversation is important, which can change the icons
             recalculateForImportantConversationChange()
         }
     }
 
-    private val sensitivityListener = NotificationEntry.OnSensitivityChangedListener {
-        entry -> updateIconsSafe(entry)
+    private val sensitivityListener = Runnable {
+        for (entry in notifCollection.allNotifs) {
+            if (entry.hasSensitiveContents()) {
+                updateIconsSafe(entry)
+            }
+        }
     }
 
     private fun recalculateForImportantConversationChange() {
@@ -182,12 +181,16 @@
         }
     }
 
+    private inline val NotificationEntry.needsRedactionInPublic: Boolean get() =
+        hasSensitiveContents() &&
+                notifLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic(sbn.userId)
+
     @Throws(InflationException::class)
     private fun getIconDescriptors(
         entry: NotificationEntry
     ): Pair<StatusBarIcon, StatusBarIcon> {
         val iconDescriptor = getIconDescriptor(entry, false /* redact */)
-        val sensitiveDescriptor = if (entry.isSensitive) {
+        val sensitiveDescriptor = if (entry.needsRedactionInPublic) {
             getIconDescriptor(entry, true /* redact */)
         } else {
             iconDescriptor
@@ -310,7 +313,7 @@
                 iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon
         val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon)
         return isImportantConversation(entry) && !isSmallIcon &&
-                (!usedInSensitiveContext || !entry.isSensitive)
+                (!usedInSensitiveContext || !entry.needsRedactionInPublic)
     }
 
     private fun isImportantConversation(entry: NotificationEntry): Boolean {
@@ -338,4 +341,4 @@
      * 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/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index dff8c47..d5088ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.notification.row;
 
 import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY;
-import static android.os.UserHandle.USER_SYSTEM;
 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
 
 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
@@ -34,8 +33,6 @@
 import android.app.NotificationChannel;
 import android.app.role.RoleManager;
 import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Canvas;
@@ -51,7 +48,6 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.Trace;
-import android.provider.Settings;
 import android.service.notification.StatusBarNotification;
 import android.util.ArraySet;
 import android.util.AttributeSet;
@@ -60,6 +56,7 @@
 import android.util.Log;
 import android.util.MathUtils;
 import android.util.Property;
+import android.util.Slog;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -93,8 +90,8 @@
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
-import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
 import com.android.systemui.statusbar.notification.FeedbackIcon;
+import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
 import com.android.systemui.statusbar.notification.NotificationFadeAware;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController;
 import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -112,7 +109,6 @@
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.statusbar.notification.stack.SwipeableView;
-import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
@@ -206,7 +202,6 @@
     /** Are we showing the "public" version */
     private boolean mShowingPublic;
     private boolean mSensitive;
-    private boolean mSensitiveHiddenInGeneral;
     private boolean mShowingPublicInitialized;
     private boolean mHideSensitiveForIntrinsicHeight;
     private float mHeaderVisibleAmount = DEFAULT_HEADER_VISIBLE_AMOUNT;
@@ -364,9 +359,6 @@
     @Nullable private OnExpansionChangedListener mExpansionChangedListener;
     @Nullable private Runnable mOnIntrinsicHeightReachedRunnable;
 
-    private SystemNotificationAsyncTask mSystemNotificationAsyncTask =
-            new SystemNotificationAsyncTask();
-
     private float mTopRoundnessDuringLaunchAnimation;
     private float mBottomRoundnessDuringLaunchAnimation;
 
@@ -518,45 +510,20 @@
     }
 
     /**
-     * Caches whether or not this row contains a system notification. Note, this is only cached
-     * once per notification as the packageInfo can't technically change for a notification row.
-     */
-    private void cacheIsSystemNotification() {
-        //TODO: This probably shouldn't be in ExpandableNotificationRow
-        if (mEntry != null && mEntry.mIsSystemNotification == null) {
-            if (mSystemNotificationAsyncTask.getStatus() == AsyncTask.Status.PENDING) {
-                // Run async task once, only if it hasn't already been executed. Note this is
-                // executed in serial - no need to parallelize this small task.
-                mSystemNotificationAsyncTask.execute();
-            }
-        }
-    }
-
-    /**
      * Returns whether this row is considered non-blockable (i.e. it's a non-blockable system notif
      * or is in an allowList).
      */
     public boolean getIsNonblockable() {
-        // If the SystemNotifAsyncTask hasn't finished running or retrieved a value, we'll try once
-        // again, but in-place on the main thread this time. This should rarely ever get called.
-        if (mEntry != null && mEntry.mIsSystemNotification == null) {
-            if (DEBUG) {
-                Log.d(TAG, "Retrieving isSystemNotification on main thread");
-            }
-            mSystemNotificationAsyncTask.cancel(true /* mayInterruptIfRunning */);
-            mEntry.mIsSystemNotification = isSystemNotification(mContext, mEntry.getSbn());
+        if (mEntry == null || mEntry.getChannel() == null) {
+            Log.w(TAG, "missing entry or channel");
+            return true;
+        }
+        if (mEntry.getChannel().isImportanceLockedByCriticalDeviceFunction()
+                && !mEntry.getChannel().isBlockable()) {
+            return true;
         }
 
-        boolean isNonblockable = mEntry.getChannel().isImportanceLockedByCriticalDeviceFunction();
-
-        if (!isNonblockable && mEntry != null && mEntry.mIsSystemNotification != null) {
-            if (mEntry.mIsSystemNotification) {
-                if (mEntry.getChannel() != null && !mEntry.getChannel().isBlockable()) {
-                    isNonblockable = true;
-                }
-            }
-        }
-        return isNonblockable;
+        return false;
     }
 
     private boolean isConversation() {
@@ -1456,8 +1423,7 @@
         dismiss(fromAccessibility);
         if (mEntry.isDismissable()) {
             if (mOnUserInteractionCallback != null) {
-                mOnUserInteractionCallback.onDismiss(mEntry, REASON_CANCEL,
-                        mOnUserInteractionCallback.getGroupSummaryToDismiss(mEntry));
+                mOnUserInteractionCallback.registerFutureDismissal(mEntry, REASON_CANCEL).run();
             }
         }
     }
@@ -1538,6 +1504,7 @@
         mUseIncreasedHeadsUpHeight = use;
     }
 
+    // TODO: remove this method and mNeedsRedaction entirely once the old pipeline is gone
     public void setNeedsRedaction(boolean needsRedaction) {
         // TODO: Move inflation logic out of this call and remove this method
         if (mNeedsRedaction != needsRedaction) {
@@ -1626,8 +1593,6 @@
         mBubblesManagerOptional = bubblesManagerOptional;
         mNotificationGutsManager = gutsManager;
         mMetricsLogger = metricsLogger;
-
-        cacheIsSystemNotification();
     }
 
     private void initDimens() {
@@ -2622,9 +2587,8 @@
         getShowingLayout().requestSelectLayout(needsAnimation || isUserLocked());
     }
 
-    public void setSensitive(boolean sensitive, boolean hideSensitive) {
+    public void setSensitive(boolean sensitive) {
         mSensitive = sensitive;
-        mSensitiveHiddenInGeneral = hideSensitive;
     }
 
     @Override
@@ -2714,7 +2678,15 @@
      *         see {@link NotificationEntry#isDismissable()}.
      */
     public boolean canViewBeDismissed() {
-        return mEntry.isDismissable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral);
+        // Entry not dismissable.
+        if (!mEntry.isDismissable()) {
+            return false;
+        }
+        // Entry shouldn't be showing the public layout, it can be dismissed.
+        if (!shouldShowPublic()) {
+            return true;
+        }
+        return false;
     }
 
     /**
@@ -2723,7 +2695,7 @@
      * clearability see {@link NotificationEntry#isClearable()}.
      */
     public boolean canViewBeCleared() {
-        return mEntry.isClearable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral);
+        return mEntry.isClearable() && !shouldShowPublic();
     }
 
     private boolean shouldShowPublic() {
@@ -3487,10 +3459,28 @@
             pw.print(", translation: " + getTranslation());
             pw.print(", removed: " + isRemoved());
             pw.print(", expandAnimationRunning: " + mExpandAnimationRunning);
-            NotificationContentView showingLayout = getShowingLayout();
-            pw.print(", privateShowing: " + (showingLayout == mPrivateLayout));
-            pw.println();
-            showingLayout.dump(pw, args);
+            pw.print(", sensitive: " + mSensitive);
+            pw.print(", hideSensitiveForIntrinsicHeight: " + mHideSensitiveForIntrinsicHeight);
+            pw.println(", privateShowing: " + !shouldShowPublic());
+            pw.print("privateLayout: ");
+            if (mPrivateLayout != null) {
+                pw.println();
+                DumpUtilsKt.withIncreasedIndent(pw, () -> {
+                    mPrivateLayout.dump(pw, args);
+                    mPrivateLayout.dumpSmartReplies(pw);
+                });
+            } else {
+                pw.println("null");
+            }
+            pw.print("publicLayout: ");
+            if (mPublicLayout != null) {
+                pw.println();
+                DumpUtilsKt.withIncreasedIndent(pw, () -> {
+                    mPublicLayout.dump(pw, args);
+                });
+            } else {
+                pw.println("null");
+            }
 
             if (getViewState() != null) {
                 getViewState().dump(pw, args);
@@ -3516,31 +3506,10 @@
                 }
                 pw.decreaseIndent();
                 pw.println("}");
-            } else if (mPrivateLayout != null) {
-                mPrivateLayout.dumpSmartReplies(pw);
             }
         });
     }
 
-    /**
-     * Background task for executing IPCs to check if the notification is a system notification. The
-     * output is used for both the blocking helper and the notification info.
-     */
-    private class SystemNotificationAsyncTask extends AsyncTask<Void, Void, Boolean> {
-
-        @Override
-        protected Boolean doInBackground(Void... voids) {
-            return isSystemNotification(mContext, mEntry.getSbn());
-        }
-
-        @Override
-        protected void onPostExecute(Boolean result) {
-            if (mEntry != null) {
-                mEntry.mIsSystemNotification = result;
-            }
-        }
-    }
-
     private void setTargetPoint(Point p) {
         mTargetPoint = p;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index 599039d..a60026c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -35,6 +35,7 @@
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.plugins.PluginManager;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.notification.FeedbackIcon;
@@ -69,6 +70,7 @@
 public class ExpandableNotificationRowController implements NotifViewController {
     private static final String TAG = "NotifRowController";
     private final ExpandableNotificationRow mView;
+    private final NotificationLockscreenUserManager mLockscreenUserManager;
     private final NotificationListContainer mListContainer;
     private final RemoteInputViewSubcomponent.Factory mRemoteInputViewSubcomponentFactory;
     private final ActivatableNotificationViewController mActivatableNotificationViewController;
@@ -86,7 +88,6 @@
     private final ExpandableNotificationRow.OnExpandClickListener mOnExpandClickListener;
     private final StatusBarStateController mStatusBarStateController;
     private final MetricsLogger mMetricsLogger;
-
     private final ExpandableNotificationRow.ExpansionLogger mExpansionLogger =
             this::logNotificationExpansion;
     private final ExpandableNotificationRow.CoordinateOnClickListener mOnFeedbackClickListener;
@@ -100,12 +101,12 @@
     private final Optional<BubblesManager> mBubblesManagerOptional;
     private final SmartReplyConstants mSmartReplyConstants;
     private final SmartReplyController mSmartReplyController;
-
     private final ExpandableNotificationRowDragController mDragController;
 
     @Inject
     public ExpandableNotificationRowController(
             ExpandableNotificationRow view,
+            NotificationLockscreenUserManager lockscreenUserManager,
             ActivatableNotificationViewController activatableNotificationViewController,
             RemoteInputViewSubcomponent.Factory rivSubcomponentFactory,
             MetricsLogger metricsLogger,
@@ -135,6 +136,7 @@
             Optional<BubblesManager> bubblesManagerOptional,
             ExpandableNotificationRowDragController dragController) {
         mView = view;
+        mLockscreenUserManager = lockscreenUserManager;
         mListContainer = listContainer;
         mRemoteInputViewSubcomponentFactory = rivSubcomponentFactory;
         mActivatableNotificationViewController = activatableNotificationViewController;
@@ -214,6 +216,10 @@
             mView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
         }
 
+        mLockscreenUserManager
+                .addOnNeedsRedactionInPublicChangedListener(mNeedsRedactionListener);
+        mNeedsRedactionListener.run();
+
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
@@ -232,6 +238,14 @@
         });
     }
 
+    private final Runnable mNeedsRedactionListener = new Runnable() {
+        @Override
+        public void run() {
+            mView.setSensitive(
+                    mLockscreenUserManager.notifNeedsRedactionInPublic(mView.getEntry()));
+        }
+    };
+
     private final StatusBarStateController.StateListener mStatusBarStateListener =
             new StatusBarStateController.StateListener() {
                 @Override
@@ -333,4 +347,5 @@
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
         mView.setFeedbackIcon(icon);
     }
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index ba26cfa..a7c6bfb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -61,6 +61,7 @@
 import com.android.systemui.statusbar.policy.SmartReplyView;
 import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent;
 import com.android.systemui.util.Compile;
+import com.android.systemui.util.DumpUtilsKt;
 import com.android.systemui.wmshell.BubblesManager;
 
 import java.io.PrintWriter;
@@ -1994,22 +1995,33 @@
         }
     }
 
-    public void dump(PrintWriter pw, String[] args) {
+    public void dump(PrintWriter pwOriginal, String[] args) {
+        IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
         pw.print("contentView visibility: " + getVisibility());
         pw.print(", alpha: " + getAlpha());
         pw.print(", clipBounds: " + getClipBounds());
         pw.print(", contentHeight: " + mContentHeight);
-        pw.print(", visibleType: " + mVisibleType);
-        View view = getViewForVisibleType(mVisibleType);
-        pw.print(", visibleView ");
-        if (view != null) {
-            pw.print(" visibility: " + view.getVisibility());
-            pw.print(", alpha: " + view.getAlpha());
-            pw.print(", clipBounds: " + view.getClipBounds());
-        } else {
-            pw.print("null");
-        }
-        pw.println();
+        pw.println(", currentVisibleType: " + mVisibleType);
+        DumpUtilsKt.withIncreasedIndent(pw, () -> {
+            int[] visTypes = {
+                    VISIBLE_TYPE_CONTRACTED,
+                    VISIBLE_TYPE_EXPANDED,
+                    VISIBLE_TYPE_HEADSUP,
+                    VISIBLE_TYPE_SINGLELINE
+            };
+            for (int visType : visTypes) {
+                pw.print("visType: " + visType + " :: ");
+                View view = getViewForVisibleType(visType);
+                if (view != null) {
+                    pw.print("visibility: " + view.getVisibility());
+                    pw.print(", alpha: " + view.getAlpha());
+                    pw.print(", clipBounds: " + view.getClipBounds());
+                } else {
+                    pw.print("null");
+                }
+                pw.println();
+            }
+        });
     }
 
     /** Add any existing SmartReplyView to the dump */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallback.java
index 94c5507..98d4353 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallback.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallback.java
@@ -16,8 +16,9 @@
 
 package com.android.systemui.statusbar.notification.row;
 
-import android.service.notification.NotificationListenerService;
+import androidx.annotation.NonNull;
 
+import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
 /**
@@ -26,29 +27,23 @@
 public interface OnUserInteractionCallback {
 
     /**
-     * Handle a user interaction that triggers a notification dismissal. Called when a user clicks
-     * on an auto-cancelled notification or manually swipes to dismiss the notification.
-     *
-     * @param entry notification being dismissed
-     * @param cancellationReason reason for the cancellation
-     * @param groupSummaryToDismiss group summary to dismiss with `entry`.
-     */
-    void onDismiss(
-            NotificationEntry entry,
-            @NotificationListenerService.NotificationCancelReason int cancellationReason,
-            NotificationEntry groupSummaryToDismiss);
-
-    /**
      * Triggered after a user has changed the importance of the notification via its
      * {@link NotificationGuts}.
      */
     void onImportanceChanged(NotificationEntry entry);
 
-
     /**
-     * @param entry being dismissed by the user
-     * @return group summary that should be dismissed along with `entry`. Can be null if no
-     * relevant group summary exists or the group summary should not be dismissed with `entry`.
+     * Called once it is known that a dismissal will take place for the given reason.
+     * This returns a Runnable which MUST be invoked when the dismissal is ready to be completed.
+     *
+     * Registering for future dismissal is typically done before notifying the NMS that a
+     * notification was clicked or dismissed, but the local dismissal may happen later.
+     *
+     * @param entry              the entry being cancelled
+     * @param cancellationReason the reason for the cancellation
+     * @return the runnable to call when the dismissal can happen
      */
-    NotificationEntry getGroupSummaryToDismiss(NotificationEntry entry);
+    @NonNull
+    Runnable registerFutureDismissal(@NonNull NotificationEntry entry,
+            @CancellationReason int cancellationReason);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
index f26ecc3..a52f638 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
@@ -102,8 +102,9 @@
      * @see InflationFlag
      */
     public void markContentViewsFreeable(@InflationFlag int contentViews) {
+        @InflationFlag int existingContentViews = contentViews &= mContentViews;
         mContentViews &= ~contentViews;
-        mDirtyContentViews &= ~contentViews;
+        mDirtyContentViews |= existingContentViews;
     }
 
     public @InflationFlag int getContentViews() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index aa3e027..7414bdc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.row;
 
+import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.util.AttributeSet;
@@ -25,6 +26,8 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.animation.Interpolators;
 
+import java.util.function.Consumer;
+
 /**
  * A common base class for all views in the notification stack scroller which don't have a
  * background.
@@ -48,7 +51,7 @@
     };
 
     private boolean mSecondaryAnimating = false;
-    private final Runnable mSecondaryVisibilityEndRunnable = () -> {
+    private final Consumer<Boolean> mSecondaryVisibilityEndRunnable = (cancelled) -> {
         mSecondaryAnimating = false;
         // If we were on screen, become GONE to avoid touches
         if (mSecondaryView == null) return;
@@ -96,18 +99,20 @@
      * @param animate True if we should fade to new visibility
      * @param runAfter Runnable to run after visibility updates
      */
-    public void setContentVisible(boolean visible, boolean animate, Runnable runAfter) {
+    public void setContentVisible(boolean visible, boolean animate, Consumer<Boolean> runAfter) {
         if (mContentVisible != visible) {
             mContentAnimating = animate;
             mContentVisible = visible;
-            Runnable endRunnable = runAfter == null ? mContentVisibilityEndRunnable : () -> {
+            Consumer<Boolean> endRunnable = (cancelled) -> {
                 mContentVisibilityEndRunnable.run();
-                runAfter.run();
+                if (runAfter != null) {
+                    runAfter.accept(cancelled);
+                }
             };
             setViewVisible(mContent, visible, animate, endRunnable);
         } else if (runAfter != null) {
             // Execute the runAfter runnable immediately if there's no animation to perform.
-            runAfter.run();
+            runAfter.accept(true);
         }
 
         if (!mContentAnimating) {
@@ -119,10 +124,6 @@
         return mContentVisible;
     }
 
-    public void setVisible(boolean nowVisible, boolean animate) {
-        setVisible(nowVisible, animate, null);
-    }
-
     /**
      * Make this view visible. If {@code false} is passed, the view will fade out it's content
      * and set the view Visibility to GONE. If only the content should be changed
@@ -131,7 +132,7 @@
      * @param nowVisible should the view be visible
      * @param animate should the change be animated.
      */
-    public void setVisible(boolean nowVisible, boolean animate, Runnable runAfter) {
+    public void setVisible(boolean nowVisible, boolean animate) {
         if (mIsVisible != nowVisible) {
             mIsVisible = nowVisible;
             if (animate) {
@@ -142,10 +143,10 @@
                 } else {
                     setWillBeGone(true);
                 }
-                setContentVisible(nowVisible, true /* animate */, runAfter);
+                setContentVisible(nowVisible, true /* animate */, null /* runAfter */);
             } else {
                 setVisibility(nowVisible ? VISIBLE : GONE);
-                setContentVisible(nowVisible, false /* animate */, runAfter);
+                setContentVisible(nowVisible, false /* animate */, null /* runAfter */);
                 setWillBeGone(false);
                 notifyHeightChanged(false /* needsAnimation */);
             }
@@ -166,7 +167,7 @@
         }
 
         if (!mSecondaryAnimating) {
-            mSecondaryVisibilityEndRunnable.run();
+            mSecondaryVisibilityEndRunnable.accept(true /* cancelled */);
         }
     }
 
@@ -195,7 +196,7 @@
      * @param endRunnable A runnable that is run when the animation is done.
      */
     private void setViewVisible(View view, boolean nowVisible,
-            boolean animate, Runnable endRunnable) {
+            boolean animate, Consumer<Boolean> endRunnable) {
         if (view == null) {
             return;
         }
@@ -211,7 +212,7 @@
         if (!animate) {
             view.setAlpha(endValue);
             if (endRunnable != null) {
-                endRunnable.run();
+                endRunnable.accept(true);
             }
             return;
         }
@@ -222,7 +223,19 @@
                 .alpha(endValue)
                 .setInterpolator(interpolator)
                 .setDuration(mDuration)
-                .withEndAction(endRunnable);
+                .setListener(new AnimatorListenerAdapter() {
+                    boolean mCancelled;
+
+                    @Override
+                    public void onAnimationCancel(Animator animation) {
+                        mCancelled = true;
+                    }
+
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        endRunnable.accept(mCancelled);
+                    }
+                });
     }
 
     @Override
@@ -231,7 +244,7 @@
             Runnable onFinishedRunnable,
             AnimatorListenerAdapter animationListener) {
         // TODO: Use duration
-        setContentVisible(false, true /* animate */, onFinishedRunnable);
+        setContentVisible(false, true /* animate */, (cancelled) -> onFinishedRunnable.run());
         return 0;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 3ea5e5b..4d325e1 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
@@ -1805,13 +1805,20 @@
     }
 
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
-    public void dismissViewAnimated(View child, Runnable endRunnable, int delay, long duration) {
+    public void dismissViewAnimated(
+            View child, Consumer<Boolean> endRunnable, int delay, long duration) {
         if (child instanceof SectionHeaderView) {
              ((StackScrollerDecorView) child).setContentVisible(
                      false /* visible */, true /* animate */, endRunnable);
              return;
         }
-        mSwipeHelper.dismissChild(child, 0, endRunnable, delay, true, duration,
+        mSwipeHelper.dismissChild(
+                child,
+                0 /* velocity */,
+                endRunnable,
+                delay,
+                true /* useAccelerateInterpolator */,
+                duration,
                 true /* isClearAll */);
     }
 
@@ -5196,11 +5203,15 @@
         if (mClearAllListener != null) {
             mClearAllListener.onClearAll(selection);
         }
-        final Runnable dismissInBackend = () -> {
-            onClearAllAnimationsEnd(rowsToDismissInBackend, selection);
+        final Consumer<Boolean> dismissInBackend = (cancelled) -> {
+            if (cancelled) {
+                post(() -> onClearAllAnimationsEnd(rowsToDismissInBackend, selection));
+            } else {
+                onClearAllAnimationsEnd(rowsToDismissInBackend, selection);
+            }
         };
         if (viewsToAnimateAway.isEmpty()) {
-            dismissInBackend.run();
+            dismissInBackend.accept(true);
             return;
         }
         // Disable normal animations
@@ -5215,7 +5226,7 @@
         final int numItems = viewsToAnimateAway.size();
         for (int i = numItems - 1; i >= 0; i--) {
             View view = viewsToAnimateAway.get(i);
-            Runnable endRunnable = null;
+            Consumer<Boolean> endRunnable = null;
             if (i == 0) {
                 endRunnable = dismissInBackend;
             }
@@ -5517,6 +5528,7 @@
      */
     public void setFractionToShade(float fraction) {
         mAmbientState.setFractionToShade(fraction);
+        updateContentHeight();  // Recompute stack height with different section gap.
         requestChildrenUpdate();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 2493ccb..6a8f479 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -216,6 +216,9 @@
                     mBarState = mStatusBarStateController.getState();
                     mStatusBarStateController.addCallback(
                             mStateListener, SysuiStatusBarStateController.RANK_STACK_SCROLLER);
+                    mLockscreenUserManager.addOnNeedsRedactionInPublicChangedListener(
+                            mOnNeedsRedactionInPublicChangedListener);
+                    updateClearButtonVisibility();
                 }
 
                 @Override
@@ -223,6 +226,8 @@
                     mConfigurationController.removeCallback(mConfigurationListener);
                     mZenModeController.removeCallback(mZenModeControllerCallback);
                     mStatusBarStateController.removeCallback(mStateListener);
+                    mLockscreenUserManager.removeOnNeedsRedactionInPublicChangedListener(
+                            mOnNeedsRedactionInPublicChangedListener);
                 }
             };
 
@@ -326,6 +331,7 @@
                             mLockscreenUserManager.isAnyProfilePublicMode());
                     mView.onStatePostChange(mStatusBarStateController.fromShadeLocked());
                     mNotificationEntryManager.updateNotifications("CentralSurfaces state changed");
+                    updateClearButtonVisibility();
                 }
             };
 
@@ -338,6 +344,17 @@
         }
     };
 
+    private final Runnable mOnNeedsRedactionInPublicChangedListener = new Runnable() {
+        @Override
+        public void run() {
+            // Whether or not the notification needs redaction when in public has changed, but if
+            // we're not actually in public, then we don't need to update anything.
+            if (mLockscreenUserManager.isAnyProfilePublicMode()) {
+                updateClearButtonVisibility();
+            }
+        }
+    };
+
     /**
      * Set the overexpansion of the panel to be applied to the view.
      */
@@ -1274,12 +1291,44 @@
         return hasNotifications(selection, true /* clearable */);
     }
 
+    private boolean hasRedactedClearableSilentNotifs() {
+        if (!mLockscreenUserManager.isAnyProfilePublicMode()) {
+            return false;
+        }
+        for (int userId : mNotifStats.getClearableSilentSensitiveNotifUsers()) {
+            if (mLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic(userId)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasClearableSilentNotifs() {
+        return mNotifStats.getHasClearableSilentNotifs() && !hasRedactedClearableSilentNotifs();
+    }
+
+    private boolean hasRedactedClearableAlertingNotifs() {
+        if (!mLockscreenUserManager.isAnyProfilePublicMode()) {
+            return false;
+        }
+        for (int userId : mNotifStats.getClearableAlertingSensitiveNotifUsers()) {
+            if (mLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic(userId)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean hasClearableAlertingNotifs() {
+        return mNotifStats.getHasClearableAlertingNotifs() && !hasRedactedClearableAlertingNotifs();
+    }
+
     public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) {
         boolean hasAlertingMatchingClearable = isClearable
-                ? mNotifStats.getHasClearableAlertingNotifs()
+                ? hasClearableAlertingNotifs()
                 : mNotifStats.getHasNonClearableAlertingNotifs();
         boolean hasSilentMatchingClearable = isClearable
-                ? mNotifStats.getHasClearableSilentNotifs()
+                ? hasClearableSilentNotifs()
                 : mNotifStats.getHasNonClearableSilentNotifs();
         switch (selection) {
             case ROWS_GENTLE:
@@ -1579,6 +1628,15 @@
         mNotificationActivityStarter = activityStarter;
     }
 
+    private void updateClearButtonVisibility() {
+        updateClearSilentButton();
+        updateFooter();
+    }
+
+    private void updateClearSilentButton() {
+        mSilentHeaderController.setClearSectionEnabled(hasClearableSilentNotifs());
+    }
+
     /**
      * Enum for UiEvent logged from this class
      */
@@ -1904,6 +1962,7 @@
         @Override
         public void setNotifStats(@NonNull NotifStats notifStats) {
             mNotifStats = notifStats;
+            updateClearSilentButton();
             updateFooter();
             updateShowEmptyShadeView();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 2b79986..799fee5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -49,9 +49,10 @@
 
     public static final float START_FRACTION = 0.5f;
 
-    private static final String LOG_TAG = "StackScrollAlgorithm";
-    private final ViewGroup mHostView;
+    private static final String TAG = "StackScrollAlgorithm";
+    private static final Boolean DEBUG = false;
 
+    private final ViewGroup mHostView;
     private int mPaddingBetweenElements;
     private int mGapHeight;
     private int mGapHeightOnLockscreen;
@@ -126,6 +127,37 @@
         return getExpansionFractionWithoutShelf(mTempAlgorithmState, ambientState);
     }
 
+    private void log(String s) {
+        if (DEBUG) {
+            android.util.Log.i(TAG, s);
+        }
+    }
+
+    public void logView(View view, String s) {
+        String viewString = "";
+        if (view instanceof ExpandableNotificationRow) {
+            ExpandableNotificationRow row = ((ExpandableNotificationRow) view);
+            if (row.getEntry() == null) {
+                viewString = "ExpandableNotificationRow has null NotificationEntry";
+            } else {
+                viewString = row.getEntry().getSbn().getId() + "";
+            }
+        } else if (view == null) {
+            viewString = "View is null";
+        } else if (view instanceof SectionHeaderView) {
+            viewString = "SectionHeaderView";
+        } else if (view instanceof FooterView) {
+            viewString = "FooterView";
+        } else if (view instanceof MediaContainerView) {
+            viewString = "MediaContainerView";
+        } else if (view instanceof EmptyShadeView) {
+            viewString = "EmptyShadeView";
+        } else {
+            viewString = view.toString();
+        }
+        log(viewString + " " + s);
+    }
+
     private void resetChildViewStates() {
         int numChildren = mHostView.getChildCount();
         for (int i = 0; i < numChildren; i++) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
index 9863a0e..f386797 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
@@ -29,6 +29,7 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.CrossFadeHelper;
 import com.android.systemui.statusbar.HeadsUpStatusBarView;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -40,8 +41,8 @@
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
 import com.android.systemui.util.ViewController;
 
-import java.util.Optional;
 import java.util.ArrayList;
+import java.util.Optional;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
@@ -61,17 +62,17 @@
     private final NotificationIconAreaController mNotificationIconAreaController;
     private final HeadsUpManagerPhone mHeadsUpManager;
     private final NotificationStackScrollLayoutController mStackScrollerController;
-
     private final DarkIconDispatcher mDarkIconDispatcher;
     private final NotificationPanelViewController mNotificationPanelViewController;
-    private final Consumer<ExpandableNotificationRow>
-            mSetTrackingHeadsUp = this::setTrackingHeadsUp;
+    private final Consumer<ExpandableNotificationRow> mSetTrackingHeadsUp =
+            this::setTrackingHeadsUp;
     private final BiConsumer<Float, Float> mSetExpandedHeight = this::setAppearFraction;
     private final KeyguardBypassController mBypassController;
     private final StatusBarStateController mStatusBarStateController;
     private final CommandQueue mCommandQueue;
     private final NotificationWakeUpCoordinator mWakeUpCoordinator;
-
+    private final NotificationLockscreenUserManager mNotifLockscreenUserManager;
+    private final Runnable mRedactionChanged = this::updateRedaction;
     private final View mClockView;
     private final Optional<View> mOperatorNameViewOptional;
 
@@ -90,6 +91,13 @@
             };
     private boolean mAnimationsEnabled = true;
     private final KeyguardStateController mKeyguardStateController;
+    private final StatusBarStateController.StateListener mStatusBarStateListener =
+            new StatusBarStateController.StateListener() {
+                @Override
+                public void onStatePostChange() {
+                    updateRedaction();
+                }
+            };
 
     @VisibleForTesting
     @Inject
@@ -98,6 +106,7 @@
             HeadsUpManagerPhone headsUpManager,
             StatusBarStateController stateController,
             KeyguardBypassController bypassController,
+            NotificationLockscreenUserManager notifLockscreenUserManager,
             NotificationWakeUpCoordinator wakeUpCoordinator,
             DarkIconDispatcher darkIconDispatcher,
             KeyguardStateController keyguardStateController,
@@ -125,6 +134,7 @@
         mClockView = clockView;
         mOperatorNameViewOptional = operatorNameViewOptional;
         mDarkIconDispatcher = darkIconDispatcher;
+        mNotifLockscreenUserManager = notifLockscreenUserManager;
 
         mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
             @Override
@@ -156,6 +166,8 @@
         mNotificationPanelViewController.setHeadsUpAppearanceController(this);
         mStackScrollerController.addOnExpandedHeightChangedListener(mSetExpandedHeight);
         mDarkIconDispatcher.addDarkReceiver(this);
+        mNotifLockscreenUserManager.addOnNeedsRedactionInPublicChangedListener(mRedactionChanged);
+        mStatusBarStateController.addCallback(mStatusBarStateListener);
     }
 
     @Override
@@ -167,6 +179,9 @@
         mNotificationPanelViewController.setHeadsUpAppearanceController(null);
         mStackScrollerController.removeOnExpandedHeightChangedListener(mSetExpandedHeight);
         mDarkIconDispatcher.removeDarkReceiver(this);
+        mNotifLockscreenUserManager
+                .removeOnNeedsRedactionInPublicChangedListener(mRedactionChanged);
+        mStatusBarStateController.removeCallback(mStatusBarStateListener);
     }
 
     private void updateIsolatedIconLocation(boolean requireStateUpdate) {
@@ -180,6 +195,19 @@
         updateHeader(entry);
     }
 
+    private void updateRedaction() {
+        NotificationEntry showingEntry = mView.getShowingEntry();
+        if (showingEntry == null) {
+            return;
+        }
+        int notifUserId = showingEntry.getSbn().getUserId();
+        boolean redactSensitiveContent =
+                mNotifLockscreenUserManager.isLockscreenPublicMode(notifUserId)
+                        && mNotifLockscreenUserManager
+                                .sensitiveNotifsNeedRedactionInPublic(notifUserId);
+        mView.setRedactSensitiveContent(redactSensitiveContent);
+    }
+
     private void updateTopEntry() {
         NotificationEntry newEntry = null;
         if (shouldBeVisible()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index 347e05c..fd307df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -1198,15 +1198,19 @@
             if (tileIcon != null) {
                 mWalletButton.setImageDrawable(tileIcon);
             }
-            updateWalletVisibility();
-            updateAffordanceColors();
+            post(() -> {
+                updateWalletVisibility();
+                updateAffordanceColors();
+            });
         }
 
         @Override
         public void onWalletCardRetrievalError(@NonNull GetWalletCardsError error) {
             mHasCard = false;
-            updateWalletVisibility();
-            updateAffordanceColors();
+            post(() -> {
+                updateWalletVisibility();
+                updateAffordanceColors();
+            });
         }
     }
 }
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 0b72138..8792118 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -188,6 +188,10 @@
             }
 
             if (mContainer.getVisibility() == View.VISIBLE || mShowingSoon) {
+                // Calls to reset must resume the ViewControllers when in fullscreen mode
+                if (needsFullscreenBouncer()) {
+                    mKeyguardViewController.onResume();
+                }
                 return;
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt
index 0623507..d44a569 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NPVCDownEventState.kt
@@ -14,9 +14,9 @@
 package com.android.systemui.statusbar.phone
 
 import android.view.MotionEvent
-import com.android.internal.util.RingBuffer
 import com.android.systemui.dump.DumpsysTableLogger
 import com.android.systemui.dump.Row
+import com.android.systemui.util.collection.RingBuffer
 import java.text.SimpleDateFormat
 import java.util.Locale
 
@@ -67,16 +67,9 @@
      * Do not use [append] to add new elements. Instead use [insert], as it will recycle if
      * necessary.
      */
-    class Buffer(
-        capacity: Int
-    ) : RingBuffer<NPVCDownEventState>(NPVCDownEventState::class.java, capacity) {
-        override fun append(t: NPVCDownEventState?) {
-            throw UnsupportedOperationException("Not supported, use insert instead")
-        }
+    class Buffer(capacity: Int) {
 
-        override fun createNewItem(): NPVCDownEventState {
-            return NPVCDownEventState()
-        }
+        private val buffer = RingBuffer(capacity) { NPVCDownEventState() }
 
         /**
          * Insert a new element in the buffer.
@@ -94,7 +87,7 @@
             touchSlopExceededBeforeDown: Boolean,
             lastEventSynthesized: Boolean
         ) {
-            nextSlot.apply {
+            buffer.advance().apply {
                 this.timeStamp = timeStamp
                 this.x = x
                 this.y = y
@@ -115,7 +108,7 @@
          * @see NPVCDownEventState.asStringList
          */
         fun toList(): List<Row> {
-            return toArray().map { it.asStringList }
+            return buffer.asSequence().map { it.asStringList }.toList()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEvents.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEvents.kt
deleted file mode 100644
index f46d073..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEvents.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone
-
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-
-/** Provides events about [android.app.Activity] launches from Notifications. */
-interface NotifActivityLaunchEvents {
-
-    /** Registers a [Listener] to be invoked when notification activity launch events occur. */
-    fun registerListener(listener: Listener)
-
-    /** Unregisters a [Listener] previously registered via [registerListener] */
-    fun unregisterListener(listener: Listener)
-
-    /** Listener for events about [android.app.Activity] launches from Notifications. */
-    interface Listener {
-
-        /** Invoked when an activity has started launching from a notification. */
-        fun onStartLaunchNotifActivity(entry: NotificationEntry)
-
-        /** Invoked when an activity has finished launching. */
-        fun onFinishLaunchNotifActivity(entry: NotificationEntry)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEventsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEventsModule.java
deleted file mode 100644
index 84ff538..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEventsModule.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone;
-
-import com.android.systemui.dagger.SysUISingleton;
-
-import dagger.Binds;
-import dagger.Module;
-
-/** Provides a {@link NotifActivityLaunchEvents} in {@link SysUISingleton} scope. */
-@Module
-public abstract class NotifActivityLaunchEventsModule {
-    @Binds
-    abstract NotifActivityLaunchEvents bindLaunchEvents(
-            StatusBarNotificationActivityStarter.LaunchEventsEmitter impl);
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index ec13b1f..366cd07 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -32,7 +32,6 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
-import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
 import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED;
 import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN;
@@ -323,7 +322,7 @@
     private final LargeScreenShadeHeaderController mLargeScreenShadeHeaderController;
     private final RecordingController mRecordingController;
     private final PanelEventsEmitter mPanelEventsEmitter;
-    private boolean mShouldUseSplitNotificationShade;
+    private boolean mSplitShadeEnabled;
     // The bottom padding reserved for elements of the keyguard measuring notifications
     private float mKeyguardNotificationBottomPadding;
     /**
@@ -814,7 +813,7 @@
         mKeyguardUserSwitcherComponentFactory = keyguardUserSwitcherComponentFactory;
         mFragmentService = fragmentService;
         mSettingsChangeObserver = new SettingsChangeObserver(handler);
-        mShouldUseSplitNotificationShade =
+        mSplitShadeEnabled =
                 LargeScreenUtils.shouldUseSplitNotificationShade(mResources);
         mView.setWillNotDraw(!DEBUG);
         mLargeScreenShadeHeaderController = largeScreenShadeHeaderController;
@@ -1029,7 +1028,7 @@
         });
 
         mView.setAccessibilityDelegate(mAccessibilityDelegate);
-        if (mShouldUseSplitNotificationShade) {
+        if (mSplitShadeEnabled) {
             updateResources();
         }
 
@@ -1126,20 +1125,18 @@
         mSplitShadeNotificationsScrimMarginBottom =
                 mResources.getDimensionPixelSize(
                         R.dimen.split_shade_notifications_scrim_margin_bottom);
-
         mShelfAndLockIconOverlap =
                 mResources.getDimensionPixelSize(R.dimen.shelf_and_lock_icon_overlap);
 
-        final boolean newShouldUseSplitNotificationShade =
+        final boolean newSplitShadeEnabled =
                 LargeScreenUtils.shouldUseSplitNotificationShade(mResources);
-        final boolean splitNotificationShadeChanged =
-                mShouldUseSplitNotificationShade != newShouldUseSplitNotificationShade;
+        final boolean splitShadeChanged = mSplitShadeEnabled != newSplitShadeEnabled;
+        mSplitShadeEnabled = newSplitShadeEnabled;
 
-        mShouldUseSplitNotificationShade = newShouldUseSplitNotificationShade;
         boolean useLargeScreenShadeHeader =
                 LargeScreenUtils.shouldUseLargeScreenShadeHeader(mView.getResources());
         if (mQs != null) {
-            mQs.setInSplitShade(mShouldUseSplitNotificationShade);
+            mQs.setInSplitShade(mSplitShadeEnabled);
         }
         mLargeScreenShadeHeaderHeight =
                 mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height);
@@ -1155,7 +1152,12 @@
 
         mKeyguardMediaController.refreshMediaPosition();
 
-        if (splitNotificationShadeChanged) {
+        if (splitShadeChanged) {
+            // when we switch from split shade to regular shade we want to enforce setting qs to
+            // the default state: expanded for split shade and collapsed otherwise
+            if (!isOnKeyguard() && mPanelExpanded) {
+                setQsExpanded(mSplitShadeEnabled);
+            }
             updateClockAppearance();
             updateQsState();
             mNotificationStackScrollLayoutController.updateFooter();
@@ -1384,7 +1386,7 @@
             updateClockAppearance();
         }
         if (!onKeyguard) {
-            if (mShouldUseSplitNotificationShade) {
+            if (mSplitShadeEnabled) {
                 // Quick settings are not on the top of the notifications
                 // when in split shade mode (they are on the left side),
                 // so we should not add a padding for them
@@ -1412,10 +1414,9 @@
                 .getVisibleNotificationCount() != 0
                 || mMediaDataManager.hasActiveMediaOrRecommendation();
         boolean splitShadeWithActiveMedia =
-                mShouldUseSplitNotificationShade
-                        && mMediaDataManager.hasActiveMediaOrRecommendation();
+                mSplitShadeEnabled && mMediaDataManager.hasActiveMediaOrRecommendation();
         boolean shouldAnimateClockChange = mScreenOffAnimationController.shouldAnimateClockChange();
-        if ((hasVisibleNotifications && !mShouldUseSplitNotificationShade)
+        if ((hasVisibleNotifications && !mSplitShadeEnabled)
                 || (splitShadeWithActiveMedia && !mDozing)) {
             mKeyguardStatusViewController.displayClock(SMALL, shouldAnimateClockChange);
         } else {
@@ -1452,7 +1453,7 @@
                 bypassEnabled, getUnlockedStackScrollerPadding(),
                 computeQsExpansionFraction(),
                 mDisplayTopInset,
-                mShouldUseSplitNotificationShade,
+                mSplitShadeEnabled,
                 udfpsAodTopLocation,
                 mKeyguardStatusViewController.getClockBottom(mStatusBarHeaderHeightKeyguard),
                 mKeyguardStatusViewController.isClockTopAligned());
@@ -1482,8 +1483,7 @@
         boolean hasVisibleNotifications = mNotificationStackScrollLayoutController
                 .getVisibleNotificationCount() != 0
                 || mMediaDataManager.hasActiveMediaOrRecommendation();
-        boolean shouldBeCentered = !mShouldUseSplitNotificationShade || !hasVisibleNotifications
-                || mDozing;
+        boolean shouldBeCentered = !mSplitShadeEnabled || !hasVisibleNotifications || mDozing;
         if (mStatusViewCentered != shouldBeCentered) {
             mStatusViewCentered = shouldBeCentered;
             ConstraintSet constraintSet = new ConstraintSet();
@@ -1492,7 +1492,7 @@
             constraintSet.connect(R.id.keyguard_status_view, END, statusConstraint, END);
             if (animate) {
                 ChangeBounds transition = new ChangeBounds();
-                if (mShouldUseSplitNotificationShade) {
+                if (mSplitShadeEnabled) {
                     // Excluding media from the transition on split-shade, as it doesn't transition
                     // horizontally properly.
                     transition.excludeTarget(R.id.status_view_media_container, true);
@@ -1649,7 +1649,7 @@
 
     private void setShowShelfOnly(boolean shelfOnly) {
         mNotificationStackScrollLayoutController.setShouldShowShelfOnly(
-                shelfOnly && !mShouldUseSplitNotificationShade);
+                shelfOnly && !mSplitShadeEnabled);
     }
 
     public void closeQs() {
@@ -1690,12 +1690,14 @@
             mQsExpandImmediate = true;
             setShowShelfOnly(true);
         }
-        if (mShouldUseSplitNotificationShade && isOnKeyguard()) {
+        if (mSplitShadeEnabled && isOnKeyguard()) {
             // It's a special case as this method is likely to not be initiated by finger movement
             // but rather called from adb shell or accessibility service.
-            // In the future method below could be used for non-split shade as well but currently
-            // motion in that case looks worse than using flingSettings.
-            // TODO: make below function transitioning smoothly also in portrait with empty target
+            // We're using LockscreenShadeTransitionController because on lockscreen that's the
+            // source of truth for all shade motion. Not using it would make part of state to be
+            // outdated and will cause bugs. Ideally we'd use this controller also for non-split
+            // case but currently motion in portrait looks worse than when using flingSettings.
+            // TODO: make below function transitioning smoothly also in portrait with null target
             mLockscreenShadeTransitionController.goToLockedShade(
                     /* expandedView= */null, /* needsQSAnimation= */false);
         } else if (isFullyCollapsed()) {
@@ -1918,7 +1920,7 @@
         int flingType;
         if (expandsQs && !isCancelMotionEvent) {
             flingType = FLING_EXPAND;
-        } else if (mShouldUseSplitNotificationShade) {
+        } else if (mSplitShadeEnabled) {
             flingType = FLING_HIDE;
         } else {
             flingType = FLING_COLLAPSE;
@@ -1984,11 +1986,11 @@
 
 
     private boolean handleQsTouch(MotionEvent event) {
-        if (mShouldUseSplitNotificationShade && touchXOutsideOfQs(event.getX())) {
+        if (mSplitShadeEnabled && touchXOutsideOfQs(event.getX())) {
             return false;
         }
         final int action = event.getActionMasked();
-        boolean collapsedQs = !mQsExpanded && !mShouldUseSplitNotificationShade;
+        boolean collapsedQs = !mQsExpanded && !mSplitShadeEnabled;
         boolean expandedShadeCollapsedQs = getExpandedFraction() == 1f && mBarState != KEYGUARD
                 && collapsedQs && isQsExpansionEnabled();
         if (action == MotionEvent.ACTION_DOWN && expandedShadeCollapsedQs) {
@@ -2006,7 +2008,7 @@
         }
         if (!mQsExpandImmediate && mQsTracking) {
             onQsTouch(event);
-            if (!mConflictingQsExpansionGesture && !mShouldUseSplitNotificationShade) {
+            if (!mConflictingQsExpansionGesture && !mSplitShadeEnabled) {
                 return true;
             }
         }
@@ -2297,7 +2299,7 @@
     }
 
     private void updateQsState() {
-        boolean qsFullScreen = mQsExpanded && !mShouldUseSplitNotificationShade;
+        boolean qsFullScreen = mQsExpanded && !mSplitShadeEnabled;
         mNotificationStackScrollLayoutController.setQsFullScreen(qsFullScreen);
         mNotificationStackScrollLayoutController.setScrollingEnabled(
                 mBarState != KEYGUARD && (!qsFullScreen || mQsExpansionFromOverscroll));
@@ -2313,8 +2315,9 @@
     void setQsExpansion(float height) {
         height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
         mQsFullyExpanded = height == mQsMaxExpansionHeight && mQsMaxExpansionHeight != 0;
+        boolean qsAnimatingAway = !mQsAnimatorExpand && mAnimatingQS;
         if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling
-                && !mDozing) {
+                && !mDozing && !qsAnimatingAway) {
             setQsExpanded(true);
         } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
             setQsExpanded(false);
@@ -2345,7 +2348,7 @@
     private void updateQsExpansion() {
         if (mQs == null) return;
         final float squishiness;
-        if ((mQsExpandImmediate || mQsExpanded) && !mShouldUseSplitNotificationShade) {
+        if ((mQsExpandImmediate || mQsExpanded) && !mSplitShadeEnabled) {
             squishiness = 1;
         } else if (mLockscreenShadeTransitionController.getQSDragProgress() > 0) {
             squishiness = mLockscreenShadeTransitionController.getQSDragProgress();
@@ -2354,7 +2357,7 @@
                     .getNotificationSquishinessFraction();
         }
         final float qsExpansionFraction = computeQsExpansionFraction();
-        final float adjustedExpansionFraction = mShouldUseSplitNotificationShade
+        final float adjustedExpansionFraction = mSplitShadeEnabled
                 ? 1f : computeQsExpansionFraction();
         mQs.setQsExpansion(adjustedExpansionFraction, getExpandedFraction(), getHeaderTranslation(),
                 squishiness);
@@ -2363,9 +2366,12 @@
         mScrimController.setQsPosition(qsExpansionFraction, qsPanelBottomY);
         setQSClippingBounds();
 
-        // Only need to notify the notification stack when we're not in split screen mode. If we
-        // do, then the notification panel starts scrolling along with the QS.
-        if (!mShouldUseSplitNotificationShade) {
+        if (mSplitShadeEnabled) {
+            // In split shade we want to pretend that QS are always collapsed so their behaviour and
+            // interactions don't influence notifications as they do in portrait. But we want to set
+            // 0 explicitly in case we're rotating from non-split shade with QS expansion of 1.
+            mNotificationStackScrollLayoutController.setQsExpansionFraction(0);
+        } else {
             mNotificationStackScrollLayoutController.setQsExpansionFraction(qsExpansionFraction);
         }
 
@@ -2400,7 +2406,7 @@
 
     private void updateQSExpansionEnabledAmbient() {
         final float scrollRangeToTop = mAmbientState.getTopPadding() - mQuickQsHeaderHeight;
-        mQsExpansionEnabledAmbient = mShouldUseSplitNotificationShade
+        mQsExpansionEnabledAmbient = mSplitShadeEnabled
                 || (mAmbientState.getScrollY() <= scrollRangeToTop);
         setQsExpansionEnabled();
     }
@@ -2424,7 +2430,7 @@
 
     private int calculateTopQsClippingBound(int qsPanelBottomY) {
         int top;
-        if (mShouldUseSplitNotificationShade) {
+        if (mSplitShadeEnabled) {
             top = Math.min(qsPanelBottomY, mLargeScreenShadeHeaderHeight);
         } else {
             if (mTransitioningToFullShadeProgress > 0.0f) {
@@ -2458,7 +2464,7 @@
     }
 
     private int calculateBottomQsClippingBound(int top) {
-        if (mShouldUseSplitNotificationShade) {
+        if (mSplitShadeEnabled) {
             return top + mNotificationStackScrollLayoutController.getHeight()
                     + mSplitShadeNotificationsScrimMarginBottom;
         } else {
@@ -2555,7 +2561,7 @@
                     // qsTranslation should only be positive during pulse expansion because it's
                     // already translating in from the top
                     qsTranslation = Math.max(0, (top - mQs.getHeader().getHeight()) / 2.0f);
-                } else if (!mShouldUseSplitNotificationShade) {
+                } else if (!mSplitShadeEnabled) {
                     qsTranslation = (top - mQs.getHeader().getHeight()) * QS_PARALLAX_AMOUNT;
                 }
             }
@@ -2569,11 +2575,11 @@
                     mQsClipTop,
                     mQsClipBottom,
                     radius, qsVisible
-                    && !mShouldUseSplitNotificationShade);
+                    && !mSplitShadeEnabled);
         }
         mKeyguardStatusViewController.setClipBounds(
                 clipStatusView ? mKeyguardStatusAreaClipBounds : null);
-        if (!qsVisible && mShouldUseSplitNotificationShade) {
+        if (!qsVisible && mSplitShadeEnabled) {
             // On the lockscreen when qs isn't visible, we don't want the bounds of the shade to
             // be visible, otherwise you can see the bounds once swiping up to see bouncer
             mScrimController.setNotificationsBounds(0, 0, 0, 0);
@@ -2581,12 +2587,11 @@
             // Increase the height of the notifications scrim when not in split shade
             // (e.g. portrait tablet) so the rounded corners are not visible at the bottom,
             // in this case they are rendered off-screen
-            final int notificationsScrimBottom =
-                    mShouldUseSplitNotificationShade ? bottom : bottom + radius;
+            final int notificationsScrimBottom = mSplitShadeEnabled ? bottom : bottom + radius;
             mScrimController.setNotificationsBounds(left, top, right, notificationsScrimBottom);
         }
 
-        if (mShouldUseSplitNotificationShade) {
+        if (mSplitShadeEnabled) {
             mKeyguardStatusBarViewController.setNoTopClipping();
         } else {
             mKeyguardStatusBarViewController.updateTopClipping(top);
@@ -2600,7 +2605,7 @@
         int nsslRight = right - mNotificationStackScrollLayoutController.getLeft();
         int nsslTop = top - mNotificationStackScrollLayoutController.getTop();
         int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
-        int bottomRadius = mShouldUseSplitNotificationShade ? radius : 0;
+        int bottomRadius = mSplitShadeEnabled ? radius : 0;
         mNotificationStackScrollLayoutController.setRoundedClippingBounds(
                 nsslLeft, nsslTop, nsslRight, nsslBottom, radius, bottomRadius);
     }
@@ -2642,7 +2647,7 @@
     }
 
     private float calculateNotificationsTopPadding() {
-        if (mShouldUseSplitNotificationShade && !mKeyguardShowing) {
+        if (mSplitShadeEnabled && !mKeyguardShowing) {
             return 0;
         }
         if (mKeyguardShowing && (mQsExpandImmediate
@@ -2915,7 +2920,7 @@
     private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
         if (!isQsExpansionEnabled() || mCollapsedOnDown
                 || (mKeyguardShowing && mKeyguardBypassController.getBypassEnabled())
-                || mShouldUseSplitNotificationShade) {
+                || mSplitShadeEnabled) {
             return false;
         }
         View header = mKeyguardShowing || mQs == null ? mKeyguardStatusBar : mQs.getHeader();
@@ -2946,7 +2951,7 @@
             return true;
         }
 
-        return !mShouldUseSplitNotificationShade && (isInSettings() || mIsPanelCollapseOnQQS);
+        return !mSplitShadeEnabled && (isInSettings() || mIsPanelCollapseOnQQS);
     }
 
     @Override
@@ -3130,7 +3135,7 @@
         float appearAmount = mNotificationStackScrollLayoutController
                 .calculateAppearFraction(mExpandedHeight);
         float startHeight = -mQsExpansionHeight;
-        if (!mShouldUseSplitNotificationShade && mBarState == StatusBarState.SHADE) {
+        if (!mSplitShadeEnabled && mBarState == StatusBarState.SHADE) {
             // Small parallax as we pull down and clip QS
             startHeight = -mQsExpansionHeight * QS_PARALLAX_AMOUNT;
         }
@@ -3373,9 +3378,9 @@
 
     @Override
     public void setIsLaunchAnimationRunning(boolean running) {
-        boolean wasRunning = isLaunchTransitionRunning();
+        boolean wasRunning = mIsLaunchAnimationRunning;
         super.setIsLaunchAnimationRunning(running);
-        if (wasRunning != isLaunchTransitionRunning()) {
+        if (wasRunning != mIsLaunchAnimationRunning) {
             mPanelEventsEmitter.notifyLaunchingActivityChanged(running);
         }
     }
@@ -3678,7 +3683,7 @@
             mQs.setCollapseExpandAction(mCollapseExpandAction);
             mQs.setHeaderClickable(isQsExpansionEnabled());
             mQs.setOverscrolling(mStackScrollerOverscrolling);
-            mQs.setInSplitShade(mShouldUseSplitNotificationShade);
+            mQs.setInSplitShade(mSplitShadeEnabled);
 
             // recompute internal state when qspanel height changes
             mQs.getView().addOnLayoutChangeListener(
@@ -3939,10 +3944,6 @@
         }
     }
 
-    public boolean hasActiveClearableNotifications() {
-        return mNotificationStackScrollLayoutController.hasActiveClearableNotifications(ROWS_ALL);
-    }
-
     public RemoteInputController.Delegate createRemoteInputDelegate() {
         return mNotificationStackScrollLayoutController.createDelegate();
     }
@@ -4353,7 +4354,7 @@
         @Override
         public void onOverscrollTopChanged(float amount, boolean isRubberbanded) {
             // When in split shade, overscroll shouldn't carry through to QS
-            if (mShouldUseSplitNotificationShade) {
+            if (mSplitShadeEnabled) {
                 return;
             }
             cancelQsAnimation();
@@ -4371,7 +4372,7 @@
         @Override
         public void flingTopOverscroll(float velocity, boolean open) {
             // in split shade mode we want to expand/collapse QS only when touch happens within QS
-            if (mShouldUseSplitNotificationShade && touchXOutsideOfQs(mInitialTouchX)) {
+            if (mSplitShadeEnabled && touchXOutsideOfQs(mInitialTouchX)) {
                 return;
             }
             mLastOverscroll = 0f;
@@ -4711,7 +4712,7 @@
                 // the top of QS
                 if (!mQsExpanded) {
                     // TODO(b/185683835) Nicer clipping when using new spacial model
-                    if (mShouldUseSplitNotificationShade) {
+                    if (mSplitShadeEnabled) {
                         mQs.animateHeaderSlidingOut();
                     }
                 }
@@ -4908,7 +4909,7 @@
 
     private void updateQSMinHeight() {
         float previousMin = mQsMinExpansionHeight;
-        if (mKeyguardShowing || mShouldUseSplitNotificationShade) {
+        if (mKeyguardShowing || mSplitShadeEnabled) {
             mQsMinExpansionHeight = 0;
         } else {
             mQsMinExpansionHeight = mQs.getQsMinExpansionHeight();
@@ -5042,7 +5043,7 @@
             // we need to ignore it on keyguard as this is a false alarm - transition from unlocked
             // to locked will trigger this event and we're not actually in the process of opening
             // the shade, lockscreen is just always expanded
-            if (mShouldUseSplitNotificationShade && !isOnKeyguard()) {
+            if (mSplitShadeEnabled && !isOnKeyguard()) {
                 mQsExpandImmediate = true;
             }
             mCentralSurfaces.makeExpandedVisible(false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index f9e17da..0e77866 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -402,6 +402,8 @@
         mScrimBehind.setFocusable(!state.isLowPowerState());
         mNotificationsScrim.setFocusable(!state.isLowPowerState());
 
+        mScrimInFront.setBlendWithMainColor(state.shouldBlendWithMainColor());
+
         // Cancel blanking transitions that were pending before we requested a new state
         if (mPendingFrameCallback != null) {
             mScrimBehind.removeCallbacks(mPendingFrameCallback);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
index 47b7058..4a5f712 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java
@@ -203,6 +203,11 @@
         public boolean isLowPowerState() {
             return true;
         }
+
+        @Override
+        public boolean shouldBlendWithMainColor() {
+            return false;
+        }
     },
 
     /**
@@ -325,6 +330,13 @@
     public void prepare(ScrimState previousState) {
     }
 
+    /**
+     * Whether a particular state should enable blending with extracted theme colors.
+     */
+    public boolean shouldBlendWithMainColor() {
+        return true;
+    }
+
     public float getFrontAlpha() {
         return mFrontAlpha;
     }
@@ -422,4 +434,4 @@
     public void setClipQsScrim(boolean clipsQsScrim) {
         mClipQsScrim = clipsQsScrim;
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
index 56b6dfc..c092216 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
@@ -20,7 +20,9 @@
 
     override fun onIntentStarted(willAnimate: Boolean) {
         delegate.onIntentStarted(willAnimate)
-        if (!willAnimate) {
+        if (willAnimate) {
+            centralSurfaces.notificationPanelViewController.setIsLaunchAnimationRunning(true)
+        } else {
             centralSurfaces.collapsePanelOnMainThread()
         }
     }
@@ -51,6 +53,7 @@
 
     override fun onLaunchAnimationCancelled() {
         delegate.onLaunchAnimationCancelled()
+        centralSurfaces.notificationPanelViewController.setIsLaunchAnimationRunning(false)
         centralSurfaces.onLaunchAnimationCancelled(isLaunchForActivity)
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 87ca942..cf776e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -41,7 +41,6 @@
 import android.util.EventLog;
 import android.view.View;
 
-import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.jank.InteractionJankMonitor;
@@ -52,7 +51,6 @@
 import com.android.systemui.EventLogTags;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.assist.AssistManager;
-import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
@@ -77,7 +75,6 @@
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.policy.HeadsUpUtil;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.ListenerSet;
 import com.android.systemui.wmshell.BubblesManager;
 
 import java.util.Optional;
@@ -131,7 +128,6 @@
     private final ActivityLaunchAnimator mActivityLaunchAnimator;
     private final NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
     private final OnUserInteractionCallback mOnUserInteractionCallback;
-    private final LaunchEventsEmitter mLaunchEventsEmitter;
 
     private boolean mIsCollapsingToShowActivityOverLockscreen;
 
@@ -170,8 +166,7 @@
             NotificationPresenter presenter,
             NotificationPanelViewController panel,
             ActivityLaunchAnimator activityLaunchAnimator,
-            NotificationLaunchAnimatorControllerProvider notificationAnimationProvider,
-            LaunchEventsEmitter launchEventsEmitter) {
+            NotificationLaunchAnimatorControllerProvider notificationAnimationProvider) {
         mContext = context;
         mCommandQueue = commandQueue;
         mMainThreadHandler = mainThreadHandler;
@@ -207,7 +202,6 @@
         mNotificationPanel = panel;
         mActivityLaunchAnimator = activityLaunchAnimator;
         mNotificationAnimationProvider = notificationAnimationProvider;
-        mLaunchEventsEmitter = launchEventsEmitter;
 
         if (!mNotifPipelineFlags.isNewPipelineEnabled()) {
             mEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
@@ -229,14 +223,13 @@
     /**
      * Called when a notification is clicked.
      *
-     * @param sbn notification that was clicked
+     * @param entry notification that was clicked
      * @param row row for that notification
      */
     @Override
-    public void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row) {
-        mLogger.logStartingActivityFromClick(sbn.getKey());
+    public void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row) {
+        mLogger.logStartingActivityFromClick(entry);
 
-        final NotificationEntry entry = row.getEntry();
         if (mRemoteInputManager.isRemoteInputActive(entry)
                 && !TextUtils.isEmpty(row.getActiveRemoteInputText())) {
             // We have an active remote input typed and the user clicked on the notification.
@@ -244,7 +237,7 @@
             mRemoteInputManager.closeRemoteInputs();
             return;
         }
-        Notification notification = sbn.getNotification();
+        Notification notification = entry.getSbn().getNotification();
         final PendingIntent intent = notification.contentIntent != null
                 ? notification.contentIntent
                 : notification.fullScreenIntent;
@@ -254,12 +247,10 @@
         // The only valid case is Bubble notifications. Guard against other cases
         // entering here.
         if (intent == null && !isBubble) {
-            mLogger.logNonClickableNotification(sbn.getKey());
+            mLogger.logNonClickableNotification(entry);
             return;
         }
 
-        mLaunchEventsEmitter.notifyStartLaunchNotifActivity(entry);
-
         boolean isActivityIntent = intent != null && intent.isActivity() && !isBubble;
         final boolean willLaunchResolverActivity = isActivityIntent
                 && mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(),
@@ -287,7 +278,7 @@
         } else {
             mActivityStarter.dismissKeyguardThenExecute(
                     postKeyguardAction,
-                    () -> mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry),
+                    null,
                     willLaunchResolverActivity);
         }
     }
@@ -299,7 +290,7 @@
             boolean isActivityIntent,
             boolean animate,
             boolean showOverLockscreen) {
-        mLogger.logHandleClickAfterKeyguardDismissed(entry.getKey());
+        mLogger.logHandleClickAfterKeyguardDismissed(entry);
 
         final Runnable runnable = () -> handleNotificationClickAfterPanelCollapsed(
                 entry, row, intent, isActivityIntent, animate);
@@ -326,7 +317,7 @@
             boolean isActivityIntent,
             boolean animate) {
         String notificationKey = entry.getKey();
-        mLogger.logHandleClickAfterPanelCollapsed(notificationKey);
+        mLogger.logHandleClickAfterPanelCollapsed(entry);
 
         try {
             // The intent we are sending is for the application, which
@@ -367,11 +358,9 @@
         }
         final boolean canBubble = entry.canBubble();
         if (canBubble) {
-            mLogger.logExpandingBubble(notificationKey);
+            mLogger.logExpandingBubble(entry);
             removeHunAfterClick(row);
             expandBubbleStackOnMainThread(entry);
-            mMainThreadHandler.post(
-                    () -> mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry));
         } else {
             startNotificationIntent(intent, fillInIntent, entry, row, animate, isActivityIntent);
         }
@@ -381,30 +370,13 @@
 
         final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true);
 
-        // retrieve the group summary to remove with this entry before we tell NMS the
-        // notification was clicked to avoid a race condition
-        final boolean shouldAutoCancel = shouldAutoCancel(entry.getSbn());
-        final NotificationEntry summaryToRemove = shouldAutoCancel
-                ? mOnUserInteractionCallback.getGroupSummaryToDismiss(entry) : null;
-
-        // inform NMS that the notification was clicked
-        mClickNotifier.onNotificationClick(notificationKey, nv);
-
-        if (!canBubble && (shouldAutoCancel
+        if (!canBubble && (shouldAutoCancel(entry.getSbn())
                 || mRemoteInputManager.isNotificationKeptForRemoteInputHistory(notificationKey))) {
+            final Runnable removeNotification =
+                    mOnUserInteractionCallback.registerFutureDismissal(entry, REASON_CLICK);
             // Immediately remove notification from visually showing.
             // We have to post the removal to the UI thread for synchronization.
             mMainThreadHandler.post(() -> {
-                final Runnable removeNotification = () -> {
-                    mOnUserInteractionCallback.onDismiss(entry, REASON_CLICK, summaryToRemove);
-                    if (!animate) {
-                        // If we're animating, this would be invoked after the activity launch
-                        // animation completes. Since we're not animating, the launch already
-                        // happened synchronously, so we notify the launch is complete here after
-                        // onDismiss.
-                        mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry);
-                    }
-                };
                 if (mPresenter.isCollapsing()) {
                     // To avoid lags we're only performing the remove after the shade is collapsed
                     mShadeController.addPostCollapseAction(removeNotification);
@@ -412,12 +384,11 @@
                     removeNotification.run();
                 }
             });
-        } else if (!canBubble && !animate) {
-            // Not animating, this is the end of the launch flow (see above comment for more info).
-            mMainThreadHandler.post(
-                    () -> mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry));
         }
 
+        // inform NMS that the notification was clicked
+        mClickNotifier.onNotificationClick(notificationKey, nv);
+
         mIsCollapsingToShowActivityOverLockscreen = false;
     }
 
@@ -434,24 +405,14 @@
         // will focus follow operation only after drag-and-drop that notification.
         final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true);
 
-        // retrieve the group summary to remove with this entry before we tell NMS the
-        // notification was clicked to avoid a race condition
-        final boolean shouldAutoCancel = shouldAutoCancel(entry.getSbn());
-        final NotificationEntry summaryToRemove = shouldAutoCancel
-                ? mOnUserInteractionCallback.getGroupSummaryToDismiss(entry) : null;
-
         String notificationKey = entry.getKey();
-        // inform NMS that the notification was clicked
-        mClickNotifier.onNotificationClick(notificationKey, nv);
-
-        if (shouldAutoCancel || mRemoteInputManager.isNotificationKeptForRemoteInputHistory(
-                notificationKey)) {
+        if (shouldAutoCancel(entry.getSbn())
+                || mRemoteInputManager.isNotificationKeptForRemoteInputHistory(notificationKey)) {
+            final Runnable removeNotification =
+                    mOnUserInteractionCallback.registerFutureDismissal(entry, REASON_CLICK);
             // Immediately remove notification from visually showing.
             // We have to post the removal to the UI thread for synchronization.
             mMainThreadHandler.post(() -> {
-                final Runnable removeNotification = () ->
-                        mOnUserInteractionCallback.onDismiss(
-                                entry, REASON_CLICK, summaryToRemove);
                 if (mPresenter.isCollapsing()) {
                     // To avoid lags we're only performing the remove
                     // after the shade is collapsed
@@ -462,6 +423,9 @@
             });
         }
 
+        // inform NMS that the notification was clicked
+        mClickNotifier.onNotificationClick(notificationKey, nv);
+
         mIsCollapsingToShowActivityOverLockscreen = false;
     }
 
@@ -489,15 +453,11 @@
             ExpandableNotificationRow row,
             boolean animate,
             boolean isActivityIntent) {
-        mLogger.logStartNotificationIntent(entry.getKey());
+        mLogger.logStartNotificationIntent(entry);
         try {
-            Runnable onFinishAnimationCallback = animate
-                    ? () -> mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry)
-                    : null;
             ActivityLaunchAnimator.Controller animationController =
                     new StatusBarLaunchAnimatorController(
-                            mNotificationAnimationProvider
-                                    .getAnimatorController(row, onFinishAnimationCallback),
+                            mNotificationAnimationProvider.getAnimatorController(row, null),
                             mCentralSurfaces,
                             isActivityIntent);
             mActivityLaunchAnimator.startPendingIntentWithAnimation(
@@ -515,7 +475,7 @@
                                 : getActivityOptions(mCentralSurfaces.getDisplayId(), adapter);
                         int result = intent.sendAndReturnResult(mContext, 0, fillInIntent, null,
                                 null, null, options);
-                        mLogger.logSendPendingIntent(entry.getKey(), intent, result);
+                        mLogger.logSendPendingIntent(entry, intent, result);
                         return result;
                     });
         } catch (PendingIntent.CanceledException e) {
@@ -622,9 +582,9 @@
     void handleFullScreenIntent(NotificationEntry entry) {
         if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) {
             if (shouldSuppressFullScreenIntent(entry)) {
-                mLogger.logFullScreenIntentSuppressedByDnD(entry.getKey());
+                mLogger.logFullScreenIntentSuppressedByDnD(entry);
             } else if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
-                mLogger.logFullScreenIntentNotImportantEnough(entry.getKey());
+                mLogger.logFullScreenIntentNotImportantEnough(entry);
             } else {
                 // Stop screensaver if the notification has a fullscreen intent.
                 // (like an incoming phone call)
@@ -639,7 +599,7 @@
                 // not immersive & a fullscreen alert should be shown
                 final PendingIntent fullscreenIntent =
                         entry.getSbn().getNotification().fullScreenIntent;
-                mLogger.logSendingFullScreenIntent(entry.getKey(), fullscreenIntent);
+                mLogger.logSendingFullScreenIntent(entry, fullscreenIntent);
                 try {
                     EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION,
                             entry.getKey());
@@ -685,35 +645,4 @@
 
         return entry.shouldSuppressFullScreenIntent();
     }
-
-    @SysUISingleton
-    static class LaunchEventsEmitter implements NotifActivityLaunchEvents {
-
-        private final ListenerSet<Listener> mListeners = new ListenerSet<>();
-
-        @Inject
-        LaunchEventsEmitter() {}
-
-        @Override
-        public void registerListener(@NonNull Listener listener) {
-            mListeners.addIfAbsent(listener);
-        }
-
-        @Override
-        public void unregisterListener(@NonNull Listener listener) {
-            mListeners.remove(listener);
-        }
-
-        private void notifyStartLaunchNotifActivity(NotificationEntry entry) {
-            for (Listener listener : mListeners) {
-                listener.onStartLaunchNotifActivity(entry);
-            }
-        }
-
-        private void notifyFinishLaunchNotifActivity(NotificationEntry entry) {
-            for (Listener listener : mListeners) {
-                listener.onFinishLaunchNotifActivity(entry);
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
index 2fbe520..b9a1413 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
@@ -23,46 +23,48 @@
 import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.LogLevel.WARNING
 import com.android.systemui.log.dagger.NotifInteractionLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
 class StatusBarNotificationActivityStarterLogger @Inject constructor(
     @NotifInteractionLog private val buffer: LogBuffer
 ) {
-    fun logStartingActivityFromClick(key: String) {
+    fun logStartingActivityFromClick(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "(1/5) onNotificationClicked: $str1"
         })
     }
 
-    fun logHandleClickAfterKeyguardDismissed(key: String) {
+    fun logHandleClickAfterKeyguardDismissed(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "(2/5) handleNotificationClickAfterKeyguardDismissed: $str1"
         })
     }
 
-    fun logHandleClickAfterPanelCollapsed(key: String) {
+    fun logHandleClickAfterPanelCollapsed(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "(3/5) handleNotificationClickAfterPanelCollapsed: $str1"
         })
     }
 
-    fun logStartNotificationIntent(key: String) {
+    fun logStartNotificationIntent(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "(4/5) startNotificationIntent: $str1"
         })
     }
 
-    fun logSendPendingIntent(key: String, pendingIntent: PendingIntent, result: Int) {
+    fun logSendPendingIntent(entry: NotificationEntry, pendingIntent: PendingIntent, result: Int) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
             str2 = pendingIntent.intent.toString()
             int1 = result
         }, {
@@ -70,9 +72,9 @@
         })
     }
 
-    fun logExpandingBubble(key: String) {
+    fun logExpandingBubble(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "Expanding bubble for $str1 (rather than firing intent)"
         })
@@ -86,33 +88,33 @@
         })
     }
 
-    fun logNonClickableNotification(key: String) {
+    fun logNonClickableNotification(entry: NotificationEntry) {
         buffer.log(TAG, ERROR, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "onNotificationClicked called for non-clickable notification! $str1"
         })
     }
 
-    fun logFullScreenIntentSuppressedByDnD(key: String) {
+    fun logFullScreenIntentSuppressedByDnD(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "No Fullscreen intent: suppressed by DND: $str1"
         })
     }
 
-    fun logFullScreenIntentNotImportantEnough(key: String) {
+    fun logFullScreenIntentNotImportantEnough(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "No Fullscreen intent: not important enough: $str1"
         })
     }
 
-    fun logSendingFullScreenIntent(key: String, pendingIntent: PendingIntent) {
+    fun logSendingFullScreenIntent(entry: NotificationEntry, pendingIntent: PendingIntent) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
             str2 = pendingIntent.intent.toString()
         }, {
             "Notification $str1 has fullScreenIntent; sending fullScreenIntent $str2"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index aa061d7..037063f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -411,8 +411,8 @@
         if (nowExpanded) {
             if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
                 mShadeTransitionController.goToLockedShade(clickedEntry.getRow());
-            } else if (clickedEntry.isSensitive()
-                    && mDynamicPrivacyController.isInLockedDownShade()) {
+            } else if (mDynamicPrivacyController.isInLockedDownShade()
+                    && mLockscreenUserManager.notifNeedsRedactionInPublic(clickedEntry)) {
                 mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
                 mActivityStarter.dismissKeyguardThenExecute(() -> false /* dismissAction */
                         , null /* cancelRunnable */, false /* afterKeyguardGone */);
@@ -480,7 +480,7 @@
                         .isLockscreenPublicMode(mLockscreenUserManager.getCurrentUserId());
                 boolean userPublic = devicePublic
                         || mLockscreenUserManager.isLockscreenPublicMode(sbn.getUserId());
-                boolean needsRedaction = mLockscreenUserManager.needsRedaction(entry);
+                boolean needsRedaction = mLockscreenUserManager.notifNeedsRedactionInPublic(entry);
                 if (userPublic && needsRedaction) {
                     // TODO(b/135046837): we can probably relax this with dynamic privacy
                     return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index d26b378..36a0456 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -31,6 +31,7 @@
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.util.TypedValue;
+import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewRootImpl;
 import android.view.Window;
@@ -376,11 +377,17 @@
     }
 
     private static int getHorizontalInsets(Dialog dialog) {
-        if (dialog.getWindow().getDecorView() == null) {
+        View decorView = dialog.getWindow().getDecorView();
+        if (decorView == null) {
             return 0;
         }
 
-        Drawable background = dialog.getWindow().getDecorView().getBackground();
+        // We first look for the background on the dialogContentWithBackground added by
+        // DialogLaunchAnimator. If it's not there, we use the background of the DecorView.
+        View viewWithBackground = decorView.findViewByPredicate(
+                view -> view.getTag(R.id.tag_dialog_background) != null);
+        Drawable background = viewWithBackground != null ? viewWithBackground.getBackground()
+                : decorView.getBackground();
         Insets insets = background != null ? background.getOpticalInsets() : Insets.NONE;
         return insets.left + insets.right;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
index 4c43734..576962d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
@@ -103,6 +103,10 @@
     private boolean mShowSeconds;
     private Handler mSecondsHandler;
 
+    // Fields to cache the width so the clock remains at an approximately constant width
+    private int mCharsAtCurrentWidth = -1;
+    private int mCachedWidth = -1;
+
     /**
      * Color to be set on this {@link TextView}, when wallpaperTextColor is <b>not</b> utilized.
      */
@@ -302,6 +306,32 @@
         setContentDescription(mContentDescriptionFormat.format(mCalendar.getTime()));
     }
 
+    /**
+     * In order to avoid the clock growing and shrinking due to proportional fonts, we want to
+     * cache the drawn width at a given number of characters (removing the cache when it changes),
+     * and only use the biggest value. This means that the clock width with grow to the maximum
+     * size over time, but reset whenever the number of characters changes (or the configuration
+     * changes)
+     */
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        int chars = getText().length();
+        if (chars != mCharsAtCurrentWidth) {
+            mCharsAtCurrentWidth = chars;
+            mCachedWidth = getMeasuredWidth();
+            return;
+        }
+
+        int measuredWidth = getMeasuredWidth();
+        if (mCachedWidth > measuredWidth) {
+            setMeasuredDimension(mCachedWidth, getMeasuredHeight());
+        } else {
+            mCachedWidth = measuredWidth;
+        }
+    }
+
     @Override
     public void onTuningChanged(String key, String newValue) {
         if (CLOCK_SECONDS.equals(key)) {
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 7920d388..094490b 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -48,6 +48,7 @@
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
@@ -78,6 +79,7 @@
 import org.json.JSONObject;
 
 import java.io.PrintWriter;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
@@ -474,10 +476,6 @@
             mThemeStyle = fetchThemeStyleFromSetting();
             mSecondaryOverlay = getOverlay(mMainWallpaperColor, ACCENT, mThemeStyle);
             mNeutralOverlay = getOverlay(mMainWallpaperColor, NEUTRAL, mThemeStyle);
-            if (colorSchemeIsApplied()) {
-                Log.d(TAG, "Skipping overlay creation. Theme was already: " + mColorScheme);
-                return;
-            }
             mNeedsOverlayCreation = true;
             if (DEBUG) {
                 Log.d(TAG, "fetched overlays. accent: " + mSecondaryOverlay
@@ -537,19 +535,28 @@
 
     /**
      * Checks if the color scheme in mColorScheme matches the current system palettes.
+     * @param managedProfiles List of managed profiles for this user.
      */
-    private boolean colorSchemeIsApplied() {
-        return mResources.getColor(
-                android.R.color.system_accent1_500, mContext.getTheme())
-                        == mColorScheme.getAccent1().get(6)
-                && mResources.getColor(android.R.color.system_accent2_500, mContext.getTheme())
-                        == mColorScheme.getAccent2().get(6)
-                && mResources.getColor(android.R.color.system_accent3_500, mContext.getTheme())
-                        == mColorScheme.getAccent3().get(6)
-                && mResources.getColor(android.R.color.system_neutral1_500, mContext.getTheme())
-                        == mColorScheme.getNeutral1().get(6)
-                && mResources.getColor(android.R.color.system_neutral2_500, mContext.getTheme())
-                        == mColorScheme.getNeutral2().get(6);
+    private boolean colorSchemeIsApplied(Set<UserHandle> managedProfiles) {
+        final ArraySet<UserHandle> allProfiles = new ArraySet<>(managedProfiles);
+        allProfiles.add(UserHandle.SYSTEM);
+        for (UserHandle userHandle : allProfiles) {
+            Resources res = userHandle.isSystem()
+                    ? mResources : mContext.createContextAsUser(userHandle, 0).getResources();
+            if (!(res.getColor(android.R.color.system_accent1_500, mContext.getTheme())
+                    == mColorScheme.getAccent1().get(6)
+                    && res.getColor(android.R.color.system_accent2_500, mContext.getTheme())
+                    == mColorScheme.getAccent2().get(6)
+                    && res.getColor(android.R.color.system_accent3_500, mContext.getTheme())
+                    == mColorScheme.getAccent3().get(6)
+                    && res.getColor(android.R.color.system_neutral1_500, mContext.getTheme())
+                    == mColorScheme.getNeutral1().get(6)
+                    && res.getColor(android.R.color.system_neutral2_500, mContext.getTheme())
+                    == mColorScheme.getNeutral2().get(6))) {
+                return false;
+            }
+        }
+        return true;
     }
 
     private void updateThemeOverlays() {
@@ -622,6 +629,12 @@
                 managedProfiles.add(userInfo.getUserHandle());
             }
         }
+
+        if (colorSchemeIsApplied(managedProfiles)) {
+            Log.d(TAG, "Skipping overlay creation. Theme was already: " + mColorScheme);
+            return;
+        }
+
         if (DEBUG) {
             Log.d(TAG, "Applying overlays: " + categoryToPackage.keySet().stream()
                     .map(key -> key + " -> " + categoryToPackage.get(key)).collect(
@@ -639,6 +652,11 @@
     }
 
     private Style fetchThemeStyleFromSetting() {
+        // Allow-list of Style objects that can be created from a setting string, i.e. can be
+        // used as a system-wide theme.
+        // - Content intentionally excluded, intended for media player, not system-wide
+        List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT,
+                Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT);
         Style style = mThemeStyle;
         final String overlayPackageJson = mSecureSettings.getStringForUser(
                 Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES,
@@ -648,6 +666,9 @@
                 JSONObject object = new JSONObject(overlayPackageJson);
                 style = Style.valueOf(
                         object.getString(ThemeOverlayApplier.OVERLAY_CATEGORY_THEME_STYLE));
+                if (!validStyles.contains(style)) {
+                    style = Style.TONAL_SPOT;
+                }
             } catch (JSONException | IllegalArgumentException e) {
                 Log.i(TAG, "Failed to parse THEME_CUSTOMIZATION_OVERLAY_PACKAGES.", e);
                 style = Style.TONAL_SPOT;
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvBottomSheetActivity.java b/packages/SystemUI/src/com/android/systemui/tv/TvBottomSheetActivity.java
index 2b7a332..90f2434 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvBottomSheetActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvBottomSheetActivity.java
@@ -18,15 +18,18 @@
 
 import android.app.Activity;
 import android.graphics.PixelFormat;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.Gravity;
+import android.view.View;
 import android.view.WindowManager;
 
 import com.android.systemui.R;
 
+import java.util.Collections;
 import java.util.function.Consumer;
 
 /**
@@ -75,6 +78,12 @@
         getWindow().setElevation(getWindow().getElevation() + 5);
         getWindow().setBackgroundBlurRadius(getResources().getDimensionPixelSize(
                 R.dimen.bottom_sheet_background_blur_radius));
+
+        final View rootView = findViewById(R.id.bottom_sheet);
+        rootView.addOnLayoutChangeListener((view, l, t, r, b, oldL, oldT, oldR, oldB) -> {
+            rootView.setUnrestrictedPreferKeepClearRects(
+                    Collections.singletonList(new Rect(0, 0, r - l, b - t)));
+        });
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt b/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
new file mode 100644
index 0000000..97dc842
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/collection/RingBuffer.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.collection
+
+import kotlin.math.max
+
+/**
+ * A simple ring buffer implementation
+ *
+ * Use [advance] to get the least recent item in the buffer (and then presumably fill it with
+ * appropriate data). This will cause it to become the most recent item.
+ *
+ * As the buffer is used, it will grow, allocating new instances of T using [factory] until it
+ * reaches [maxSize]. After this point, no new instances will be created. Instead, the "oldest"
+ * instances will be recycled from the back of the buffer and placed at the front.
+ *
+ * @param maxSize The maximum size the buffer can grow to before it begins functioning as a ring.
+ * @param factory A function that creates a fresh instance of T. Used by the buffer while it's
+ * growing to [maxSize].
+ */
+class RingBuffer<T>(
+    private val maxSize: Int,
+    private val factory: () -> T
+) : Iterable<T> {
+
+    private val buffer = MutableList<T?>(maxSize) { null }
+
+    /**
+     * An abstract representation that points to the "end" of the buffer. Increments every time
+     * [advance] is called and never wraps. Use [indexOf] to calculate the associated index into
+     * the backing array. Always points to the "next" available slot in the buffer. Before the
+     * buffer has completely filled, the value pointed to will be null. Afterward, it will be the
+     * value at the "beginning" of the buffer.
+     *
+     * This value is unlikely to overflow. Assuming [advance] is called at rate of 100 calls/ms,
+     * omega will overflow after a little under three million years of continuous operation.
+     */
+    private var omega: Long = 0
+
+    /**
+     * The number of items currently stored in the buffer. Calls to [advance] will cause this value
+     * to increase by one until it reaches [maxSize].
+     */
+    val size: Int
+        get() = if (omega < maxSize) omega.toInt() else maxSize
+
+    /**
+     * Advances the buffer's position by one and returns the value that is now present at the "end"
+     * of the buffer. If the buffer is not yet full, uses [factory] to create a new item.
+     * Otherwise, reuses the value that was previously at the "beginning" of the buffer.
+     *
+     * IMPORTANT: The value is returned as-is, without being reset. It will retain any data that
+     * was previously stored on it.
+     */
+    fun advance(): T {
+        val index = indexOf(omega)
+        omega += 1
+        val entry = buffer[index] ?: factory().also {
+            buffer[index] = it
+        }
+        return entry
+    }
+
+    /**
+     * Returns the value stored at [index], which can range from 0 (the "start", or oldest element
+     * of the buffer) to [size] - 1 (the "end", or newest element of the buffer).
+     */
+    operator fun get(index: Int): T {
+        if (index < 0 || index >= size) {
+            throw IndexOutOfBoundsException("Index $index is out of bounds")
+        }
+
+        // If omega is larger than the maxSize, then the buffer is full, and omega is equivalent
+        // to the "start" of the buffer. If omega is smaller than the maxSize, then the buffer is
+        // not yet full and our start should be 0. However, in modspace, maxSize and 0 are
+        // equivalent, so we can get away with using it as the start value instead.
+        val start = max(omega, maxSize.toLong())
+
+        return buffer[indexOf(start + index)]!!
+    }
+
+    inline fun forEach(action: (T) -> Unit) {
+        for (i in 0 until size) {
+            action(get(i))
+        }
+    }
+
+    override fun iterator(): Iterator<T> {
+        return object : Iterator<T> {
+            private var position: Int = 0
+
+            override fun next(): T {
+                if (position >= size) {
+                    throw NoSuchElementException()
+                }
+                return get(position).also { position += 1 }
+            }
+
+            override fun hasNext(): Boolean {
+                return position < size
+            }
+        }
+    }
+
+    private fun indexOf(position: Long): Int {
+        return (position % maxSize).toInt()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
index 190e655..f71d988 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogComponent.java
@@ -152,7 +152,7 @@
 
     private void applyConfiguration() {
         mController.setVolumePolicy(mVolumePolicy);
-        mController.showDndTile(true);
+        mController.showDndTile();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 59aa4d6..bf7c459 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -18,6 +18,8 @@
 
 import static android.media.AudioManager.RINGER_MODE_NORMAL;
 
+import android.app.ActivityManager;
+import android.app.KeyguardManager;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -91,10 +93,8 @@
 public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpable {
     private static final String TAG = Util.logTag(VolumeDialogControllerImpl.class);
 
-
     private static final int TOUCH_FEEDBACK_TIMEOUT_MS = 1000;
     private static final int DYNAMIC_STREAM_START_INDEX = 100;
-    private static final int VIBRATE_HINT_DURATION = 50;
     private static final AudioAttributes SONIFICIATION_VIBRATION_ATTRIBUTES =
             new AudioAttributes.Builder()
                     .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
@@ -122,14 +122,16 @@
     private final PackageManager mPackageManager;
     private final MediaRouter2Manager mRouter2Manager;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
-    private AudioManager mAudio;
-    private IAudioService mAudioService;
+    private final AudioManager mAudio;
+    private final IAudioService mAudioService;
     private final NotificationManager mNoMan;
     private final SettingObserver mObserver;
     private final Receiver mReceiver = new Receiver();
     private final RingerModeObservers mRingerModeObservers;
     private final MediaSessions mMediaSessions;
     private final CaptioningManager mCaptioningManager;
+    private final KeyguardManager mKeyguardManager;
+    private final ActivityManager mActivityManager;
     protected C mCallbacks = new C();
     private final State mState = new State();
     protected final MediaSessionsCallbacks mMediaSessionsCallbacksW;
@@ -141,9 +143,7 @@
     private long mLastToggledRingerOn;
     private boolean mDeviceInteractive = true;
 
-    private boolean mDestroyed;
     private VolumePolicy mVolumePolicy;
-    private boolean mShowDndTile = true;
     @GuardedBy("this")
     private UserActivityListener mUserActivityListener;
 
@@ -176,7 +176,10 @@
             AccessibilityManager accessibilityManager,
             PackageManager packageManager,
             WakefulnessLifecycle wakefulnessLifecycle,
-            CaptioningManager captioningManager) {
+            CaptioningManager captioningManager,
+            KeyguardManager keyguardManager,
+            ActivityManager activityManager
+    ) {
         mContext = context.getApplicationContext();
         mPackageManager = packageManager;
         mWakefulnessLifecycle = wakefulnessLifecycle;
@@ -202,6 +205,9 @@
         mHasVibrator = mVibrator.hasVibrator();
         mAudioService = iAudioService;
         mCaptioningManager = captioningManager;
+        mKeyguardManager = keyguardManager;
+        mActivityManager = activityManager;
+
 
         boolean accessibilityVolumeStreamActive = accessibilityManager
                 .isAccessibilityVolumeStreamActive();
@@ -247,7 +253,7 @@
     public void register() {
         setVolumeController();
         setVolumePolicy(mVolumePolicy);
-        showDndTile(mShowDndTile);
+        showDndTile();
         try {
             mMediaSessions.init();
         } catch (SecurityException e) {
@@ -272,10 +278,8 @@
 
     public void dump(PrintWriter pw, String[] args) {
         pw.println(VolumeDialogControllerImpl.class.getSimpleName() + " state:");
-        pw.print("  mDestroyed: "); pw.println(mDestroyed);
         pw.print("  mVolumePolicy: "); pw.println(mVolumePolicy);
         pw.print("  mState: "); pw.println(mState.toString(4));
-        pw.print("  mShowDndTile: "); pw.println(mShowDndTile);
         pw.print("  mHasVibrator: "); pw.println(mHasVibrator);
         synchronized (mMediaSessionsCallbacksW.mRemoteStreams) {
             pw.print("  mRemoteStreams: ");
@@ -293,7 +297,6 @@
     }
 
     public void setUserActivityListener(UserActivityListener listener) {
-        if (mDestroyed) return;
         synchronized (this) {
             mUserActivityListener = listener;
         }
@@ -304,7 +307,6 @@
     }
 
     public void getState() {
-        if (mDestroyed) return;
         mWorker.sendEmptyMessage(W.GET_STATE);
     }
 
@@ -317,48 +319,39 @@
     }
 
     public void getCaptionsComponentState(boolean fromTooltip) {
-        if (mDestroyed) return;
         mWorker.obtainMessage(W.GET_CAPTIONS_COMPONENT_STATE, fromTooltip).sendToTarget();
     }
 
     public void notifyVisible(boolean visible) {
-        if (mDestroyed) return;
         mWorker.obtainMessage(W.NOTIFY_VISIBLE, visible ? 1 : 0, 0).sendToTarget();
     }
 
     public void userActivity() {
-        if (mDestroyed) return;
         mWorker.removeMessages(W.USER_ACTIVITY);
         mWorker.sendEmptyMessage(W.USER_ACTIVITY);
     }
 
     public void setRingerMode(int value, boolean external) {
-        if (mDestroyed) return;
         mWorker.obtainMessage(W.SET_RINGER_MODE, value, external ? 1 : 0).sendToTarget();
     }
 
     public void setZenMode(int value) {
-        if (mDestroyed) return;
         mWorker.obtainMessage(W.SET_ZEN_MODE, value, 0).sendToTarget();
     }
 
     public void setExitCondition(Condition condition) {
-        if (mDestroyed) return;
         mWorker.obtainMessage(W.SET_EXIT_CONDITION, condition).sendToTarget();
     }
 
     public void setStreamMute(int stream, boolean mute) {
-        if (mDestroyed) return;
         mWorker.obtainMessage(W.SET_STREAM_MUTE, stream, mute ? 1 : 0).sendToTarget();
     }
 
     public void setStreamVolume(int stream, int level) {
-        if (mDestroyed) return;
         mWorker.obtainMessage(W.SET_STREAM_VOLUME, stream, level).sendToTarget();
     }
 
     public void setActiveStream(int stream) {
-        if (mDestroyed) return;
         mWorker.obtainMessage(W.SET_ACTIVE_STREAM, stream, 0).sendToTarget();
     }
 
@@ -392,7 +385,6 @@
     }
 
     private void onNotifyVisibleW(boolean visible) {
-        if (mDestroyed) return;
         mAudio.notifyVolumeControllerVisible(mVolumeController, visible);
         if (!visible) {
             if (updateActiveStreamW(-1)) {
@@ -465,7 +457,7 @@
             mCallbacks.onStateChanged(mState);
         }
         if (showUI) {
-            mCallbacks.onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED);
+            onShowRequestedW(Events.SHOW_REASON_VOLUME_CHANGED);
         }
         if (showVibrateHint) {
             mCallbacks.onShowVibrateHint();
@@ -645,6 +637,11 @@
         return true;
     }
 
+    private void onShowRequestedW(int reason) {
+        mCallbacks.onShowRequested(reason, mKeyguardManager.isKeyguardLocked(),
+                mActivityManager.getLockTaskModeState());
+    }
+
     private void onSetRingerModeW(int mode, boolean external) {
         if (external) {
             mAudio.setRingerMode(mode);
@@ -687,9 +684,9 @@
         mCallbacks.onDismissRequested(reason);
     }
 
-    public void showDndTile(boolean visible) {
+    public void showDndTile() {
         if (D.BUG) Log.d(TAG, "showDndTile");
-        DndTile.setVisible(mContext, visible);
+        DndTile.setVisible(mContext, true);
     }
 
     private final class VC extends IVolumeController.Stub {
@@ -699,7 +696,6 @@
         public void displaySafeVolumeWarning(int flags) throws RemoteException {
             if (D.BUG) Log.d(TAG, "displaySafeVolumeWarning "
                     + Util.audioManagerFlagsToString(flags));
-            if (mDestroyed) return;
             mWorker.obtainMessage(W.SHOW_SAFETY_WARNING, flags, 0).sendToTarget();
         }
 
@@ -707,7 +703,6 @@
         public void volumeChanged(int streamType, int flags) throws RemoteException {
             if (D.BUG) Log.d(TAG, "volumeChanged " + AudioSystem.streamToString(streamType)
                     + " " + Util.audioManagerFlagsToString(flags));
-            if (mDestroyed) return;
             mWorker.obtainMessage(W.VOLUME_CHANGED, streamType, flags).sendToTarget();
         }
 
@@ -719,14 +714,12 @@
         @Override
         public void setLayoutDirection(int layoutDirection) throws RemoteException {
             if (D.BUG) Log.d(TAG, "setLayoutDirection");
-            if (mDestroyed) return;
             mWorker.obtainMessage(W.LAYOUT_DIRECTION_CHANGED, layoutDirection, 0).sendToTarget();
         }
 
         @Override
         public void dismiss() throws RemoteException {
             if (D.BUG) Log.d(TAG, "dismiss requested");
-            if (mDestroyed) return;
             mWorker.obtainMessage(W.DISMISS_REQUESTED, Events.DISMISS_REASON_VOLUME_CONTROLLER, 0)
                     .sendToTarget();
             mWorker.sendEmptyMessage(W.DISMISS_REQUESTED);
@@ -735,7 +728,6 @@
         @Override
         public void setA11yMode(int mode) {
             if (D.BUG) Log.d(TAG, "setA11yMode to " + mode);
-            if (mDestroyed) return;
             switch (mode) {
                 case VolumePolicy.A11Y_MODE_MEDIA_A11Y_VOLUME:
                     // "legacy" mode
@@ -798,7 +790,7 @@
         }
     }
 
-    class C implements Callbacks {
+    static class C implements Callbacks {
         private final Map<Callbacks, Handler> mCallbackMap = new ConcurrentHashMap<>();
 
         public void add(Callbacks callback, Handler handler) {
@@ -811,12 +803,15 @@
         }
 
         @Override
-        public void onShowRequested(final int reason) {
+        public void onShowRequested(
+                final int reason,
+                final boolean keyguardLocked,
+                final int lockTaskModeState) {
             for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
                 entry.getValue().post(new Runnable() {
                     @Override
                     public void run() {
-                        entry.getKey().onShowRequested(reason);
+                        entry.getKey().onShowRequested(reason, keyguardLocked, lockTaskModeState);
                     }
                 });
             }
@@ -923,7 +918,7 @@
 
         @Override
         public void onAccessibilityModeChanged(Boolean showA11yStream) {
-            boolean show = showA11yStream == null ? false : showA11yStream;
+            boolean show = showA11yStream != null && showA11yStream;
             for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
                 entry.getValue().post(new Runnable() {
                     @Override
@@ -937,7 +932,7 @@
         @Override
         public void onCaptionComponentStateChanged(
                 Boolean isComponentEnabled, Boolean fromTooltip) {
-            boolean componentEnabled = isComponentEnabled == null ? false : isComponentEnabled;
+            boolean componentEnabled = isComponentEnabled != null && isComponentEnabled;
             for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
                 entry.getValue().post(
                         () -> entry.getKey().onCaptionComponentStateChanged(
@@ -1183,7 +1178,7 @@
                     mCallbacks.onStateChanged(mState);
                 }
                 if (showUI) {
-                    mCallbacks.onShowRequested(Events.SHOW_REASON_REMOTE_VOLUME_CHANGED);
+                    onShowRequestedW(Events.SHOW_REASON_REMOTE_VOLUME_CHANGED);
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index bfdcbd6..e3cb2fa 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -32,6 +32,8 @@
 import static android.view.View.VISIBLE;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL;
+import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder;
 import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
 
 import android.animation.Animator;
@@ -68,6 +70,7 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.SystemClock;
+import android.os.Trace;
 import android.os.VibrationEffect;
 import android.provider.Settings;
 import android.provider.Settings.Global;
@@ -101,9 +104,11 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.graphics.drawable.BackgroundBlurDrawable;
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.view.RotationPolicy;
 import com.android.settingslib.Utils;
 import com.android.systemui.Prefs;
@@ -149,6 +154,13 @@
     private static final int DRAWER_ANIMATION_DURATION_SHORT = 175;
     private static final int DRAWER_ANIMATION_DURATION = 250;
 
+    /** Shows volume dialog show animation. */
+    private static final String TYPE_SHOW = "show";
+    /** Dismiss volume dialog animation.  */
+    private static final String TYPE_DISMISS = "dismiss";
+    /** Volume dialog slider animation. */
+    private static final String TYPE_UPDATE = "update";
+
     private final int mDialogShowAnimationDurationMs;
     private final int mDialogHideAnimationDurationMs;
     private int mDialogWidth;
@@ -248,7 +260,7 @@
     private State mState;
     private SafetyWarningDialog mSafetyWarning;
     private boolean mHovering = false;
-    private boolean mShowActiveStreamOnly;
+    private final boolean mShowActiveStreamOnly;
     private boolean mConfigChanged = false;
     private boolean mIsAnimatingDismiss = false;
     private boolean mHasSeenODICaptionsTooltip;
@@ -258,6 +270,7 @@
     private final boolean mUseBackgroundBlur;
     private Consumer<Boolean> mCrossWindowBlurEnabledListener;
     private BackgroundBlurDrawable mDialogRowsViewBackground;
+    private final InteractionJankMonitor mInteractionJankMonitor;
 
     public VolumeDialogImpl(
             Context context,
@@ -266,7 +279,8 @@
             DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
             MediaOutputDialogFactory mediaOutputDialogFactory,
-            ActivityStarter activityStarter) {
+            ActivityStarter activityStarter,
+            InteractionJankMonitor interactionJankMonitor) {
         mContext =
                 new ContextThemeWrapper(context, R.style.volume_dialog_theme);
         mController = volumeDialogController;
@@ -290,6 +304,7 @@
             mContext.getResources().getInteger(R.integer.config_dialogHideAnimationDurationMs);
         mUseBackgroundBlur =
             mContext.getResources().getBoolean(R.bool.config_volumeDialogUseBackgroundBlur);
+        mInteractionJankMonitor = interactionJankMonitor;
 
         if (mUseBackgroundBlur) {
             final int dialogRowsViewColorAboveBlur = mContext.getColor(
@@ -312,7 +327,7 @@
     }
 
     public void init(int windowType, Callback callback) {
-        initDialog();
+        initDialog(mActivityManager.getLockTaskModeState());
 
         mAccessibility.init();
 
@@ -379,7 +394,7 @@
                 Region.Op.UNION);
     }
 
-    private void initDialog() {
+    private void initDialog(int lockTaskModeState) {
         mDialog = new CustomDialog(mContext);
 
         initDimens();
@@ -422,6 +437,7 @@
                     .alpha(1)
                     .translationX(0)
                     .setDuration(mDialogShowAnimationDurationMs)
+                    .setListener(getJankListener(getDialogView(), TYPE_SHOW, DIALOG_TIMEOUT_MILLIS))
                     .setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator())
                     .withEndAction(() -> {
                         if (!Prefs.getBoolean(mContext, Prefs.Key.TOUCHED_RINGER_TOGGLE, false)) {
@@ -573,7 +589,7 @@
 
         updateRowsH(getActiveRow());
         initRingerH();
-        initSettingsH();
+        initSettingsH(lockTaskModeState);
         initODICaptionsH();
     }
 
@@ -692,7 +708,7 @@
         final int m = seekBar.getMax();
         final int n = m / 100 - 1;
         final int level = progress == 0 ? 0
-                : progress == m ? (m / 100) : (1 + (int)((progress / (float) m) * n));
+                : progress == m ? (m / 100) : (1 + (int) ((progress / (float) m) * n));
         return level;
     }
 
@@ -1018,12 +1034,11 @@
         mIsRingerDrawerOpen = false;
     }
 
-    public void initSettingsH() {
+    private void initSettingsH(int lockTaskModeState) {
         if (mSettingsView != null) {
             mSettingsView.setVisibility(
                     mDeviceProvisionedController.isCurrentUserSetup() &&
-                            mActivityManager.getLockTaskModeState() == LOCK_TASK_MODE_NONE ?
-                            VISIBLE : GONE);
+                            lockTaskModeState == LOCK_TASK_MODE_NONE ? VISIBLE : GONE);
         }
         if (mSettingsIcon != null) {
             mSettingsIcon.setOnClickListener(v -> {
@@ -1251,27 +1266,54 @@
         mHandler.obtainMessage(H.DISMISS, reason, 0).sendToTarget();
     }
 
-    private void showH(int reason) {
+    private Animator.AnimatorListener getJankListener(View v, String type, long timeout) {
+        return new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(@NonNull Animator animation) {
+                mInteractionJankMonitor.begin(Builder.withView(CUJ_VOLUME_CONTROL, v).setTag(type)
+                        .setTimeout(timeout));
+            }
+
+            @Override
+            public void onAnimationEnd(@NonNull Animator animation) {
+                mInteractionJankMonitor.end(CUJ_VOLUME_CONTROL);
+            }
+
+            @Override
+            public void onAnimationCancel(@NonNull Animator animation) {
+                mInteractionJankMonitor.cancel(CUJ_VOLUME_CONTROL);
+            }
+
+            @Override
+            public void onAnimationRepeat(@NonNull Animator animation) {
+                // no-op
+            }
+        };
+    }
+
+    private void showH(int reason, boolean keyguardLocked, int lockTaskModeState) {
+        Trace.beginSection("VolumeDialogImpl#showH");
         if (D.BUG) Log.d(TAG, "showH r=" + Events.SHOW_REASONS[reason]);
         mHandler.removeMessages(H.SHOW);
         mHandler.removeMessages(H.DISMISS);
         rescheduleTimeoutH();
 
         if (mConfigChanged) {
-            initDialog(); // resets mShowing to false
+            initDialog(lockTaskModeState); // resets mShowing to false
             mConfigurableTexts.update();
             mConfigChanged = false;
         }
 
-        initSettingsH();
+        initSettingsH(lockTaskModeState);
         mShowing = true;
         mIsAnimatingDismiss = false;
         mDialog.show();
-        Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
+        Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, keyguardLocked);
         mController.notifyVisible(true);
         mController.getCaptionsComponentState(false);
         checkODICaptionsTooltip(false);
         updateBackgroundForDrawerClosedAmount();
+        Trace.endSection();
     }
 
     protected void rescheduleTimeoutH() {
@@ -1305,6 +1347,7 @@
     }
 
     protected void dismissH(int reason) {
+        Trace.beginSection("VolumeDialogImpl#dismissH");
         if (D.BUG) {
             Log.d(TAG, "mDialog.dismiss() reason: " + Events.DISMISS_REASONS[reason]
                     + " from: " + Debug.getCaller());
@@ -1335,7 +1378,8 @@
                     hideRingerDrawer();
                 }, 50));
         if (!shouldSlideInVolumeTray()) animator.translationX(mDialogView.getWidth() / 2.0f);
-        animator.start();
+        animator.setListener(getJankListener(getDialogView(), TYPE_DISMISS,
+                mDialogHideAnimationDurationMs)).start();
         checkODICaptionsTooltip(true);
         mController.notifyVisible(false);
         synchronized (mSafetyWarningLock) {
@@ -1344,6 +1388,7 @@
                 mSafetyWarning.dismiss();
             }
         }
+        Trace.endSection();
     }
 
     private boolean showActiveStreamOnly() {
@@ -1383,6 +1428,7 @@
     }
 
     private void updateRowsH(final VolumeRow activeRow) {
+        Trace.beginSection("VolumeDialogImpl#updateRowsH");
         if (D.BUG) Log.d(TAG, "updateRowsH");
         if (!mShowing) {
             trimObsoleteH();
@@ -1446,6 +1492,7 @@
         }
 
         updateBackgroundForDrawerClosedAmount();
+        Trace.endSection();
     }
 
     protected void updateRingerH() {
@@ -1730,7 +1777,9 @@
         final boolean enableSlider = !zenMuted;
         final int vlevel = row.ss.muted && (!isRingStream && !zenMuted) ? 0
                 : row.ss.level;
+        Trace.beginSection("VolumeDialogImpl#updateVolumeRowSliderH");
         updateVolumeRowSliderH(row, enableSlider, vlevel);
+        Trace.endSection();
         if (row.number != null) row.number.setText(Integer.toString(vlevel));
     }
 
@@ -1760,14 +1809,6 @@
                 mContext, com.android.internal.R.attr.textColorOnAccent);
 
         row.sliderProgressSolid.setTintList(colorTint);
-        if (row.sliderBgIcon != null) {
-            row.sliderBgIcon.setTintList(colorTint);
-        }
-
-        if (row.sliderBgSolid != null) {
-            row.sliderBgSolid.setTintList(bgTint);
-        }
-
         if (row.sliderProgressIcon != null) {
             row.sliderProgressIcon.setTintList(bgTint);
         }
@@ -1824,6 +1865,8 @@
                 }
                 row.animTargetProgress = newProgress;
                 row.anim.setDuration(UPDATE_ANIMATION_DURATION);
+                row.anim.addListener(
+                        getJankListener(row.view, TYPE_UPDATE, UPDATE_ANIMATION_DURATION));
                 row.anim.start();
             } else {
                 // update slider directly to clamped value
@@ -1994,8 +2037,8 @@
     private final VolumeDialogController.Callbacks mControllerCallbackH
             = new VolumeDialogController.Callbacks() {
         @Override
-        public void onShowRequested(int reason) {
-            showH(reason);
+        public void onShowRequested(int reason, boolean keyguardLocked, int lockTaskModeState) {
+            showH(reason, keyguardLocked, lockTaskModeState);
         }
 
         @Override
@@ -2078,7 +2121,8 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case SHOW: showH(msg.arg1); break;
+                case SHOW: showH(msg.arg1, VolumeDialogImpl.this.mKeyguard.isKeyguardLocked(),
+                        VolumeDialogImpl.this.mActivityManager.getLockTaskModeState()); break;
                 case DISMISS: dismissH(msg.arg1); break;
                 case RECHECK: recheckH((VolumeRow) msg.obj); break;
                 case RECHECK_ALL: recheckH(null); break;
@@ -2100,7 +2144,7 @@
          * within the bounds of the volume dialog, will fall through to the window below.
          */
         @Override
-        public boolean dispatchTouchEvent(MotionEvent ev) {
+        public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
             rescheduleTimeoutH();
             return super.dispatchTouchEvent(ev);
         }
@@ -2124,7 +2168,7 @@
          * volume dialog.
          */
         @Override
-        public boolean onTouchEvent(MotionEvent event) {
+        public boolean onTouchEvent(@NonNull MotionEvent event) {
             if (mShowing) {
                 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                     dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
@@ -2213,8 +2257,6 @@
         private View view;
         private TextView header;
         private ImageButton icon;
-        private Drawable sliderBgSolid;
-        private AlphaTintDrawableWrapper sliderBgIcon;
         private Drawable sliderProgressSolid;
         private AlphaTintDrawableWrapper sliderProgressIcon;
         private SeekBar slider;
@@ -2228,7 +2270,6 @@
         private int iconMuteRes;
         private boolean important;
         private boolean defaultStream;
-        private ColorStateList cachedTint;
         private int iconState;  // from Events
         private ObjectAnimator anim;  // slider progress animation for non-touch-related updates
         private int animTargetProgress;
@@ -2243,9 +2284,6 @@
             if (sliderProgressIcon != null) {
                 sliderProgressIcon.setDrawable(view.getResources().getDrawable(iconRes, theme));
             }
-            if (sliderBgIcon != null) {
-                sliderBgIcon.setDrawable(view.getResources().getDrawable(iconRes, theme));
-            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index 79aa643..f3855bd 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.media.AudioManager;
 
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.VolumeDialog;
@@ -51,7 +52,8 @@
             DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
             MediaOutputDialogFactory mediaOutputDialogFactory,
-            ActivityStarter activityStarter) {
+            ActivityStarter activityStarter,
+            InteractionJankMonitor interactionJankMonitor) {
         VolumeDialogImpl impl = new VolumeDialogImpl(
                 context,
                 volumeDialogController,
@@ -59,7 +61,8 @@
                 deviceProvisionedController,
                 configurationController,
                 mediaOutputDialogFactory,
-                activityStarter);
+                activityStarter,
+                interactionJankMonitor);
         impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
         impl.setAutomute(true);
         impl.setSilentMode(false);
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
index acff871..8c842f1 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/QuickAccessWalletController.java
@@ -19,7 +19,6 @@
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE;
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE;
 
-import android.annotation.CallbackExecutor;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
@@ -65,7 +64,6 @@
     private static final long RECREATION_TIME_WINDOW = TimeUnit.MINUTES.toMillis(10L);
     private final Context mContext;
     private final Executor mExecutor;
-    private final Executor mCallbackExecutor;
     private final Executor mBgExecutor;
     private final SecureSettings mSecureSettings;
     private final SystemClock mClock;
@@ -82,14 +80,12 @@
     public QuickAccessWalletController(
             Context context,
             @Main Executor executor,
-            @CallbackExecutor Executor callbackExecutor,
             @Background Executor bgExecutor,
             SecureSettings secureSettings,
             QuickAccessWalletClient quickAccessWalletClient,
             SystemClock clock) {
         mContext = context;
         mExecutor = executor;
-        mCallbackExecutor = callbackExecutor;
         mBgExecutor = bgExecutor;
         mSecureSettings = secureSettings;
         mQuickAccessWalletClient = quickAccessWalletClient;
@@ -180,7 +176,7 @@
         int iconSizePx = mContext.getResources().getDimensionPixelSize(R.dimen.wallet_icon_size);
         GetWalletCardsRequest request =
                 new GetWalletCardsRequest(cardWidth, cardHeight, iconSizePx, /* maxCards= */ 1);
-        mQuickAccessWalletClient.getWalletCards(mExecutor, request, cardsRetriever);
+        mQuickAccessWalletClient.getWalletCards(mBgExecutor, request, cardsRetriever);
     }
 
     /**
@@ -212,7 +208,7 @@
     public void startQuickAccessUiIntent(ActivityStarter activityStarter,
             ActivityLaunchAnimator.Controller animationController,
             boolean hasCard) {
-        mQuickAccessWalletClient.getWalletPendingIntent(mCallbackExecutor,
+        mQuickAccessWalletClient.getWalletPendingIntent(mBgExecutor,
                 walletPendingIntent -> {
                     if (walletPendingIntent != null) {
                         startQuickAccessViaPendingIntent(walletPendingIntent, activityStarter,
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 542a537..83fc281 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -271,6 +271,11 @@
                         mBubbles.onCurrentProfilesChanged(currentProfiles);
                     }
 
+                    @Override
+                    public void onUserRemoved(int userId) {
+                        mBubbles.onUserRemoved(userId);
+                    }
+
                 });
 
         mSysuiProxy = new Bubbles.SysuiProxy() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index 7c211b2..57253af 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -159,6 +159,7 @@
         when(mContext.getDisplay()).thenReturn(mDisplay);
         // Not support hwc layer by default
         doReturn(null).when(mDisplay).getDisplayDecorationSupport();
+        doReturn(mDisplayMode).when(mDisplay).getMode();
 
         when(mMockTypedArray.length()).thenReturn(0);
         mPrivacyDotTopLeftDecorProvider = spy(new PrivacyDotCornerDecorProviderImpl(
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 6eba215..431739b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -367,6 +367,73 @@
         assertThat(controllerOverlay.matchesRequestId(REQUEST_ID)).isTrue()
         assertThat(controllerOverlay.matchesRequestId(REQUEST_ID + 1)).isFalse()
     }
+
+    @Test
+    fun testTouchOutsideAreaNoRotation() = withReason(REASON_ENROLL_ENROLLING) {
+        val touchHints =
+            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
+        val rotation = Surface.ROTATION_0
+        // touch at 0 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[0])
+        // touch at 90 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[1])
+        // touch at 180 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[2])
+        // touch at 270 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[3])
+    }
+
+    fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) {
+        val touchHints =
+            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
+        val rotation = Surface.ROTATION_90
+        // touch at 0 degrees -> 90 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[1])
+        // touch at 90 degrees -> 180 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[2])
+        // touch at 180 degrees -> 270 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[3])
+        // touch at 270 degrees -> 0 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[0])
+    }
+
+    fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) {
+        val touchHints =
+            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
+        val rotation = Surface.ROTATION_270
+        // touch at 0 degrees -> 270 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[3])
+        // touch at 90 degrees -> 0 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, -1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[0])
+        // touch at 180 degrees -> 90 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(-1.0f /* x */, 0.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[1])
+        // touch at 270 degrees -> 180 degrees
+        assertThat(controllerOverlay.onTouchOutsideOfSensorAreaImpl(0.0f /* x */, 1.0f /* y */,
+                0.0f /* sensorX */, 0.0f /* sensorY */, rotation))
+                .isEqualTo(touchHints[2])
+    }
 }
 
 private class EnrollListener(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 290965c..e7e8e73 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -117,6 +117,7 @@
     private UdfpsController mUdfpsController;
 
     // Dependencies
+    private FakeExecutor mBiometricsExecutor;
     private Execution mExecution;
     @Mock
     private LayoutInflater mLayoutInflater;
@@ -234,6 +235,10 @@
                 true /* resetLockoutRequiresHardwareAuthToken */));
         when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(props);
         mFgExecutor = new FakeExecutor(new FakeSystemClock());
+
+        // Create a fake background executor.
+        mBiometricsExecutor = new FakeExecutor(new FakeSystemClock());
+
         mUdfpsController = new UdfpsController(
                 mContext,
                 mExecution,
@@ -265,7 +270,8 @@
                 mLatencyTracker,
                 mActivityLaunchAnimator,
                 Optional.of(mAlternateTouchProvider),
-                mBroadcastSender);
+                mBroadcastSender,
+                mBiometricsExecutor);
         verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
         mOverlayController = mOverlayCaptor.getValue();
         verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
@@ -298,6 +304,7 @@
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
         MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
+        mBiometricsExecutor.runAllReady();
         downEvent.recycle();
 
         // THEN notify keyguard authenticate to dismiss the keyguard
@@ -336,6 +343,7 @@
             mFgExecutor.runAllReady();
         }
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent);
+        mBiometricsExecutor.runAllReady();
         moveEvent.recycle();
 
         // THEN notify keyguard authenticate to dismiss the keyguard
@@ -359,10 +367,12 @@
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
         MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
+        mBiometricsExecutor.runAllReady();
         downEvent.recycle();
         MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent);
+        mBiometricsExecutor.runAllReady();
         moveEvent.recycle();
 
         // THEN notify keyguard authenticate to dismiss the keyguard
@@ -516,9 +526,11 @@
         MotionEvent event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor,
                 touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
+        mBiometricsExecutor.runAllReady();
         event.recycle();
         event = obtainMotionEvent(ACTION_MOVE, displayWidth, displayHeight, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
+        mBiometricsExecutor.runAllReady();
         event.recycle();
         verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
                 eq(expectedY), eq(expectedMinor), eq(expectedMajor));
@@ -530,9 +542,11 @@
                         Surface.ROTATION_90));
         event = obtainMotionEvent(ACTION_DOWN, displayHeight, 0, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
+        mBiometricsExecutor.runAllReady();
         event.recycle();
         event = obtainMotionEvent(ACTION_MOVE, displayHeight, 0, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
+        mBiometricsExecutor.runAllReady();
         event.recycle();
         verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
                 eq(expectedY), eq(expectedMinor), eq(expectedMajor));
@@ -544,9 +558,11 @@
                         Surface.ROTATION_270));
         event = obtainMotionEvent(ACTION_DOWN, 0, displayWidth, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
+        mBiometricsExecutor.runAllReady();
         event.recycle();
         event = obtainMotionEvent(ACTION_MOVE, 0, displayWidth, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
+        mBiometricsExecutor.runAllReady();
         event.recycle();
         verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
                 eq(expectedY), eq(expectedMinor), eq(expectedMajor));
@@ -559,9 +575,11 @@
         // ROTATION_180 is not supported. It should be treated like ROTATION_0.
         event = obtainMotionEvent(ACTION_DOWN, displayWidth, displayHeight, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
+        mBiometricsExecutor.runAllReady();
         event.recycle();
         event = obtainMotionEvent(ACTION_MOVE, displayWidth, displayHeight, touchMinor, touchMajor);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);
+        mBiometricsExecutor.runAllReady();
         event.recycle();
         verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(expectedX),
                 eq(expectedY), eq(expectedMinor), eq(expectedMajor));
@@ -581,11 +599,13 @@
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
         MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
+        mBiometricsExecutor.runAllReady();
         downEvent.recycle();
         MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0);
 
         // FIX THIS TEST
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent);
+        mBiometricsExecutor.runAllReady();
         moveEvent.recycle();
         // THEN FingerprintManager is notified about onPointerDown
         verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID), eq(0), eq(0), eq(0f),
@@ -598,6 +618,7 @@
         verify(mLatencyTracker, never()).onActionEnd(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE));
         // AND onIlluminatedRunnable notifies FingerprintManager about onUiReady
         mOnIlluminatedRunnableCaptor.getValue().run();
+        mBiometricsExecutor.runAllReady();
         InOrder inOrder = inOrder(mAlternateTouchProvider, mLatencyTracker);
         inOrder.verify(mAlternateTouchProvider).onUiReady();
         inOrder.verify(mLatencyTracker).onActionEnd(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE));
@@ -617,6 +638,7 @@
         // AND onIlluminatedRunnable that notifies FingerprintManager is set
         verify(mUdfpsView).startIllumination(mOnIlluminatedRunnableCaptor.capture());
         mOnIlluminatedRunnableCaptor.getValue().run();
+        mBiometricsExecutor.runAllReady();
         verify(mAlternateTouchProvider).onPointerDown(eq(TEST_REQUEST_ID),
                 eq(0), eq(0), eq(3f) /* minor */, eq(2f) /* major */);
         verify(mFingerprintManager, never()).onPointerDown(anyLong(), anyInt(), anyInt(), anyInt(),
@@ -740,9 +762,11 @@
         verify(mUdfpsView).setOnTouchListener(mTouchListenerCaptor.capture());
         MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent);
+        mBiometricsExecutor.runAllReady();
         downEvent.recycle();
         MotionEvent moveEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 0, 0, 0);
         mTouchListenerCaptor.getValue().onTouch(mUdfpsView, moveEvent);
+        mBiometricsExecutor.runAllReady();
         moveEvent.recycle();
 
         // THEN NO haptic played
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
index 0720bdb..bd029a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/dump/LogBufferHelper.kt
@@ -24,7 +24,7 @@
  * Creates a LogBuffer that will echo everything to logcat, which is useful for debugging tests.
  */
 fun logcatLogBuffer(name: String = "EchoToLogcatLogBuffer") =
-    LogBuffer(name, 50, 50, LogcatEchoTrackerAlways())
+    LogBuffer(name, 50, LogcatEchoTrackerAlways())
 
 /**
  * A [LogcatEchoTracker] that always allows echoing to the logcat.
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 2abc666..a4d2238 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,8 @@
 import com.android.keyguard.KeyguardViewController
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController
+import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -52,6 +54,11 @@
     private lateinit var surfaceTransactionApplier: SyncRtSurfaceTransactionApplier
     @Mock
     private lateinit var statusBarStateController: SysuiStatusBarStateController
+    @Mock
+    private lateinit var notificationShadeWindowController: NotificationShadeWindowController
+
+    @Mock
+    private lateinit var launcherUnlockAnimationController: ILauncherUnlockAnimationController.Stub
 
     private lateinit var remoteAnimationTarget: RemoteAnimationTarget
 
@@ -60,8 +67,11 @@
         MockitoAnnotations.initMocks(this)
         keyguardUnlockAnimationController = KeyguardUnlockAnimationController(
             context, keyguardStateController, { keyguardViewMediator }, keyguardViewController,
-            featureFlags, { biometricUnlockController }, statusBarStateController
+            featureFlags, { biometricUnlockController }, statusBarStateController,
+            notificationShadeWindowController
         )
+        keyguardUnlockAnimationController.setLauncherUnlockController(
+            launcherUnlockAnimationController)
 
         `when`(keyguardViewController.viewRootImpl).thenReturn(mock(ViewRootImpl::class.java))
 
@@ -194,4 +204,18 @@
         assertTrue(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation())
         assertFalse(keyguardUnlockAnimationController.surfaceBehindAlphaAnimator.isRunning)
     }
+
+    @Test
+    fun doNotPlayCannedUnlockAnimation_ifLaunchingApp() {
+        `when`(notificationShadeWindowController.isLaunchingActivity).thenReturn(true)
+
+        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
+            remoteAnimationTarget,
+            0 /* startTime */,
+            true /* requestedShowSurfaceBehindKeyguard */
+        )
+
+        assertFalse(keyguardUnlockAnimationController.canPerformInWindowLauncherAnimations())
+        assertFalse(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation())
+    }
 }
\ No newline at end of file
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 c532ed5..24d0515 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
@@ -135,8 +135,7 @@
                 .startMocking();
         MockitoAnnotations.initMocks(this);
 
-        when(mLockIconView.getResources()).thenReturn(mResources);
-        when(mLockIconView.getContext()).thenReturn(mContext);
+        setupLockIconViewMocks();
         when(mContext.getResources()).thenReturn(mResources);
         when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager);
         Rect windowBounds = new Rect(0, 0, 800, 1200);
@@ -206,13 +205,14 @@
     }
 
     @Test
-    public void testUpdateFingerprintLocationOnAuthenticatorsRegistered() {
+    public void testUpdateLockIconLocationOnAuthenticatorsRegistered() {
         // GIVEN fp sensor location is not available pre-init
         when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
         when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
         mLockIconViewController.init();
         captureAttachListener();
         mAttachListener.onViewAttachedToWindow(mLockIconView);
+        resetLockIconView(); // reset any method call counts for when we verify method calls later
 
         // GIVEN fp sensor location is available post-attached
         captureAuthControllerCallback();
@@ -228,6 +228,29 @@
     }
 
     @Test
+    public void testUpdateLockIconLocationOnUdfpsLocationChanged() {
+        // GIVEN fp sensor location is not available pre-init
+        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+        when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
+        mLockIconViewController.init();
+        captureAttachListener();
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+        resetLockIconView(); // reset any method call counts for when we verify method calls later
+
+        // GIVEN fp sensor location is available post-attached
+        captureAuthControllerCallback();
+        Pair<Float, PointF> udfps = setupUdfps();
+
+        // WHEN udfps location changes
+        mAuthControllerCallback.onUdfpsLocationChanged();
+        mDelayableExecutor.runAllReady();
+
+        // THEN lock icon view location is updated with the same coordinates as auth controller vals
+        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+                eq(PADDING));
+    }
+
+    @Test
     public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() {
         // GIVEN Udpfs sensor location is available
         setupUdfps();
@@ -440,4 +463,14 @@
                 mKeyguardUpdateMonitorCallbackCaptor.capture());
         mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
     }
+
+    private void setupLockIconViewMocks() {
+        when(mLockIconView.getResources()).thenReturn(mResources);
+        when(mLockIconView.getContext()).thenReturn(mContext);
+    }
+
+    private void resetLockIconView() {
+        reset(mLockIconView);
+        setupLockIconViewMocks();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
index cd5740d..e9db8cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityControllerTest.java
@@ -60,6 +60,13 @@
 public class WorkLockActivityControllerTest extends SysuiTestCase {
     private static final int USER_ID = 333;
     private static final int TASK_ID = 444;
+    private static final ActivityManager.RunningTaskInfo TASK_INFO =
+            new ActivityManager.RunningTaskInfo();
+
+    static {
+        TASK_INFO.userId = USER_ID;
+        TASK_INFO.taskId = TASK_ID;
+    }
 
     private @Mock Context mContext;
     private @Mock TaskStackChangeListeners mTaskStackChangeListeners;
@@ -91,7 +98,7 @@
         setActivityStartCode(TASK_ID, true /*taskOverlay*/, ActivityManager.START_SUCCESS);
 
         // And the controller receives a message saying the profile is locked,
-        mTaskStackListener.onTaskProfileLocked(TASK_ID, USER_ID);
+        mTaskStackListener.onTaskProfileLocked(TASK_INFO);
 
         // The overlay should start and the task the activity started in should not be removed.
         verifyStartActivity(TASK_ID, true /*taskOverlay*/);
@@ -104,7 +111,7 @@
         setActivityStartCode(TASK_ID, true /*taskOverlay*/, ActivityManager.START_CLASS_NOT_FOUND);
 
         // And the controller receives a message saying the profile is locked,
-        mTaskStackListener.onTaskProfileLocked(TASK_ID, USER_ID);
+        mTaskStackListener.onTaskProfileLocked(TASK_INFO);
 
         // The task the activity started in should be removed to prevent the locked task from
         // being shown.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java
index e4c387a..640e6dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.keyguard;
 
-import static android.app.ActivityManager.TaskDescription;
-
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.eq;
@@ -25,14 +23,15 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.ColorInt;
 import android.annotation.UserIdInt;
-import android.app.KeyguardManager;
-import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.Color;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
 import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -53,18 +52,21 @@
 @RunWith(AndroidJUnit4.class)
 public class WorkLockActivityTest extends SysuiTestCase {
     private static final @UserIdInt int USER_ID = 270;
-    private static final String TASK_LABEL = "task label";
+    private static final String CALLING_PACKAGE_NAME = "com.android.test";
 
-    private @Mock DevicePolicyManager mDevicePolicyManager;
-    private @Mock KeyguardManager mKeyguardManager;
+    private @Mock UserManager mUserManager;
+    private @Mock PackageManager mPackageManager;
     private @Mock Context mContext;
     private @Mock BroadcastDispatcher mBroadcastDispatcher;
+    private @Mock Drawable mDrawable;
+    private @Mock Drawable mBadgedDrawable;
 
     private WorkLockActivity mActivity;
 
     private static class WorkLockActivityTestable extends WorkLockActivity {
-        WorkLockActivityTestable(Context baseContext, BroadcastDispatcher broadcastDispatcher) {
-            super(broadcastDispatcher);
+        WorkLockActivityTestable(Context baseContext, BroadcastDispatcher broadcastDispatcher,
+                UserManager userManager, PackageManager packageManager) {
+            super(broadcastDispatcher, userManager, packageManager);
             attachBaseContext(baseContext);
         }
     }
@@ -73,46 +75,26 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        when(mContext.getSystemService(eq(Context.DEVICE_POLICY_SERVICE)))
-                .thenReturn(mDevicePolicyManager);
-        when(mContext.getSystemService(eq(Context.KEYGUARD_SERVICE)))
-                .thenReturn(mKeyguardManager);
-
         if (Looper.myLooper() == null) {
             Looper.prepare();
         }
-        mActivity = new WorkLockActivityTestable(mContext, mBroadcastDispatcher);
+        mActivity = new WorkLockActivityTestable(mContext, mBroadcastDispatcher, mUserManager,
+                mPackageManager);
     }
 
     @Test
-    public void testBackgroundAlwaysOpaque() throws Exception {
-        final @ColorInt int orgColor = Color.rgb(250, 199, 67);
-        when(mDevicePolicyManager.getOrganizationColorForUser(eq(USER_ID))).thenReturn(orgColor);
-
-        final @ColorInt int opaqueColor= Color.rgb(164, 198, 57);
-        final @ColorInt int transparentColor = Color.argb(0, 0, 0, 0);
-        TaskDescription opaque = new TaskDescription(null, null, opaqueColor);
-        TaskDescription transparent = new TaskDescription(null, null, transparentColor);
-
-        // When a task description is provided with a suitable (opaque) primaryColor, it should be
-        // used as the scrim's background color.
+    public void testGetBadgedIcon() throws Exception {
+        ApplicationInfo info = new ApplicationInfo();
+        when(mPackageManager.getApplicationInfoAsUser(eq(CALLING_PACKAGE_NAME), any(),
+                eq(USER_ID))).thenReturn(info);
+        when(mPackageManager.getApplicationIcon(eq(info))).thenReturn(mDrawable);
+        when(mUserManager.getBadgedIconForUser(any(), eq(UserHandle.of(USER_ID)))).thenReturn(
+                mBadgedDrawable);
         mActivity.setIntent(new Intent()
                 .putExtra(Intent.EXTRA_USER_ID, USER_ID)
-                .putExtra(WorkLockActivity.EXTRA_TASK_DESCRIPTION, opaque));
-        assertEquals(opaqueColor, mActivity.getPrimaryColor());
+                .putExtra(Intent.EXTRA_PACKAGE_NAME, CALLING_PACKAGE_NAME));
 
-        // When a task description is provided but has no primaryColor / the primaryColor is
-        // transparent, the organization color should be used instead.
-        mActivity.setIntent(new Intent()
-                .putExtra(Intent.EXTRA_USER_ID, USER_ID)
-                .putExtra(WorkLockActivity.EXTRA_TASK_DESCRIPTION, transparent));
-        assertEquals(orgColor, mActivity.getPrimaryColor());
-
-        // When no task description is provided at all, it should be treated like a transparent
-        // description and the organization color shown instead.
-        mActivity.setIntent(new Intent()
-                .putExtra(Intent.EXTRA_USER_ID, USER_ID));
-        assertEquals(orgColor, mActivity.getPrimaryColor());
+        assertEquals(mBadgedDrawable, mActivity.getBadgedIcon());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
index 0d917e3..ceb811b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
@@ -63,6 +63,7 @@
     @Mock lateinit var falsingManager: FalsingManager
     @Mock lateinit var dumpManager: DumpManager
     @Mock lateinit var logger: MediaUiEventLogger
+    @Mock lateinit var debugLogger: MediaCarouselControllerLogger
 
     private val clock = FakeSystemClock()
     private lateinit var mediaCarouselController: MediaCarouselController
@@ -83,7 +84,8 @@
             falsingCollector,
             falsingManager,
             dumpManager,
-            logger
+            logger,
+            debugLogger
         )
 
         MediaPlayerData.clear()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
index 91c0cc2..823d4ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
@@ -23,6 +23,8 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
@@ -63,10 +65,13 @@
     @Mock private lateinit var mediaControllerFactory: MediaControllerFactory
     @Mock private lateinit var mediaController: MediaController
     @Mock private lateinit var logger: MediaTimeoutLogger
+    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
     private lateinit var executor: FakeExecutor
     @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
     @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
     @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
+    @Captor private lateinit var dozingCallbackCaptor:
+        ArgumentCaptor<StatusBarStateController.StateListener>
     @JvmField @Rule val mockito = MockitoJUnit.rule()
     private lateinit var metadataBuilder: MediaMetadata.Builder
     private lateinit var playbackBuilder: PlaybackState.Builder
@@ -74,12 +79,19 @@
     private lateinit var mediaData: MediaData
     private lateinit var resumeData: MediaData
     private lateinit var mediaTimeoutListener: MediaTimeoutListener
+    private var clock = FakeSystemClock()
 
     @Before
     fun setup() {
         `when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
-        executor = FakeExecutor(FakeSystemClock())
-        mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor, logger)
+        executor = FakeExecutor(clock)
+        mediaTimeoutListener = MediaTimeoutListener(
+            mediaControllerFactory,
+            executor,
+            logger,
+            statusBarStateController,
+            clock
+        )
         mediaTimeoutListener.timeoutCallback = timeoutCallback
         mediaTimeoutListener.stateCallback = stateCallback
 
@@ -530,6 +542,49 @@
         verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!))
     }
 
+    @Test
+    fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() {
+        // When paused media is loaded
+        testOnMediaDataLoaded_registersPlaybackListener()
+        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
+            .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
+        verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
+
+        // And we doze past the scheduled timeout
+        val time = clock.currentTimeMillis()
+        clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT)
+        assertThat(executor.numPending()).isEqualTo(1)
+
+        // Then when no longer dozing, the timeout runs immediately
+        dozingCallbackCaptor.value.onDozingChanged(false)
+        verify(timeoutCallback).invoke(eq(KEY), eq(true))
+        verify(logger).logTimeout(eq(KEY))
+
+        // and cancel any later scheduled timeout
+        verify(logger).logTimeoutCancelled(eq(KEY), any())
+        assertThat(executor.numPending()).isEqualTo(0)
+    }
+
+    @Test
+    fun testTimeoutCallback_dozeShortTime_notInvokedOnWakeup() {
+        // When paused media is loaded
+        val time = clock.currentTimeMillis()
+        clock.setElapsedRealtime(time)
+        testOnMediaDataLoaded_registersPlaybackListener()
+        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
+            .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build())
+        verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
+
+        // And we doze, but not past the scheduled timeout
+        clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT / 2L)
+        assertThat(executor.numPending()).isEqualTo(1)
+
+        // Then when no longer dozing, the timeout remains scheduled
+        dozingCallbackCaptor.value.onDozingChanged(false)
+        verify(timeoutCallback, never()).invoke(eq(KEY), eq(true))
+        assertThat(executor.numPending()).isEqualTo(1)
+    }
+
     private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
         `when`(mediaController.playbackState).thenReturn(state)
         mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
index 863484b..890e4de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/monet/ColorSchemeTest.java
@@ -133,7 +133,7 @@
                 Style.VIBRANT /* style */);
         int neutralMid = colorScheme.getNeutral1().get(colorScheme.getNeutral1().size() / 2);
         Cam cam = Cam.fromInt(neutralMid);
-        Assert.assertTrue("chroma was " + cam.getChroma(), Math.floor(cam.getChroma()) <= 10.0);
+        Assert.assertTrue("chroma was " + cam.getChroma(), Math.floor(cam.getChroma()) <= 12.0);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java
index 4a6bbbc..346d1e6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java
@@ -24,7 +24,6 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -32,6 +31,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
@@ -41,7 +41,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
-import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.DeviceConfigProxyFake;
@@ -90,8 +89,9 @@
         when(mPackageManager.queryIntentActivities(any(Intent.class),
                 any(Integer.class))).thenReturn(resolveInfoList);
         when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)).thenReturn(true);
-        mContext.getOrCreateTestableResources().addOverride(R.string.def_qr_code_component,
-                defaultActivity);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.string.config_defaultQrCodeComponent, defaultActivity);
+
         mContext.getOrCreateTestableResources().addOverride(
                 android.R.bool.config_enableQrCodeScannerOnLockScreen, enableOnLockScreen);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
new file mode 100644
index 0000000..2927669
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.IActivityManager;
+import android.app.IForegroundServiceObserver;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.provider.DeviceConfig;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.DialogLaunchAnimator;
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.settings.UserTracker;
+import com.android.systemui.util.DeviceConfigProxyFake;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class FgsManagerControllerTest extends SysuiTestCase {
+
+    FakeSystemClock mSystemClock;
+    FakeExecutor mMainExecutor;
+    FakeExecutor mBackgroundExecutor;
+    DeviceConfigProxyFake mDeviceConfigProxyFake;
+
+    @Mock
+    IActivityManager mIActivityManager;
+    @Mock
+    PackageManager mPackageManager;
+    @Mock
+    UserTracker mUserTracker;
+    @Mock
+    DialogLaunchAnimator mDialogLaunchAnimator;
+    @Mock
+    BroadcastDispatcher mBroadcastDispatcher;
+    @Mock
+    DumpManager mDumpManager;
+
+    private FgsManagerController mFmc;
+
+    private IForegroundServiceObserver mIForegroundServiceObserver;
+    private UserTracker.Callback mUserTrackerCallback;
+    private BroadcastReceiver mShowFgsManagerReceiver;
+
+    private List<UserInfo> mUserProfiles;
+
+    @Before
+    public void setUp() throws RemoteException {
+        MockitoAnnotations.initMocks(this);
+
+        mDeviceConfigProxyFake = new DeviceConfigProxyFake();
+        mDeviceConfigProxyFake.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.TASK_MANAGER_ENABLED, "true", false);
+        mSystemClock = new FakeSystemClock();
+        mMainExecutor = new FakeExecutor(mSystemClock);
+        mBackgroundExecutor = new FakeExecutor(mSystemClock);
+
+        mUserProfiles = new ArrayList<>();
+        Mockito.doReturn(mUserProfiles).when(mUserTracker).getUserProfiles();
+
+        mFmc = createFgsManagerController();
+    }
+
+    @Test
+    public void testNumPackages() throws RemoteException {
+        setUserProfiles(0);
+
+        Binder b1 = new Binder();
+        Binder b2 = new Binder();
+        Assert.assertEquals(0, mFmc.getNumRunningPackages());
+        mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, true);
+        Assert.assertEquals(1, mFmc.getNumRunningPackages());
+        mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, true);
+        Assert.assertEquals(2, mFmc.getNumRunningPackages());
+        mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, false);
+        Assert.assertEquals(1, mFmc.getNumRunningPackages());
+        mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, false);
+        Assert.assertEquals(0, mFmc.getNumRunningPackages());
+    }
+
+    @Test
+    public void testNumPackagesDoesNotChangeWhenSecondFgsIsStarted() throws RemoteException {
+        setUserProfiles(0);
+
+        // Different tokens == different services
+        Binder b1 = new Binder();
+        Binder b2 = new Binder();
+        Assert.assertEquals(0, mFmc.getNumRunningPackages());
+        mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, true);
+        Assert.assertEquals(1, mFmc.getNumRunningPackages());
+        mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg1", 0, true);
+        Assert.assertEquals(1, mFmc.getNumRunningPackages());
+        mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, false);
+        Assert.assertEquals(1, mFmc.getNumRunningPackages());
+        mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg1", 0, false);
+        Assert.assertEquals(0, mFmc.getNumRunningPackages());
+    }
+
+    @Test
+    public void testNumPackagesListener() throws RemoteException {
+        setUserProfiles(0);
+
+        FgsManagerController.OnNumberOfPackagesChangedListener onNumberOfPackagesChangedListener =
+                Mockito.mock(FgsManagerController.OnNumberOfPackagesChangedListener.class);
+
+        mFmc.addOnNumberOfPackagesChangedListener(onNumberOfPackagesChangedListener);
+
+        Binder b1 = new Binder();
+        Binder b2 = new Binder();
+
+        verify(onNumberOfPackagesChangedListener, never()).onNumberOfPackagesChanged(anyInt());
+
+        mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, true);
+        mBackgroundExecutor.advanceClockToLast();
+        mBackgroundExecutor.runAllReady();
+        verify(onNumberOfPackagesChangedListener).onNumberOfPackagesChanged(1);
+
+        mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, true);
+        mBackgroundExecutor.advanceClockToLast();
+        mBackgroundExecutor.runAllReady();
+        verify(onNumberOfPackagesChangedListener).onNumberOfPackagesChanged(2);
+
+        mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, false);
+        mBackgroundExecutor.advanceClockToLast();
+        mBackgroundExecutor.runAllReady();
+        verify(onNumberOfPackagesChangedListener, times(2)).onNumberOfPackagesChanged(1);
+
+        mIForegroundServiceObserver.onForegroundStateChanged(b2, "pkg2", 0, false);
+        mBackgroundExecutor.advanceClockToLast();
+        mBackgroundExecutor.runAllReady();
+        verify(onNumberOfPackagesChangedListener).onNumberOfPackagesChanged(0);
+    }
+
+    @Test
+    public void testChangesSinceLastDialog() throws RemoteException {
+        setUserProfiles(0);
+
+        Assert.assertFalse(mFmc.getChangesSinceDialog());
+        mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg", 0, true);
+        Assert.assertTrue(mFmc.getChangesSinceDialog());
+    }
+
+    @Test
+    public void testProfilePackagesCounted() throws RemoteException {
+        setUserProfiles(0, 10);
+
+        mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg1", 0, true);
+        mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg2", 10, true);
+        Assert.assertEquals(2, mFmc.getNumRunningPackages());
+    }
+
+    @Test
+    public void testSecondaryUserPackagesAreNotCounted() throws RemoteException {
+        setUserProfiles(0);
+
+        mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg1", 0, true);
+        mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg2", 10, true);
+        Assert.assertEquals(1, mFmc.getNumRunningPackages());
+    }
+
+    @Test
+    public void testSecondaryUserPackagesAreCountedWhenUserSwitch() throws RemoteException {
+        setUserProfiles(0);
+
+        mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg1", 0, true);
+        mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg2", 10, true);
+        mIForegroundServiceObserver.onForegroundStateChanged(new Binder(), "pkg3", 10, true);
+
+        Assert.assertEquals(1, mFmc.getNumRunningPackages());
+
+        setUserProfiles(10);
+        Assert.assertEquals(2, mFmc.getNumRunningPackages());
+    }
+
+
+
+    FgsManagerController createFgsManagerController() throws RemoteException {
+        ArgumentCaptor<IForegroundServiceObserver> iForegroundServiceObserverArgumentCaptor =
+                ArgumentCaptor.forClass(IForegroundServiceObserver.class);
+        ArgumentCaptor<UserTracker.Callback> userTrackerCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(UserTracker.Callback.class);
+        ArgumentCaptor<BroadcastReceiver> showFgsManagerReceiverArgumentCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+
+        FgsManagerController result = new FgsManagerController(
+                mContext,
+                mMainExecutor,
+                mBackgroundExecutor,
+                mSystemClock,
+                mIActivityManager,
+                mPackageManager,
+                mUserTracker,
+                mDeviceConfigProxyFake,
+                mDialogLaunchAnimator,
+                mBroadcastDispatcher,
+                mDumpManager
+        );
+        result.init();
+
+        verify(mIActivityManager).registerForegroundServiceObserver(
+                iForegroundServiceObserverArgumentCaptor.capture()
+        );
+        verify(mUserTracker).addCallback(
+                userTrackerCallbackArgumentCaptor.capture(),
+                ArgumentMatchers.eq(mBackgroundExecutor)
+        );
+        verify(mBroadcastDispatcher).registerReceiver(
+                showFgsManagerReceiverArgumentCaptor.capture(),
+                argThat(fltr -> fltr.matchAction(Intent.ACTION_SHOW_FOREGROUND_SERVICE_MANAGER)),
+                eq(mMainExecutor),
+                isNull(),
+                eq(Context.RECEIVER_NOT_EXPORTED),
+                isNull()
+        );
+
+        mIForegroundServiceObserver = iForegroundServiceObserverArgumentCaptor.getValue();
+        mUserTrackerCallback = userTrackerCallbackArgumentCaptor.getValue();
+        mShowFgsManagerReceiver = showFgsManagerReceiverArgumentCaptor.getValue();
+
+        return result;
+    }
+
+    private void setUserProfiles(int current, int... profileUserIds) {
+        mUserProfiles.clear();
+        mUserProfiles.add(new UserInfo(current, "current:" + current, 0));
+        for (int id : profileUserIds) {
+            mUserProfiles.add(new UserInfo(id, "profile:" + id, 0));
+        }
+
+        if (mUserTrackerCallback != null) {
+            mUserTrackerCallback.onUserChanged(current, mock(Context.class));
+            mUserTrackerCallback.onProfilesChanged(mUserProfiles);
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java
new file mode 100644
index 0000000..b86713d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HotspotTileTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.os.Handler;
+import android.service.quicksettings.Tile;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.logging.MetricsLogger;
+import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.qs.QSTile;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.statusbar.policy.DataSaverController;
+import com.android.systemui.statusbar.policy.HotspotController;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class HotspotTileTest extends SysuiTestCase {
+
+    @Rule
+    public MockitoRule mRule = MockitoJUnit.rule();
+    @Mock
+    private QSTileHost mHost;
+    @Mock
+    private HotspotController mHotspotController;
+    @Mock
+    private DataSaverController mDataSaverController;
+
+    private TestableLooper mTestableLooper;
+    private HotspotTile mTile;
+    private QSTile.BooleanState mState = new QSTile.BooleanState();
+
+    @Before
+    public void setUp() throws Exception {
+        mTestableLooper = TestableLooper.get(this);
+        when(mHost.getContext()).thenReturn(mContext);
+        when(mHost.getUserContext()).thenReturn(mContext);
+        when(mDataSaverController.isDataSaverEnabled()).thenReturn(false);
+
+        mTile = new HotspotTile(
+                mHost,
+                mTestableLooper.getLooper(),
+                new Handler(mTestableLooper.getLooper()),
+                new FalsingManagerFake(),
+                mock(MetricsLogger.class),
+                mock(StatusBarStateController.class),
+                mock(ActivityStarter.class),
+                mock(QSLogger.class),
+                mHotspotController,
+                mDataSaverController
+        );
+
+        mTile.initialize();
+        mTestableLooper.processAllMessages();
+    }
+
+    @Test
+    public void handleUpdateState_wifiTetheringIsAllowed_stateIsNotUnavailable() {
+        MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
+                .spyStatic(WifiEnterpriseRestrictionUtils.class)
+                .startMocking();
+        when(WifiEnterpriseRestrictionUtils.isWifiTetheringAllowed(mContext)).thenReturn(true);
+
+        mTile.handleUpdateState(mState, null);
+
+        assertThat(mState.state).isNotEqualTo(Tile.STATE_UNAVAILABLE);
+        assertThat(String.valueOf(mState.secondaryLabel))
+                .isNotEqualTo(mContext.getString(R.string.wifitrackerlib_admin_restricted_network));
+        mockitoSession.finishMocking();
+    }
+
+    @Test
+    public void handleUpdateState_wifiTetheringIsDisallowed_stateIsUnavailable() {
+        MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
+                .spyStatic(WifiEnterpriseRestrictionUtils.class)
+                .startMocking();
+        when(WifiEnterpriseRestrictionUtils.isWifiTetheringAllowed(mContext)).thenReturn(false);
+
+        mTile.handleUpdateState(mState, null);
+
+        assertThat(mState.state).isEqualTo(Tile.STATE_UNAVAILABLE);
+        assertThat(String.valueOf(mState.secondaryLabel))
+                .isEqualTo(mContext.getString(R.string.wifitrackerlib_admin_restricted_network));
+        mockitoSession.finishMocking();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
index 6971c63..8cb530c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LSShadeTransitionLoggerTest.kt
@@ -30,7 +30,7 @@
     @Before
     fun setup() {
         logger = LSShadeTransitionLogger(
-                LogBuffer("Test", 10, 10, mock()),
+                LogBuffer("Test", 10, mock()),
                 gestureLogger,
                 displayMetrics)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 562c970..73e574e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -130,8 +130,8 @@
         whenever(statusbarStateController.state).thenReturn(StatusBarState.KEYGUARD)
         whenever(nsslController.isInLockedDownShade).thenReturn(false)
         whenever(qS.isFullyCollapsed).thenReturn(true)
-        whenever(lockScreenUserManager.userAllowsPrivateNotificationsInPublic(anyInt())).thenReturn(
-                true)
+        whenever(lockScreenUserManager.sensitiveNotifsNeedRedactionInPublic(anyInt()))
+                .thenReturn(false)
         whenever(lockScreenUserManager.shouldShowLockscreenNotifications()).thenReturn(true)
         whenever(lockScreenUserManager.isLockscreenPublicMode(anyInt())).thenReturn(true)
         whenever(falsingCollector.shouldEnforceBouncer()).thenReturn(false)
@@ -207,8 +207,8 @@
 
     @Test
     fun testTriggeringBouncerWhenPrivateNotificationsArentAllowed() {
-        whenever(lockScreenUserManager.userAllowsPrivateNotificationsInPublic(anyInt())).thenReturn(
-                false)
+        whenever(lockScreenUserManager.sensitiveNotifsNeedRedactionInPublic(anyInt()))
+                .thenReturn(true)
         transitionController.goToLockedShade(null)
         verify(statusbarStateController, never()).setState(anyInt())
         verify(statusbarStateController).setLeaveOpenOnKeyguardHide(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 7687d12..18937e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -53,11 +53,13 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.KeyguardNotificationSuppressor;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -106,6 +108,10 @@
     private BroadcastDispatcher mBroadcastDispatcher;
     @Mock
     private KeyguardStateController mKeyguardStateController;
+    @Mock
+    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Mock
+    private OverviewProxyService mOverviewProxyService;
 
     private UserInfo mCurrentUser;
     private UserInfo mSecondaryUser;
@@ -119,7 +125,6 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager);
 
         int currentUserId = ActivityManager.getCurrentUser();
         mSettings = new FakeSettings();
@@ -212,7 +217,7 @@
         mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false);
 
         // THEN current user's notification is redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
+        assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mCurrentUserNotif));
     }
 
     @Test
@@ -223,7 +228,7 @@
         mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false);
 
         // THEN current user's notification isn't redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
+        assertFalse(mLockscreenUserManager.notifNeedsRedactionInPublic(mCurrentUserNotif));
     }
 
     @Test
@@ -234,7 +239,7 @@
         mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false);
 
         // THEN work profile notification is redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
+        assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mWorkProfileNotif));
     }
 
     @Test
@@ -245,7 +250,7 @@
         mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false);
 
         // THEN work profile notification isn't redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
+        assertFalse(mLockscreenUserManager.notifNeedsRedactionInPublic(mWorkProfileNotif));
     }
 
     @Test
@@ -260,11 +265,11 @@
         mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false);
 
         // THEN the work profile notification doesn't need to be redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
+        assertFalse(mLockscreenUserManager.notifNeedsRedactionInPublic(mWorkProfileNotif));
 
         // THEN the current user and secondary user notifications do need to be redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
-        assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
+        assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mCurrentUserNotif));
+        assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mSecondaryUserNotif));
     }
 
     @Test
@@ -279,11 +284,11 @@
         mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false);
 
         // THEN the work profile notification needs to be redacted
-        assertTrue(mLockscreenUserManager.needsRedaction(mWorkProfileNotif));
+        assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mWorkProfileNotif));
 
         // THEN the current user and secondary user notifications don't need to be redacted
-        assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
-        assertFalse(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
+        assertFalse(mLockscreenUserManager.notifNeedsRedactionInPublic(mCurrentUserNotif));
+        assertFalse(mLockscreenUserManager.notifNeedsRedactionInPublic(mSecondaryUserNotif));
     }
 
     @Test
@@ -298,7 +303,7 @@
 
         // THEN the secondary profile notification still needs to be redacted because the current
         // user's setting takes precedence
-        assertTrue(mLockscreenUserManager.needsRedaction(mSecondaryUserNotif));
+        assertTrue(mLockscreenUserManager.notifNeedsRedactionInPublic(mSecondaryUserNotif));
     }
 
     @Test
@@ -418,9 +423,12 @@
                     context,
                     mBroadcastDispatcher,
                     mDevicePolicyManager,
+                    mKeyguardUpdateMonitor,
+                    () -> mEntryManager,
+                    () -> mOverviewProxyService,
                     mUserManager,
-                    (() -> mVisibilityProvider),
-                    (() -> mNotifCollection),
+                    () -> mVisibilityProvider,
+                    () -> mNotifCollection,
                     mClickNotifier,
                     NotificationLockscreenUserManagerTest.this.mKeyguardManager,
                     mStatusBarStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
index 7d06abf..e43ae0d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
@@ -124,8 +124,8 @@
     }
 
     private void allowPrivateNotificationsInPublic(boolean allow) {
-        when(mLockScreenUserManager.userAllowsPrivateNotificationsInPublic(anyInt())).thenReturn(
-                allow);
+        when(mLockScreenUserManager.sensitiveNotifsNeedRedactionInPublic(anyInt())).thenReturn(
+                !allow);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
index 0fff5f5..16b0376 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
@@ -294,7 +294,8 @@
 
         verify(mPresenter).updateNotificationViews(any());
         verify(mEntryListener).onEntryRemoved(
-                eq(mEntry), any(), eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON));
+                argThat(matchEntryOnKey()), any(),
+                eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON));
         verify(mRow).setRemoved();
 
         assertNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey()));
@@ -319,8 +320,8 @@
 
         mEntryManager.removeNotification("not_a_real_key", mRankingMap, UNDEFINED_DISMISS_REASON);
 
-        verify(mEntryListener, never()).onEntryRemoved(
-                eq(mEntry), any(), eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON));
+        verify(mEntryListener, never()).onEntryRemoved(argThat(matchEntryOnKey()), any(),
+                eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON));
     }
 
     /** Regression test for b/201097913. */
@@ -333,10 +334,10 @@
         // Verify that only the listener for the NEW pipeline is notified.
         // Old pipeline:
         verify(mEntryListener, never()).onEntryRemoved(
-                argThat(matchEntryOnSbn()), any(), anyBoolean(), anyInt());
+                argThat(matchEntryOnKey()), any(), anyBoolean(), anyInt());
         // New pipeline:
         verify(mNotifCollectionListener).onEntryRemoved(
-                argThat(matchEntryOnSbn()), anyInt());
+                argThat(matchEntryOnKey()), anyInt());
     }
 
     @Test
@@ -457,7 +458,7 @@
         // THEN the notification is retained
         assertNotNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey()));
         verify(mEntryListener, never()).onEntryRemoved(
-                eq(mEntry), any(), eq(false), eq(UNDEFINED_DISMISS_REASON));
+                argThat(matchEntryOnKey()), any(), eq(false), eq(UNDEFINED_DISMISS_REASON));
     }
 
     @Test
@@ -476,7 +477,7 @@
         // THEN the notification is removed
         assertNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey()));
         verify(mEntryListener).onEntryRemoved(
-                eq(mEntry), any(), eq(false), eq(UNDEFINED_DISMISS_REASON));
+                argThat(matchEntryOnKey()), any(), eq(false), eq(UNDEFINED_DISMISS_REASON));
     }
 
     @Test
@@ -541,7 +542,7 @@
 
         // GIVEN interceptor that intercepts that entry
         when(mRemoveInterceptor.onNotificationRemoveRequested(
-                eq(mEntry.getKey()), eq(mEntry), anyInt()))
+                eq(mEntry.getKey()), argThat(matchEntryOnKey()), anyInt()))
                 .thenReturn(true);
 
         // WHEN the notification is removed
@@ -549,7 +550,7 @@
 
         // THEN the interceptor intercepts & the entry is not removed & no listeners are called
         assertNotNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey()));
-        verify(mEntryListener, never()).onEntryRemoved(eq(mEntry),
+        verify(mEntryListener, never()).onEntryRemoved(argThat(matchEntryOnKey()),
                 any(NotificationVisibility.class), anyBoolean(), eq(UNDEFINED_DISMISS_REASON));
     }
 
@@ -560,7 +561,7 @@
 
         // GIVEN interceptor that doesn't intercept
         when(mRemoveInterceptor.onNotificationRemoveRequested(
-                eq(mEntry.getKey()), eq(mEntry), anyInt()))
+                eq(mEntry.getKey()), argThat(matchEntryOnKey()), anyInt()))
                 .thenReturn(false);
 
         // WHEN the notification is removed
@@ -568,7 +569,7 @@
 
         // THEN the interceptor intercepts & the entry is not removed & no listeners are called
         assertNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey()));
-        verify(mEntryListener, atLeastOnce()).onEntryRemoved(eq(mEntry),
+        verify(mEntryListener, atLeastOnce()).onEntryRemoved(argThat(matchEntryOnKey()),
                 any(NotificationVisibility.class), anyBoolean(), eq(UNDEFINED_DISMISS_REASON));
     }
 
@@ -663,9 +664,8 @@
                     PendingIntent.FLAG_IMMUTABLE)).build();
     }
 
-    // TODO(b/201321631): Update more tests to use this function instead of eq(mEntry).
-    private ArgumentMatcher<NotificationEntry> matchEntryOnSbn() {
-        return e -> e.getSbn().equals(mSbn);
+    private ArgumentMatcher<NotificationEntry> matchEntryOnKey() {
+        return e -> e.getKey().equals(mEntry.getKey());
     }
 
     private static class FakeNotificationLifetimeExtender implements NotificationLifetimeExtender {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 7068009..958d542 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -32,6 +32,8 @@
 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED;
 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -47,6 +49,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import static java.util.Collections.singletonList;
@@ -180,13 +183,14 @@
 
     @Test
     public void testGetGroupSummary() {
-        assertEquals(null, mCollection.getGroupSummary("group"));
-        NotifEvent summary = mNoMan.postNotif(
-                buildNotif(TEST_PACKAGE, 0)
-                        .setGroup(mContext, "group")
-                        .setGroupSummary(mContext, true));
+        final NotificationEntryBuilder entryBuilder = buildNotif(TEST_PACKAGE, 0)
+                .setGroup(mContext, "group")
+                .setGroupSummary(mContext, true);
+        final String groupKey = entryBuilder.build().getSbn().getGroupKey();
+        assertEquals(null, mCollection.getGroupSummary(groupKey));
+        NotifEvent summary = mNoMan.postNotif(entryBuilder);
 
-        final NotificationEntry entry = mCollection.getGroupSummary("group");
+        final NotificationEntry entry = mCollection.getGroupSummary(groupKey);
         assertEquals(summary.key, entry.getKey());
         assertEquals(summary.sbn, entry.getSbn());
         assertEquals(summary.ranking, entry.getRanking());
@@ -194,9 +198,9 @@
 
     @Test
     public void testIsOnlyChildInGroup() {
-        NotifEvent notif1 = mNoMan.postNotif(
-                buildNotif(TEST_PACKAGE, 1)
-                        .setGroup(mContext, "group"));
+        final NotificationEntryBuilder entryBuilder = buildNotif(TEST_PACKAGE, 1)
+                .setGroup(mContext, "group");
+        NotifEvent notif1 = mNoMan.postNotif(entryBuilder);
         final NotificationEntry entry = mCollection.getEntry(notif1.key);
         assertTrue(mCollection.isOnlyChildInGroup(entry));
 
@@ -1488,6 +1492,55 @@
     }
 
     @Test
+    public void testRegisterFutureDismissal() throws RemoteException {
+        // GIVEN a pipeline with one notification
+        NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
+        NotificationEntry entry = requireNonNull(mCollection.getEntry(notifEvent.key));
+        clearInvocations(mCollectionListener);
+
+        // WHEN registering a future dismissal, nothing happens right away
+        final Runnable onDismiss = mCollection.registerFutureDismissal(entry, REASON_CLICK,
+                NotifCollectionTest::defaultStats);
+        verifyNoMoreInteractions(mCollectionListener);
+
+        // WHEN finally dismissing
+        onDismiss.run();
+        verify(mStatusBarService).onNotificationClear(any(), anyInt(), eq(notifEvent.key),
+                anyInt(), anyInt(), any());
+        verifyNoMoreInteractions(mStatusBarService);
+        verifyNoMoreInteractions(mCollectionListener);
+    }
+
+    @Test
+    public void testRegisterFutureDismissalWithRetractionAndRepost() {
+        // GIVEN a pipeline with one notification
+        NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
+        NotificationEntry entry = requireNonNull(mCollection.getEntry(notifEvent.key));
+        clearInvocations(mCollectionListener);
+
+        // WHEN registering a future dismissal, nothing happens right away
+        final Runnable onDismiss = mCollection.registerFutureDismissal(entry, REASON_CLICK,
+                NotifCollectionTest::defaultStats);
+        verifyNoMoreInteractions(mCollectionListener);
+
+        // WHEN retracting the notification, and then reposting
+        mNoMan.retractNotif(notifEvent.sbn, REASON_CLICK);
+        mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
+        clearInvocations(mCollectionListener);
+
+        // KNOWING that the entry in the collection is different now
+        assertThat(mCollection.getEntry(notifEvent.key)).isNotSameInstanceAs(entry);
+
+        // WHEN finally dismissing
+        onDismiss.run();
+
+        // VERIFY that nothing happens; the notification should not be removed
+        verifyNoMoreInteractions(mCollectionListener);
+        assertThat(mCollection.getEntry(notifEvent.key)).isNotNull();
+        verifyNoMoreInteractions(mStatusBarService);
+    }
+
+    @Test
     public void testCannotDismissOngoingNotificationChildren() {
         // GIVEN an ongoing notification
         final NotificationEntry container = new NotificationEntryBuilder()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 9ea1813..4e7e79f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -1528,6 +1528,34 @@
     }
 
     @Test
+    public void testContiguousSections() {
+        mListBuilder.setSectioners(List.of(
+                new PackageSectioner("pkg", 1),
+                new PackageSectioner("pkg", 1),
+                new PackageSectioner("pkg", 3),
+                new PackageSectioner("pkg", 2)
+        ));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testNonContiguousSections() {
+        mListBuilder.setSectioners(List.of(
+                new PackageSectioner("pkg", 1),
+                new PackageSectioner("pkg", 1),
+                new PackageSectioner("pkg", 3),
+                new PackageSectioner("pkg", 1)
+        ));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testBucketZeroNotAllowed() {
+        mListBuilder.setSectioners(List.of(
+                new PackageSectioner("pkg", 0),
+                new PackageSectioner("pkg", 1)
+        ));
+    }
+
+    @Test
     public void testStabilizeGroupsDelayedSummaryRendersAllNotifsTopLevel() {
         // GIVEN group children posted without a summary
         addGroupChild(0, PACKAGE_1, GROUP_1);
@@ -2189,7 +2217,11 @@
         }
 
         PackageSectioner(String pkg) {
-            super("PackageSection_" + pkg, 0);
+            this(pkg, 0);
+        }
+
+        PackageSectioner(String pkg, int bucket) {
+            super("PackageSection_" + pkg, bucket);
             mPackages = List.of(pkg);
             mComparator = null;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinatorTest.kt
deleted file mode 100644
index c6c043a..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinatorTest.kt
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.collection.coordinator
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-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.notifcollection.NotifLifetimeExtender
-import com.android.systemui.statusbar.phone.NotifActivityLaunchEvents
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.withArgCaptor
-import dagger.BindsInstance
-import dagger.Component
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Test
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-
-@SmallTest
-class ActivityLaunchAnimCoordinatorTest : SysuiTestCase() {
-
-    val activityLaunchEvents: NotifActivityLaunchEvents = mock()
-    val pipeline: NotifPipeline = mock()
-
-    val coordinator: ActivityLaunchAnimCoordinator =
-            DaggerTestActivityStarterCoordinatorComponent
-                    .factory()
-                    .create(activityLaunchEvents)
-                    .coordinator
-
-    @Test
-    fun testNoLifetimeExtensionIfNoAssociatedActivityLaunch() {
-        coordinator.attach(pipeline)
-        val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> {
-            verify(pipeline).addNotificationLifetimeExtender(capture())
-        }
-        val fakeEntry = mock<NotificationEntry>().also {
-            whenever(it.key).thenReturn("0")
-        }
-        assertFalse(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0))
-    }
-
-    @Test
-    fun testNoLifetimeExtensionIfAssociatedActivityLaunchAlreadyEnded() {
-        coordinator.attach(pipeline)
-        val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> {
-            verify(pipeline).addNotificationLifetimeExtender(capture())
-        }
-        val eventListener = withArgCaptor<NotifActivityLaunchEvents.Listener> {
-            verify(activityLaunchEvents).registerListener(capture())
-        }
-        val fakeEntry = mock<NotificationEntry>().also {
-            whenever(it.key).thenReturn("0")
-        }
-        eventListener.onStartLaunchNotifActivity(fakeEntry)
-        eventListener.onFinishLaunchNotifActivity(fakeEntry)
-        assertFalse(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0))
-    }
-
-    @Test
-    fun testLifetimeExtensionWhileActivityLaunchInProgress() {
-        coordinator.attach(pipeline)
-        val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> {
-            verify(pipeline).addNotificationLifetimeExtender(capture())
-        }
-        val eventListener = withArgCaptor<NotifActivityLaunchEvents.Listener> {
-            verify(activityLaunchEvents).registerListener(capture())
-        }
-        val onEndLifetimeExtensionCallback =
-                mock<NotifLifetimeExtender.OnEndLifetimeExtensionCallback>()
-        lifetimeExtender.setCallback(onEndLifetimeExtensionCallback)
-
-        val fakeEntry = mock<NotificationEntry>().also {
-            whenever(it.key).thenReturn("0")
-        }
-        eventListener.onStartLaunchNotifActivity(fakeEntry)
-        assertTrue(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0))
-
-        eventListener.onFinishLaunchNotifActivity(fakeEntry)
-        verify(onEndLifetimeExtensionCallback).onEndLifetimeExtension(lifetimeExtender, fakeEntry)
-    }
-
-    @Test
-    fun testCancelLifetimeExtensionDoesNotInvokeCallback() {
-        coordinator.attach(pipeline)
-        val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> {
-            verify(pipeline).addNotificationLifetimeExtender(capture())
-        }
-        val eventListener = withArgCaptor<NotifActivityLaunchEvents.Listener> {
-            verify(activityLaunchEvents).registerListener(capture())
-        }
-        val onEndLifetimeExtensionCallback =
-                mock<NotifLifetimeExtender.OnEndLifetimeExtensionCallback>()
-        lifetimeExtender.setCallback(onEndLifetimeExtensionCallback)
-
-        val fakeEntry = mock<NotificationEntry>().also {
-            whenever(it.key).thenReturn("0")
-        }
-        eventListener.onStartLaunchNotifActivity(fakeEntry)
-        assertTrue(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0))
-
-        lifetimeExtender.cancelLifetimeExtension(fakeEntry)
-        eventListener.onFinishLaunchNotifActivity(fakeEntry)
-        verify(onEndLifetimeExtensionCallback, never())
-                .onEndLifetimeExtension(lifetimeExtender, fakeEntry)
-    }
-}
-
-@CoordinatorScope
-@Component(modules = [ActivityLaunchAnimCoordinatorModule::class])
-interface TestActivityStarterCoordinatorComponent {
-    val coordinator: ActivityLaunchAnimCoordinator
-
-    @Component.Factory
-    interface Factory {
-        fun create(
-            @BindsInstance activityLaunchEvents: NotifActivityLaunchEvents
-        ): TestActivityStarterCoordinatorComponent
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
index f4d8405..5c2f5fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
@@ -23,7 +23,6 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -33,7 +32,6 @@
 import android.app.NotificationManager;
 import android.testing.AndroidTestingRunner;
 
-import androidx.annotation.Nullable;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -41,7 +39,6 @@
 import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.SbnBuilder;
 import com.android.systemui.statusbar.notification.SectionClassifier;
-import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -49,7 +46,6 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
 import com.android.systemui.statusbar.notification.collection.render.NodeController;
-import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -72,7 +68,6 @@
     @Mock private NotifPipeline mNotifPipeline;
     @Mock private NodeController mAlertingHeaderController;
     @Mock private NodeController mSilentNodeController;
-    @Mock private SectionHeaderController mSilentHeaderController;
 
     @Captor private ArgumentCaptor<NotifFilter> mNotifFilterCaptor;
 
@@ -94,7 +89,6 @@
                 mHighPriorityProvider,
                 mSectionClassifier,
                 mAlertingHeaderController,
-                mSilentHeaderController,
                 mSilentNodeController);
         mEntry = spy(new NotificationEntryBuilder().build());
         mEntry.setRanking(getRankingForUnfilteredNotif().build());
@@ -112,25 +106,6 @@
     }
 
     @Test
-    public void testSilentHeaderClearableChildrenUpdate() {
-        ListEntry listEntry = new ListEntry(mEntry.getKey(), 0L) {
-            @Nullable
-            @Override
-            public NotificationEntry getRepresentativeEntry() {
-                return mEntry;
-            }
-        };
-        setRankingAmbient(false);
-        setSbnClearable(true);
-        mSilentSectioner.onEntriesUpdated(Arrays.asList(listEntry));
-        verify(mSilentHeaderController).setClearSectionEnabled(eq(true));
-
-        setSbnClearable(false);
-        mSilentSectioner.onEntriesUpdated(Arrays.asList(listEntry));
-        verify(mSilentHeaderController).setClearSectionEnabled(eq(false));
-    }
-
-    @Test
     public void testUnfilteredState() {
         // GIVEN no suppressed visual effects + app not suspended
         mEntry.setRanking(getRankingForUnfilteredNotif().build());
@@ -225,46 +200,6 @@
         assertInSection(mEntry, mSilentSectioner);
     }
 
-    @Test
-    public void testClearableSilentSection() {
-        when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
-        setSbnClearable(true);
-        setRankingAmbient(false);
-        mSilentSectioner.onEntriesUpdated(Arrays.asList(mEntry));
-        verify(mSilentHeaderController).setClearSectionEnabled(eq(true));
-    }
-
-    @Test
-    public void testClearableMinimizedSection() {
-        when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
-        setSbnClearable(true);
-        setRankingAmbient(true);
-        mMinimizedSectioner.onEntriesUpdated(Arrays.asList(mEntry));
-        verify(mSilentHeaderController).setClearSectionEnabled(eq(true));
-    }
-
-    @Test
-    public void testNotClearableSilentSection() {
-        setSbnClearable(false);
-        when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
-        setRankingAmbient(false);
-        mSilentSectioner.onEntriesUpdated(Arrays.asList(mEntry));
-        mMinimizedSectioner.onEntriesUpdated(Arrays.asList(mEntry));
-        mAlertingSectioner.onEntriesUpdated(Arrays.asList(mEntry));
-        verify(mSilentHeaderController, times(2)).setClearSectionEnabled(eq(false));
-    }
-
-    @Test
-    public void testNotClearableMinimizedSection() {
-        setSbnClearable(false);
-        when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
-        setRankingAmbient(true);
-        mSilentSectioner.onEntriesUpdated(Arrays.asList(mEntry));
-        mMinimizedSectioner.onEntriesUpdated(Arrays.asList(mEntry));
-        mAlertingSectioner.onEntriesUpdated(Arrays.asList(mEntry));
-        verify(mSilentHeaderController, times(2)).setClearSectionEnabled(eq(false));
-    }
-
     private void assertInSection(NotificationEntry entry, NotifSectioner section) {
         for (NotifSectioner current: mSections) {
             if (current == section) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
deleted file mode 100644
index a2d8e3d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.collection.coordinator
-
-import android.os.UserHandle
-import android.service.notification.StatusBarNotification
-import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
-import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.notification.DynamicPrivacyController
-import com.android.systemui.statusbar.notification.collection.ListEntry
-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.Invalidator
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.withArgCaptor
-import dagger.BindsInstance
-import dagger.Component
-import org.junit.Test
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
-
-@SmallTest
-class SensitiveContentCoordinatorTest : SysuiTestCase() {
-
-    val dynamicPrivacyController: DynamicPrivacyController = mock()
-    val lockscreenUserManager: NotificationLockscreenUserManager = mock()
-    val pipeline: NotifPipeline = mock()
-    val keyguardUpdateMonitor: KeyguardUpdateMonitor = mock()
-    val statusBarStateController: StatusBarStateController = mock()
-    val keyguardStateController: KeyguardStateController = mock()
-
-    val coordinator: SensitiveContentCoordinator =
-        DaggerTestSensitiveContentCoordinatorComponent
-                .factory()
-                .create(
-                        dynamicPrivacyController,
-                        lockscreenUserManager,
-                        keyguardUpdateMonitor,
-                        statusBarStateController,
-                        keyguardStateController)
-                .coordinator
-
-    @Test
-    fun onDynamicPrivacyChanged_invokeInvalidationListener() {
-        coordinator.attach(pipeline)
-        val invalidator = withArgCaptor<Invalidator> {
-            verify(pipeline).addPreRenderInvalidator(capture())
-        }
-        val dynamicPrivacyListener = withArgCaptor<DynamicPrivacyController.Listener> {
-            verify(dynamicPrivacyController).addListener(capture())
-        }
-
-        val invalidationListener = mock<Pluggable.PluggableListener<Invalidator>>()
-        invalidator.setInvalidationListener(invalidationListener)
-
-        dynamicPrivacyListener.onDynamicPrivacyChanged()
-
-        verify(invalidationListener).onPluggableInvalidated(invalidator)
-    }
-
-    @Test
-    fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction() {
-        coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
-
-        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
-        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
-        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
-        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
-        val entry = fakeNotification(1, false)
-
-        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
-
-        verify(entry.representativeEntry!!).setSensitive(false, false)
-    }
-
-    @Test
-    fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction() {
-        coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
-
-        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
-        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
-        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
-        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
-        val entry = fakeNotification(1, true)
-
-        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
-
-        verify(entry.representativeEntry!!).setSensitive(false, false)
-    }
-
-    @Test
-    fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs() {
-        coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
-
-        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
-        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
-        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
-        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
-        val entry = fakeNotification(1, false)
-
-        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
-
-        verify(entry.representativeEntry!!).setSensitive(false, false)
-    }
-
-    @Test
-    fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction() {
-        coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
-
-        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
-        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
-        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
-        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
-        val entry = fakeNotification(1, false)
-
-        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
-
-        verify(entry.representativeEntry!!).setSensitive(false, true)
-    }
-
-    @Test
-    fun onBeforeRenderList_deviceLocked_notifNeedsRedaction() {
-        coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
-
-        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
-        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
-        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
-        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
-        val entry = fakeNotification(1, true)
-
-        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
-
-        verify(entry.representativeEntry!!).setSensitive(true, true)
-    }
-
-    @Test
-    fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction() {
-        coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
-
-        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
-        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
-        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
-        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
-        val entry = fakeNotification(1, true)
-
-        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
-
-        verify(entry.representativeEntry!!).setSensitive(false, true)
-    }
-
-    @Test
-    fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge() {
-        coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
-
-        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
-        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
-        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
-        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
-        whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true)
-
-        val entry = fakeNotification(2, true)
-
-        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
-
-        verify(entry.representativeEntry!!).setSensitive(true, true)
-    }
-
-    @Test
-    fun onBeforeRenderList_deviceDynamicallyUnlocked_deviceBiometricBypassingLockScreen() {
-        coordinator.attach(pipeline)
-        val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
-            verify(pipeline).addOnBeforeRenderListListener(capture())
-        }
-
-        whenever(lockscreenUserManager.currentUserId).thenReturn(1)
-        whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
-        whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
-        whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
-        whenever(statusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD)
-        whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(any()))
-                .thenReturn(true)
-
-        val entry = fakeNotification(2, true)
-
-        onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
-
-        verify(entry.representativeEntry!!, never()).setSensitive(any(), any())
-    }
-
-    private fun fakeNotification(notifUserId: Int, needsRedaction: Boolean): ListEntry {
-        val mockUserHandle = mock<UserHandle>().apply {
-            whenever(identifier).thenReturn(notifUserId)
-        }
-        val mockSbn: StatusBarNotification = mock<StatusBarNotification>().apply {
-            whenever(user).thenReturn(mockUserHandle)
-        }
-        val mockEntry = mock<NotificationEntry>().apply {
-            whenever(sbn).thenReturn(mockSbn)
-        }
-        whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction)
-        whenever(mockEntry.rowExists()).thenReturn(true)
-        return object : ListEntry("key", 0) {
-            override fun getRepresentativeEntry(): NotificationEntry = mockEntry
-        }
-    }
-}
-
-@CoordinatorScope
-@Component(modules = [SensitiveContentCoordinatorModule::class])
-interface TestSensitiveContentCoordinatorComponent {
-    val coordinator: SensitiveContentCoordinator
-
-    @Component.Factory
-    interface Factory {
-        fun create(
-            @BindsInstance dynamicPrivacyController: DynamicPrivacyController,
-            @BindsInstance lockscreenUserManager: NotificationLockscreenUserManager,
-            @BindsInstance keyguardUpdateMonitor: KeyguardUpdateMonitor,
-            @BindsInstance statusBarStateController: StatusBarStateController,
-            @BindsInstance keyguardStateController: KeyguardStateController
-        ): TestSensitiveContentCoordinatorComponent
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
index 70266e4..cf2fc7c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.statusbar.notification.collection.coordinator
 
+import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
@@ -43,6 +44,11 @@
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper
 class StackCoordinatorTest : SysuiTestCase() {
+
+    companion object {
+        const val NOTIF_USER_ID = 0
+    }
+
     private lateinit var coordinator: StackCoordinator
     private lateinit var afterRenderListListener: OnAfterRenderListListener
 
@@ -61,7 +67,10 @@
         afterRenderListListener = withArgCaptor {
             verify(pipeline).addOnAfterRenderListListener(capture())
         }
-        entry = NotificationEntryBuilder().setSection(section).build()
+        entry = NotificationEntryBuilder()
+                .setSection(section)
+                .setUser(UserHandle.of(NOTIF_USER_ID))
+                .build()
     }
 
     @Test
@@ -74,13 +83,31 @@
     fun testSetNotificationStats_clearableAlerting() {
         whenever(section.bucket).thenReturn(BUCKET_ALERTING)
         afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
-        verify(stackController).setNotifStats(NotifStats(1, false, true, false, false))
+        verify(stackController)
+                .setNotifStats(
+                        NotifStats(
+                                1,
+                                false,
+                                true,
+                                false,
+                                false,
+                                setOf(NOTIF_USER_ID),
+                                emptySet()))
     }
 
     @Test
     fun testSetNotificationStats_clearableSilent() {
         whenever(section.bucket).thenReturn(BUCKET_SILENT)
         afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
-        verify(stackController).setNotifStats(NotifStats(1, false, false, false, true))
+        verify(stackController)
+                .setNotifStats(
+                        NotifStats(
+                                1,
+                                false,
+                                false,
+                                false,
+                                true,
+                                emptySet(),
+                                setOf(NOTIF_USER_ID)))
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
index b63e66f..c7f7ec2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification.icon;
+package com.android.systemui.statusbar.notification.icon
 
-import android.app.ActivityManager;
-import android.app.Notification;
+import android.app.ActivityManager
+import android.app.Notification
 import android.app.NotificationChannel
 import android.app.NotificationManager.IMPORTANCE_DEFAULT
 import android.app.Person
@@ -27,11 +27,12 @@
 import android.graphics.drawable.Icon
 import android.os.SystemClock
 import android.os.UserHandle
-import android.testing.AndroidTestingRunner;
+import android.testing.AndroidTestingRunner
 import androidx.test.InstrumentationRegistry
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapperTest.Companion.any
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
@@ -40,7 +41,7 @@
 import org.junit.Before
 import org.junit.Test
 
-import org.junit.runner.RunWith;
+import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.anyInt
@@ -48,15 +49,14 @@
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
-class IconManagerTest: SysuiTestCase() {
+class IconManagerTest : SysuiTestCase() {
     companion object {
-        private const val TEST_PACKAGE_NAME = "test";
-        private const val TEST_UID = 0;
+        private const val TEST_PACKAGE_NAME = "test"
+        private const val TEST_UID = 0
     }
 
-
     private var id = 0
-    private val context = InstrumentationRegistry.getTargetContext();
+    private val context = InstrumentationRegistry.getTargetContext()
     @Mock private lateinit var shortcut: ShortcutInfo
     @Mock private lateinit var shortcutIc: Icon
     @Mock private lateinit var messageIc: Icon
@@ -65,6 +65,7 @@
     @Mock private lateinit var drawable: Drawable
     @Mock private lateinit var row: ExpandableNotificationRow
 
+    @Mock private lateinit var notifLockscreenUserManager: NotificationLockscreenUserManager
     @Mock private lateinit var notifCollection: CommonNotifCollection
     @Mock private lateinit var launcherApps: LauncherApps
 
@@ -83,13 +84,16 @@
 
         `when`(shortcut.icon).thenReturn(shortcutIc)
         `when`(launcherApps.getShortcutIcon(shortcut)).thenReturn(shortcutIc)
+        `when`(notifLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic(TEST_UID))
+                .thenReturn(true)
 
-        iconManager = IconManager(notifCollection, launcherApps, iconBuilder)
+        iconManager =
+                IconManager(notifCollection, launcherApps, iconBuilder, notifLockscreenUserManager)
     }
 
     @Test
     fun testCreateIcons_importantConversation_shortcutIcon() {
-        val entry = notificationEntry(true, true, true)
+        val entry = notificationEntry()
         entry?.channel?.isImportantConversation = true
         entry?.let {
             iconManager.createIcons(it)
@@ -99,7 +103,7 @@
 
     @Test
     fun testCreateIcons_importantConversation_messageIcon() {
-        val entry = notificationEntry(false, true, true)
+        val entry = notificationEntry(hasShortcut = false)
         entry?.channel?.isImportantConversation = true
         entry?.let {
             iconManager.createIcons(it)
@@ -109,7 +113,7 @@
 
     @Test
     fun testCreateIcons_importantConversation_largeIcon() {
-        val entry = notificationEntry(false, false, true)
+        val entry = notificationEntry(hasShortcut = false, hasMessage = false)
         entry?.channel?.isImportantConversation = true
         entry?.let {
             iconManager.createIcons(it)
@@ -119,7 +123,7 @@
 
     @Test
     fun testCreateIcons_importantConversation_smallIcon() {
-        val entry = notificationEntry(false, false, false)
+        val entry = notificationEntry(hasShortcut = false, hasMessage = false, hasLargeIcon = false)
         entry?.channel?.isImportantConversation = true
         entry?.let {
             iconManager.createIcons(it)
@@ -129,7 +133,7 @@
 
     @Test
     fun testCreateIcons_notImportantConversation() {
-        val entry = notificationEntry(true, true, true)
+        val entry = notificationEntry()
         entry?.let {
             iconManager.createIcons(it)
         }
@@ -138,8 +142,10 @@
 
     @Test
     fun testCreateIcons_sensitiveImportantConversation() {
-        val entry = notificationEntry(true, false, false)
-        entry?.setSensitive(true, true);
+        val entry = notificationEntry(
+                hasMessage = false,
+                hasLargeIcon = false,
+                hasSensitiveContent = true)
         entry?.channel?.isImportantConversation = true
         entry?.let {
             iconManager.createIcons(it)
@@ -151,14 +157,17 @@
 
     @Test
     fun testUpdateIcons_sensitivityChange() {
-        val entry = notificationEntry(true, false, false)
+        val entry = notificationEntry(
+                hasMessage = false,
+                hasLargeIcon = false,
+                hasSensitiveContent = true)
         entry?.channel?.isImportantConversation = true
-        entry?.setSensitive(true, true);
         entry?.let {
             iconManager.createIcons(it)
         }
         assertEquals(entry?.icons?.aodIcon?.sourceIcon, smallIc)
-        entry?.setSensitive(false, false);
+        `when`(notifLockscreenUserManager.sensitiveNotifsNeedRedactionInPublic(TEST_UID))
+                .thenReturn(false)
         entry?.let {
             iconManager.updateIcons(it)
         }
@@ -166,14 +175,19 @@
     }
 
     private fun notificationEntry(
-            hasShortcut: Boolean,
-            hasMessage: Boolean,
-            hasLargeIcon: Boolean
+        hasShortcut: Boolean = true,
+        hasMessage: Boolean = true,
+        hasLargeIcon: Boolean = true,
+        hasSensitiveContent: Boolean = false
     ): NotificationEntry? {
         val n = Notification.Builder(mContext, "id")
                 .setSmallIcon(smallIc)
                 .setContentTitle("Title")
                 .setContentText("Text")
+                .setVisibility(
+                        if (hasSensitiveContent)
+                            Notification.VISIBILITY_PRIVATE
+                        else Notification.VISIBILITY_PUBLIC)
 
         if (hasMessage) {
             n.style = Notification.MessagingStyle("")
@@ -203,7 +217,6 @@
 
         val entry = builder.build()
         entry.row = row
-        entry.setSensitive(false, true);
         return entry
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 0f2e9bc..f5a0e2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -91,7 +91,7 @@
 
     @Test
     public void testGroupSummaryNotShowingIconWhenPublic() {
-        mGroupRow.setSensitive(true, true);
+        mGroupRow.setSensitive(true);
         mGroupRow.setHideSensitiveForIntrinsicHeight(true);
         assertTrue(mGroupRow.isSummaryWithChildren());
         assertFalse(mGroupRow.isShowingIcon());
@@ -99,7 +99,7 @@
 
     @Test
     public void testNotificationHeaderVisibleWhenAnimating() {
-        mGroupRow.setSensitive(true, true);
+        mGroupRow.setSensitive(true);
         mGroupRow.setHideSensitive(true, false, 0, 0);
         mGroupRow.setHideSensitive(false, true, 0, 0);
         assertEquals(View.VISIBLE, mGroupRow.getChildrenContainer().getVisibleWrapper()
@@ -130,7 +130,7 @@
     public void testIconColorShouldBeUpdatedWhenSensitive() throws Exception {
         ExpandableNotificationRow row = spy(mNotificationTestHelper.createRow(
                 FLAG_CONTENT_VIEW_ALL));
-        row.setSensitive(true, true);
+        row.setSensitive(true);
         row.setHideSensitive(true, false, 0, 0);
         verify(row).updateShelfIconColor();
     }
@@ -214,7 +214,7 @@
     @Test
     public void testFeedback_noHeader() {
         // public notification is custom layout - no header
-        mGroupRow.setSensitive(true, true);
+        mGroupRow.setSensitive(true);
         mGroupRow.setOnFeedbackClickListener(null);
         mGroupRow.setFeedbackIcon(null);
     }
@@ -322,11 +322,22 @@
         ExpandableNotificationRow row =
                 mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification());
         row.getEntry().getChannel().setImportanceLockedByCriticalDeviceFunction(true);
+        row.getEntry().getChannel().setBlockable(false);
 
         assertTrue(row.getIsNonblockable());
     }
 
     @Test
+    public void testGetIsNonblockable_criticalDeviceFunction_butBlockable() throws Exception {
+        ExpandableNotificationRow row =
+                mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification());
+        row.getEntry().getChannel().setImportanceLockedByCriticalDeviceFunction(true);
+        row.getEntry().getChannel().setBlockable(true);
+
+        assertFalse(row.getIsNonblockable());
+    }
+
+    @Test
     public void testCanDismissNoClear() throws Exception {
         ExpandableNotificationRow row =
                 mNotificationTestHelper.createRow(mNotificationTestHelper.createNotification());
@@ -335,7 +346,7 @@
                 .build();
         row.performDismiss(false);
         verify(mNotificationTestHelper.mOnUserInteractionCallback)
-                .onDismiss(any(), anyInt(), any());
+                .registerFutureDismissal(any(), anyInt());
     }
 
     @Test
@@ -347,6 +358,6 @@
                 .build();
         row.performDismiss(false);
         verify(mNotificationTestHelper.mOnUserInteractionCallback, never())
-                .onDismiss(any(), anyInt(), any());
+                .registerFutureDismissal(any(), anyInt());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
index 251ac7d..a855705 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
@@ -78,7 +78,6 @@
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
 import com.android.systemui.statusbar.notification.icon.IconBuilder;
 import com.android.systemui.statusbar.notification.icon.IconManager;
-import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
 import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent;
@@ -132,7 +131,6 @@
     @Mock private NotificationEntryListener mEntryListener;
     @Mock private NotificationRowBinderImpl.BindRowCallback mBindCallback;
     @Mock private HeadsUpManager mHeadsUpManager;
-    @Mock private NotificationInterruptStateProvider mNotificationInterruptionStateProvider;
     @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
     @Mock private NotificationGutsManager mGutsManager;
     @Mock private NotificationRemoteInputManager mRemoteInputManager;
@@ -254,6 +252,7 @@
                 .thenAnswer((Answer<ExpandableNotificationRowController>) invocation ->
                         new ExpandableNotificationRowController(
                                 viewCaptor.getValue(),
+                                mLockscreenUserManager,
                                 mock(ActivatableNotificationViewController.class),
                                 mock(RemoteInputViewSubcomponent.Factory.class),
                                 mock(MetricsLogger.class),
@@ -300,7 +299,8 @@
                 new IconManager(
                         mEntryManager,
                         mock(LauncherApps.class),
-                        new IconBuilder(mContext)),
+                        new IconBuilder(mContext),
+                        mLockscreenUserManager),
                 mock(LowPriorityInflationHelper.class),
                 mNotifPipelineFlags);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 1ecb09b..7a8b329 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -23,8 +23,11 @@
 import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking;
 
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.annotation.Nullable;
 import android.app.ActivityManager;
@@ -52,6 +55,7 @@
 import com.android.systemui.media.MediaFeatureFlag;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -120,6 +124,7 @@
     private StatusBarStateController mStatusBarStateController;
     private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;
     public final OnUserInteractionCallback mOnUserInteractionCallback;
+    public final Runnable mFutureDismissalRunnable;
 
     public NotificationTestHelper(
             Context context,
@@ -152,7 +157,8 @@
         mIconManager = new IconManager(
                 mock(CommonNotifCollection.class),
                 mock(LauncherApps.class),
-                new IconBuilder(mContext));
+                new IconBuilder(mContext),
+                mock(NotificationLockscreenUserManager.class));
 
         NotificationContentInflater contentBinder = new NotificationContentInflater(
                 mock(NotifRemoteViewCache.class),
@@ -180,6 +186,9 @@
         mBindPipelineEntryListener = collectionListenerCaptor.getValue();
         mPeopleNotificationIdentifier = mock(PeopleNotificationIdentifier.class);
         mOnUserInteractionCallback = mock(OnUserInteractionCallback.class);
+        mFutureDismissalRunnable = mock(Runnable.class);
+        when(mOnUserInteractionCallback.registerFutureDismissal(any(), anyInt()))
+                .thenReturn(mFutureDismissalRunnable);
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index 4270d72..3f19036 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -5,8 +5,9 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.NotificationShelf
-import junit.framework.Assert.assertFalse
-import junit.framework.Assert.assertTrue
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.notification.row.ExpandableView
+import junit.framework.Assert.*
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -143,6 +144,113 @@
         assertFalse(isYBelowShelfInView)
     }
 
+    @Test
+    fun getAmountInShelf_lastViewBelowShelf_completelyInShelf() {
+        val shelfClipStart = 0f
+        val viewStart = 1f
+
+        val expandableView = mock(ExpandableView::class.java)
+        whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
+        whenever(expandableView.translationY).thenReturn(viewStart)
+        whenever(expandableView.actualHeight).thenReturn(20)
+
+        whenever(expandableView.minHeight).thenReturn(20)
+        whenever(expandableView.shelfTransformationTarget).thenReturn(null)  // use translationY
+        whenever(expandableView.isInShelf).thenReturn(true)
+
+        whenever(ambientState.isOnKeyguard).thenReturn(true)
+        whenever(ambientState.isExpansionChanging).thenReturn(false)
+        whenever(ambientState.isShadeExpanded).thenReturn(true)
+
+        val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
+                /* view= */ expandableView,
+                /* scrollingFast= */ false,
+                /* expandingAnimated= */ false,
+                /* isLastChild= */ true,
+                shelfClipStart)
+        assertEquals(1f, amountInShelf)
+    }
+
+    @Test
+    fun getAmountInShelf_lastViewAlmostBelowShelf_completelyInShelf() {
+        val viewStart = 0f
+        val shelfClipStart = 0.001f
+
+        val expandableView = mock(ExpandableView::class.java)
+        whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
+        whenever(expandableView.translationY).thenReturn(viewStart)
+        whenever(expandableView.actualHeight).thenReturn(20)
+
+        whenever(expandableView.minHeight).thenReturn(20)
+        whenever(expandableView.shelfTransformationTarget).thenReturn(null)  // use translationY
+        whenever(expandableView.isInShelf).thenReturn(true)
+
+        whenever(ambientState.isOnKeyguard).thenReturn(true)
+        whenever(ambientState.isExpansionChanging).thenReturn(false)
+        whenever(ambientState.isShadeExpanded).thenReturn(true)
+
+        val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
+                /* view= */ expandableView,
+                /* scrollingFast= */ false,
+                /* expandingAnimated= */ false,
+                /* isLastChild= */ true,
+                shelfClipStart)
+        assertEquals(1f, amountInShelf)
+    }
+
+    @Test
+    fun getAmountInShelf_lastViewHalfClippedByShelf_halfInShelf() {
+        val viewStart = 0f
+        val shelfClipStart = 10f
+
+        val expandableView = mock(ExpandableView::class.java)
+        whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
+        whenever(expandableView.translationY).thenReturn(viewStart)
+        whenever(expandableView.actualHeight).thenReturn(25)
+
+        whenever(expandableView.minHeight).thenReturn(25)
+        whenever(expandableView.shelfTransformationTarget).thenReturn(null)  // use translationY
+        whenever(expandableView.isInShelf).thenReturn(true)
+
+        whenever(ambientState.isOnKeyguard).thenReturn(true)
+        whenever(ambientState.isExpansionChanging).thenReturn(false)
+        whenever(ambientState.isShadeExpanded).thenReturn(true)
+
+        val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
+                /* view= */ expandableView,
+                /* scrollingFast= */ false,
+                /* expandingAnimated= */ false,
+                /* isLastChild= */ true,
+                shelfClipStart)
+        assertEquals(0.5f, amountInShelf)
+    }
+
+    @Test
+    fun getAmountInShelf_lastViewAboveShelf_notInShelf() {
+        val viewStart = 0f
+        val shelfClipStart = 15f
+
+        val expandableView = mock(ExpandableView::class.java)
+        whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
+        whenever(expandableView.translationY).thenReturn(viewStart)
+        whenever(expandableView.actualHeight).thenReturn(10)
+
+        whenever(expandableView.minHeight).thenReturn(10)
+        whenever(expandableView.shelfTransformationTarget).thenReturn(null)  // use translationY
+        whenever(expandableView.isInShelf).thenReturn(false)
+
+        whenever(ambientState.isExpansionChanging).thenReturn(false)
+        whenever(ambientState.isOnKeyguard).thenReturn(true)
+
+        val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
+                /* view= */ expandableView,
+                /* scrollingFast= */ false,
+                /* expandingAnimated= */ false,
+                /* isLastChild= */ true,
+                shelfClipStart)
+        assertEquals(0f, amountInShelf)
+    }
+
     private fun setFractionToShade(fraction: Float) {
         whenever(ambientState.fractionToShade).thenReturn(fraction)
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index e8608fa..63e0f53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -31,6 +31,8 @@
 import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
@@ -612,6 +614,12 @@
         assertTrue(mStackScroller.isInsideQsHeader(event2));
     }
 
+    @Test
+    public void setFractionToShade_recomputesStackHeight() {
+        mStackScroller.setFractionToShade(1f);
+        verify(mNotificationStackSizeCalculator).computeHeight(any(), anyInt(), anyFloat());
+    }
+
     private void setBarStateForTest(int state) {
         // Can't inject this through the listener or we end up on the actual implementation
         // rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
index ed22cd3..169c04c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
@@ -36,6 +36,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.HeadsUpStatusBarView;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
@@ -57,9 +58,12 @@
 
     private final NotificationStackScrollLayoutController mStackScrollerController =
             mock(NotificationStackScrollLayoutController.class);
-    private final NotificationPanelViewController mPanelView =
+    private final NotificationPanelViewController mPanelViewController =
             mock(NotificationPanelViewController.class);
     private final DarkIconDispatcher mDarkIconDispatcher = mock(DarkIconDispatcher.class);
+    private final NotificationLockscreenUserManager mLockscreenUserManager =
+            mock(NotificationLockscreenUserManager.class);
+
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
     private ExpandableNotificationRow mFirst;
     private HeadsUpStatusBarView mHeadsUpStatusBarView;
@@ -93,12 +97,13 @@
                 mHeadsUpManager,
                 mStatusbarStateController,
                 mBypassController,
+                mLockscreenUserManager,
                 mWakeUpCoordinator,
                 mDarkIconDispatcher,
                 mKeyguardStateController,
                 mCommandQueue,
                 mStackScrollerController,
-                mPanelView,
+                mPanelViewController,
                 mHeadsUpStatusBarView,
                 new Clock(mContext, null),
                 Optional.of(mOperatorNameView));
@@ -175,12 +180,13 @@
                 mHeadsUpManager,
                 mStatusbarStateController,
                 mBypassController,
+                mLockscreenUserManager,
                 mWakeUpCoordinator,
                 mDarkIconDispatcher,
                 mKeyguardStateController,
                 mCommandQueue,
                 mStackScrollerController,
-                mPanelView,
+                mPanelViewController,
                 mHeadsUpStatusBarView,
                 new Clock(mContext, null),
                 Optional.empty());
@@ -193,15 +199,15 @@
     public void testDestroy() {
         reset(mHeadsUpManager);
         reset(mDarkIconDispatcher);
-        reset(mPanelView);
+        reset(mPanelViewController);
         reset(mStackScrollerController);
 
         mHeadsUpAppearanceController.onViewDetached();
 
         verify(mHeadsUpManager).removeListener(any());
         verify(mDarkIconDispatcher).removeDarkReceiver((DarkIconDispatcher.DarkReceiver) any());
-        verify(mPanelView).removeTrackingHeadsUpListener(any());
-        verify(mPanelView).setHeadsUpAppearanceController(isNull());
+        verify(mPanelViewController).removeTrackingHeadsUpListener(any());
+        verify(mPanelViewController).setHeadsUpAppearanceController(isNull());
         verify(mStackScrollerController).removeOnExpandedHeightChangedListener(any());
     }
 }
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 f43c2a1..9c02216 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
@@ -475,4 +475,19 @@
         mBouncer.setExpansion(bouncerHideAmount);
         verify(callback, never()).onExpansionChanged(bouncerHideAmount);
     }
+
+    @Test
+    public void testOnResumeCalledForFullscreenBouncerOnSecondShow() {
+        // GIVEN a security mode which requires fullscreen bouncer
+        when(mKeyguardSecurityModel.getSecurityMode(anyInt()))
+                .thenReturn(KeyguardSecurityModel.SecurityMode.SimPin);
+        mBouncer.show(true);
+
+        // WHEN a second call to show occurs, the bouncer will already by visible
+        reset(mKeyguardHostViewController);
+        mBouncer.show(true);
+
+        // THEN ensure the ViewController is told to resume
+        verify(mKeyguardHostViewController).onResume();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
index 356d002..7ebf750 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
@@ -1090,6 +1090,21 @@
     }
 
     @Test
+    public void testQsExpansionChangedToDefaultWhenRotatingFromOrToSplitShade() {
+        // to make sure shade is in expanded state
+        mNotificationPanelViewController.startWaitingForOpenPanelGesture();
+        assertThat(mNotificationPanelViewController.isQsExpanded()).isFalse();
+
+        // switch to split shade from portrait (default state)
+        enableSplitShade(/* enabled= */ true);
+        assertThat(mNotificationPanelViewController.isQsExpanded()).isTrue();
+
+        // switch to portrait from split shade
+        enableSplitShade(/* enabled= */ false);
+        assertThat(mNotificationPanelViewController.isQsExpanded()).isFalse();
+    }
+
+    @Test
     public void interceptTouchEvent_withinQs_shadeExpanded_startsQsTracking() {
         mNotificationPanelViewController.mQs = mQs;
         when(mQsFrame.getX()).thenReturn(0f);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index dce520c..5f8dda3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -1490,6 +1490,13 @@
         assertAlphaAfterExpansion(mNotificationsScrim, 0f, expansion);
     }
 
+    @Test
+    public void aodStateSetsFrontScrimToNotBlend() {
+        mScrimController.transitionTo(ScrimState.AOD);
+        Assert.assertFalse("Front scrim should not blend with main color",
+                mScrimInFront.shouldBlendWithMainColor());
+    }
+
     private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) {
         mScrimController.setRawPanelExpansionFraction(expansion);
         finishAnimationsImmediately();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index fa867e2..ecea14c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -85,7 +85,6 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.Mockito;
@@ -93,6 +92,7 @@
 import org.mockito.stubbing.Answer;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Optional;
 
 @SmallTest
@@ -141,12 +141,13 @@
     @Mock
     private OnUserInteractionCallback mOnUserInteractionCallback;
     @Mock
+    private Runnable mFutureDismissalRunnable;
+    @Mock
     private StatusBarNotificationActivityStarter mNotificationActivityStarter;
     @Mock
     private ActivityLaunchAnimator mActivityLaunchAnimator;
     @Mock
     private InteractionJankMonitor mJankMonitor;
-    private StatusBarNotificationActivityStarter.LaunchEventsEmitter mLaunchEventsEmitter;
     private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
     private NotificationTestHelper mNotificationTestHelper;
     private ExpandableNotificationRow mNotificationRow;
@@ -187,8 +188,8 @@
         when(mEntryManager.getVisibleNotifications()).thenReturn(mActiveNotifications);
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
         when(mNotifPipelineFlags.isNewPipelineEnabled()).thenReturn(false);
-        when(mOnUserInteractionCallback.getGroupSummaryToDismiss(mNotificationRow.getEntry()))
-                .thenReturn(null);
+        when(mOnUserInteractionCallback.registerFutureDismissal(eq(mNotificationRow.getEntry()),
+                anyInt())).thenReturn(mFutureDismissalRunnable);
         when(mVisibilityProvider.obtain(anyString(), anyBoolean()))
                 .thenAnswer(invocation -> NotificationVisibility.obtain(
                         invocation.getArgument(0), 0, 1, false));
@@ -203,7 +204,6 @@
                         NotificationListContainer.class),
                         headsUpManager,
                         mJankMonitor);
-        mLaunchEventsEmitter = new StatusBarNotificationActivityStarter.LaunchEventsEmitter();
         mNotificationActivityStarter =
                 new StatusBarNotificationActivityStarter(
                         getContext(),
@@ -239,8 +239,7 @@
                         mock(NotificationPresenter.class),
                         mock(NotificationPanelViewController.class),
                         mActivityLaunchAnimator,
-                        notificationAnimationProvider,
-                        mLaunchEventsEmitter
+                        notificationAnimationProvider
                 );
 
         // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg
@@ -264,16 +263,23 @@
     @Test
     public void testOnNotificationClicked_keyGuardShowing()
             throws PendingIntent.CanceledException, RemoteException {
+        // To get the order right, collect posted runnables and run them later
+        List<Runnable> runnables = new ArrayList<>();
+        doAnswer(answerVoid(r -> runnables.add((Runnable) r)))
+                .when(mHandler).post(any(Runnable.class));
         // Given
-        StatusBarNotification sbn = mNotificationRow.getEntry().getSbn();
-        sbn.getNotification().contentIntent = mContentIntent;
-        sbn.getNotification().flags |= Notification.FLAG_AUTO_CANCEL;
+        NotificationEntry entry = mNotificationRow.getEntry();
+        Notification notification = entry.getSbn().getNotification();
+        notification.contentIntent = mContentIntent;
+        notification.flags |= Notification.FLAG_AUTO_CANCEL;
 
         when(mKeyguardStateController.isShowing()).thenReturn(true);
         when(mCentralSurfaces.isOccluded()).thenReturn(true);
 
         // When
-        mNotificationActivityStarter.onNotificationClicked(sbn, mNotificationRow);
+        mNotificationActivityStarter.onNotificationClicked(entry, mNotificationRow);
+        // Run the collected runnables in fifo order, the way post() really does.
+        while (!runnables.isEmpty()) runnables.remove(0).run();
 
         // Then
         verify(mShadeController, atLeastOnce()).collapsePanel();
@@ -283,24 +289,27 @@
 
         verify(mAssistManager).hideAssist();
 
-        InOrder orderVerifier = Mockito.inOrder(mClickNotifier, mOnUserInteractionCallback);
-        orderVerifier.verify(mClickNotifier).onNotificationClick(
-                eq(sbn.getKey()), any(NotificationVisibility.class));
+        InOrder orderVerifier = Mockito.inOrder(mClickNotifier, mOnUserInteractionCallback,
+                mFutureDismissalRunnable);
         // Notification calls dismiss callback to remove notification due to FLAG_AUTO_CANCEL
-        orderVerifier.verify(mOnUserInteractionCallback).onDismiss(mNotificationRow.getEntry(),
-                REASON_CLICK, null);
+        orderVerifier.verify(mOnUserInteractionCallback)
+                .registerFutureDismissal(eq(entry), eq(REASON_CLICK));
+        orderVerifier.verify(mClickNotifier).onNotificationClick(
+                eq(entry.getKey()), any(NotificationVisibility.class));
+        orderVerifier.verify(mFutureDismissalRunnable).run();
     }
 
     @Test
     public void testOnNotificationClicked_bubble_noContentIntent_noKeyGuard()
             throws RemoteException {
-        StatusBarNotification sbn = mBubbleNotificationRow.getEntry().getSbn();
+        NotificationEntry entry = mBubbleNotificationRow.getEntry();
+        StatusBarNotification sbn = entry.getSbn();
 
         // Given
         sbn.getNotification().contentIntent = null;
 
         // When
-        mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow);
+        mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow);
 
         // Then
         verify(mBubblesManager).expandStackAndSelectBubble(eq(mBubbleNotificationRow.getEntry()));
@@ -311,20 +320,22 @@
         verify(mAssistManager).hideAssist();
 
         verify(mClickNotifier).onNotificationClick(
-                eq(sbn.getKey()), any(NotificationVisibility.class));
+                eq(entry.getKey()), any(NotificationVisibility.class));
 
         // The content intent should NOT be sent on click.
         verifyZeroInteractions(mContentIntent);
 
         // Notification should not be cancelled.
-        verify(mOnUserInteractionCallback, never()).onDismiss(eq(mNotificationRow.getEntry()),
-                anyInt(), eq(null));
+        verify(mOnUserInteractionCallback, never())
+                .registerFutureDismissal(eq(mNotificationRow.getEntry()), anyInt());
+        verify(mFutureDismissalRunnable, never()).run();
     }
 
     @Test
     public void testOnNotificationClicked_bubble_noContentIntent_keyGuardShowing()
             throws RemoteException {
-        StatusBarNotification sbn = mBubbleNotificationRow.getEntry().getSbn();
+        NotificationEntry entry = mBubbleNotificationRow.getEntry();
+        StatusBarNotification sbn = entry.getSbn();
 
         // Given
         sbn.getNotification().contentIntent = null;
@@ -332,7 +343,7 @@
         when(mCentralSurfaces.isOccluded()).thenReturn(true);
 
         // When
-        mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow);
+        mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow);
 
         // Then
         verify(mBubblesManager).expandStackAndSelectBubble(eq(mBubbleNotificationRow.getEntry()));
@@ -342,7 +353,7 @@
         verify(mAssistManager).hideAssist();
 
         verify(mClickNotifier).onNotificationClick(
-                eq(sbn.getKey()), any(NotificationVisibility.class));
+                eq(entry.getKey()), any(NotificationVisibility.class));
 
         // The content intent should NOT be sent on click.
         verifyZeroInteractions(mContentIntent);
@@ -354,7 +365,8 @@
     @Test
     public void testOnNotificationClicked_bubble_withContentIntent_keyGuardShowing()
             throws RemoteException {
-        StatusBarNotification sbn = mBubbleNotificationRow.getEntry().getSbn();
+        NotificationEntry entry = mBubbleNotificationRow.getEntry();
+        StatusBarNotification sbn = entry.getSbn();
 
         // Given
         sbn.getNotification().contentIntent = mContentIntent;
@@ -362,7 +374,7 @@
         when(mCentralSurfaces.isOccluded()).thenReturn(true);
 
         // When
-        mNotificationActivityStarter.onNotificationClicked(sbn, mBubbleNotificationRow);
+        mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow);
 
         // Then
         verify(mBubblesManager).expandStackAndSelectBubble(mBubbleNotificationRow.getEntry());
@@ -372,7 +384,7 @@
         verify(mAssistManager).hideAssist();
 
         verify(mClickNotifier).onNotificationClick(
-                eq(sbn.getKey()), any(NotificationVisibility.class));
+                eq(entry.getKey()), any(NotificationVisibility.class));
 
         // The content intent should NOT be sent on click.
         verify(mContentIntent).getIntent();
@@ -405,57 +417,4 @@
         // THEN display should try wake up for the full screen intent
         verify(mCentralSurfaces).wakeUpForFullScreenIntent();
     }
-
-    @Test
-    public void testNotifActivityStarterEventSourceStartEvent_onNotificationClicked() {
-        NotifActivityLaunchEvents.Listener listener =
-                mock(NotifActivityLaunchEvents.Listener.class);
-        mLaunchEventsEmitter.registerListener(listener);
-        mNotificationActivityStarter
-                .onNotificationClicked(mNotificationRow.getEntry().getSbn(), mNotificationRow);
-        verify(listener).onStartLaunchNotifActivity(mNotificationRow.getEntry());
-    }
-
-    @Test
-    public void testNotifActivityStarterEventSourceFinishEvent_dismissKeyguardCancelled() {
-        NotifActivityLaunchEvents.Listener listener =
-                mock(NotifActivityLaunchEvents.Listener.class);
-        mLaunchEventsEmitter.registerListener(listener);
-        // set up dismissKeyguardThenExecute to synchronously invoke the cancel runnable arg
-        doAnswer(answerVoid(
-                (OnDismissAction dismissAction, Runnable cancel, Boolean afterKeyguardGone) ->
-                        cancel.run()))
-                .when(mActivityStarter)
-                .dismissKeyguardThenExecute(any(OnDismissAction.class), any(), anyBoolean());
-        mNotificationActivityStarter
-                .onNotificationClicked(mNotificationRow.getEntry().getSbn(), mNotificationRow);
-        verify(listener).onFinishLaunchNotifActivity(mNotificationRow.getEntry());
-    }
-
-    @Test
-    public void testNotifActivityStarterEventSourceFinishEvent_postPanelCollapse()
-            throws Exception {
-        NotifActivityLaunchEvents.Listener listener =
-                mock(NotifActivityLaunchEvents.Listener.class);
-        mLaunchEventsEmitter.registerListener(listener);
-        mNotificationActivityStarter
-                .onNotificationClicked(mNotificationRow.getEntry().getSbn(), mNotificationRow);
-        ArgumentCaptor<ActivityLaunchAnimator.Controller> controllerCaptor =
-                ArgumentCaptor.forClass(ActivityLaunchAnimator.Controller.class);
-        verify(mActivityLaunchAnimator).startPendingIntentWithAnimation(
-                controllerCaptor.capture(), anyBoolean(), any(), any());
-        controllerCaptor.getValue().onIntentStarted(false);
-        verify(listener).onFinishLaunchNotifActivity(mNotificationRow.getEntry());
-    }
-
-    @Test
-    public void testNotifActivityStarterEventSourceFinishEvent_postPanelCollapse_noAnimate() {
-        NotifActivityLaunchEvents.Listener listener =
-                mock(NotifActivityLaunchEvents.Listener.class);
-        mLaunchEventsEmitter.registerListener(listener);
-        when(mCentralSurfaces.shouldAnimateLaunch(anyBoolean())).thenReturn(false);
-        mNotificationActivityStarter
-                .onNotificationClicked(mNotificationRow.getEntry().getSbn(), mNotificationRow);
-        verify(listener).onFinishLaunchNotifActivity(mNotificationRow.getEntry());
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 98397fb..6abc687 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -379,7 +379,7 @@
                 mCommandQueue,
                 mCarrierConfigTracker,
                 new CollapsedStatusBarFragmentLogger(
-                        new LogBuffer("TEST", 1, 1, mock(LogcatEchoTracker.class)),
+                        new LogBuffer("TEST", 1, mock(LogcatEchoTracker.class)),
                         new DisableFlagsLogger()
                         ),
                 mOperatorNameViewControllerFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ClockTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ClockTest.kt
new file mode 100644
index 0000000..22c72cc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ClockTest.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View.MeasureSpec.UNSPECIFIED
+import android.view.View.MeasureSpec.makeMeasureSpec
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.LinearLayout
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Before
+import org.junit.runner.RunWith
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class ClockTest : SysuiTestCase() {
+    private lateinit var clockView: Clock
+
+    @Before
+    fun setUp() {
+        allowTestableLooperAsMainThread()
+        TestableLooper.get(this).runWithLooper {
+            val container = LinearLayout(context)
+            val lp = LinearLayout.LayoutParams(1000, WRAP_CONTENT)
+            container.layoutParams = lp
+            clockView = Clock(context, null)
+            container.addView(clockView)
+            measureClock()
+        }
+    }
+
+    @Test
+    fun testWidthDoesNotDecrease_sameCharLength() {
+        // GIVEN time is narrow
+        clockView.text = ONE_3
+        measureClock()
+        val width1 = clockView.measuredWidth
+
+        // WHEN the text changes to be wider characters
+        clockView.text = ZERO_3
+        measureClock()
+        val width2 = clockView.measuredWidth
+
+        // THEN the width should be wider (or equals when using monospace font)
+        assertThat(width2).isAtLeast(width1)
+    }
+
+    @Test
+    fun testWidthDoesNotDecrease_narrowerFont_sameNumberOfChars() {
+        // GIVEN time is wide
+        clockView.text = ZERO_3
+        measureClock()
+        val width1 = clockView.measuredWidth
+
+        // WHEN the text changes to a narrower font
+        clockView.text = ONE_3
+        measureClock()
+        val width2 = clockView.measuredWidth
+
+        // THEN the width should not have decreased, and they should in fact be the same
+        assertThat(width2).isEqualTo(width1)
+    }
+
+    @Test
+    fun testWidthIncreases_whenCharsChanges() {
+        // GIVEN wide 3-char text
+        clockView.text = ZERO_3
+        measureClock()
+        val width1 = clockView.measuredWidth
+
+        // WHEN text changes to 4-char wide text
+        clockView.text = ZERO_4
+        measureClock()
+        val width2 = clockView.measuredWidth
+
+        // THEN the text field is wider
+        assertThat(width2).isGreaterThan(width1)
+    }
+
+    @Test
+    fun testWidthDecreases_whenCharsChange_longToShort() {
+        // GIVEN wide 4-char text
+        clockView.text = ZERO_4
+        measureClock()
+        val width1 = clockView.measuredWidth
+
+        // WHEN number of characters changes to a narrow 3-char text
+        clockView.text = ONE_3
+        measureClock()
+        val width2 = clockView.measuredWidth
+
+        // THEN the width can shrink, because number of chars changed
+        assertThat(width2).isLessThan(width1)
+    }
+
+    private fun measureClock() {
+        clockView.measure(
+                makeMeasureSpec(0, UNSPECIFIED),
+                makeMeasureSpec(0, UNSPECIFIED)
+        )
+    }
+}
+
+/**
+ * In a non-monospace font, it is expected that "0:00" is wider than "1:11"
+ */
+private const val ZERO_3 = "0:00"
+private const val ZERO_4 = "00:00"
+private const val ONE_3 = "1:11"
+private const val ONE_4 = "11:11"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index 3dfc94b..b7f38f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -74,6 +74,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Executor;
 
@@ -345,7 +347,9 @@
     @Test
     public void onSettingChanged_honorThemeStyle() {
         when(mDeviceProvisionedController.isUserSetup(anyInt())).thenReturn(true);
-        for (Style style : Style.values()) {
+        List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT,
+                Style.FRUIT_SALAD, Style.RAINBOW, Style.VIBRANT);
+        for (Style style : validStyles) {
             reset(mSecureSettings);
 
             String jsonString = "{\"android.theme.customization.system_palette\":\"A16B00\","
@@ -729,6 +733,18 @@
         mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
                 USER_SYSTEM);
 
+        reset(mResources);
+        when(mResources.getColor(eq(android.R.color.system_accent1_500), any()))
+                .thenReturn(mThemeOverlayController.mColorScheme.getAccent1().get(6));
+        when(mResources.getColor(eq(android.R.color.system_accent2_500), any()))
+                .thenReturn(mThemeOverlayController.mColorScheme.getAccent2().get(6));
+        when(mResources.getColor(eq(android.R.color.system_accent3_500), any()))
+                .thenReturn(mThemeOverlayController.mColorScheme.getAccent3().get(6));
+        when(mResources.getColor(eq(android.R.color.system_neutral1_500), any()))
+                .thenReturn(mThemeOverlayController.mColorScheme.getNeutral1().get(6));
+        when(mResources.getColor(eq(android.R.color.system_neutral2_500), any()))
+                .thenReturn(mThemeOverlayController.mColorScheme.getNeutral2().get(6));
+
         // Defers event because we already have initial colors.
         verify(mThemeOverlayApplier, never())
                 .applyCurrentUserOverlays(any(), any(), anyInt(), any());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
new file mode 100644
index 0000000..5e09b81
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/collection/RingBufferTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.collection
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class RingBufferTest : SysuiTestCase() {
+
+    private val buffer = RingBuffer(5) { TestElement() }
+
+    private val history = mutableListOf<TestElement>()
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun testBarelyFillBuffer() {
+        fillBuffer(5)
+
+        assertEquals(0, buffer[0].id)
+        assertEquals(1, buffer[1].id)
+        assertEquals(2, buffer[2].id)
+        assertEquals(3, buffer[3].id)
+        assertEquals(4, buffer[4].id)
+    }
+
+    @Test
+    fun testPartiallyFillBuffer() {
+        fillBuffer(3)
+
+        assertEquals(3, buffer.size)
+
+        assertEquals(0, buffer[0].id)
+        assertEquals(1, buffer[1].id)
+        assertEquals(2, buffer[2].id)
+
+        assertThrows(IndexOutOfBoundsException::class.java) { buffer[3] }
+        assertThrows(IndexOutOfBoundsException::class.java) { buffer[4] }
+    }
+
+    @Test
+    fun testSpinBuffer() {
+        fillBuffer(277)
+
+        assertEquals(272, buffer[0].id)
+        assertEquals(273, buffer[1].id)
+        assertEquals(274, buffer[2].id)
+        assertEquals(275, buffer[3].id)
+        assertEquals(276, buffer[4].id)
+        assertThrows(IndexOutOfBoundsException::class.java) { buffer[5] }
+
+        assertEquals(5, buffer.size)
+    }
+
+    @Test
+    fun testElementsAreRecycled() {
+        fillBuffer(23)
+
+        assertSame(history[4], buffer[1])
+        assertSame(history[9], buffer[1])
+        assertSame(history[14], buffer[1])
+        assertSame(history[19], buffer[1])
+    }
+
+    @Test
+    fun testIterator() {
+        fillBuffer(13)
+
+        val iterator = buffer.iterator()
+
+        for (i in 0 until 5) {
+            assertEquals(history[8 + i], iterator.next())
+        }
+        assertFalse(iterator.hasNext())
+        assertThrows(NoSuchElementException::class.java) { iterator.next() }
+    }
+
+    @Test
+    fun testForEach() {
+        fillBuffer(13)
+        var i = 8
+
+        buffer.forEach {
+            assertEquals(history[i], it)
+            i++
+        }
+        assertEquals(13, i)
+    }
+
+    private fun fillBuffer(count: Int) {
+        for (i in 0 until count) {
+            val elem = buffer.advance()
+            elem.id = history.size
+            history.add(elem)
+        }
+    }
+}
+
+private class TestElement(var id: Int = 0) {
+    override fun toString(): String {
+        return "{TestElement $id}"
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
index ec619bb..aaf2188 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogControllerImplTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.volume;
 
+import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -23,6 +25,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
+import android.app.KeyguardManager;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -91,6 +95,10 @@
     private WakefulnessLifecycle mWakefullnessLifcycle;
     @Mock
     private CaptioningManager mCaptioningManager;
+    @Mock
+    private KeyguardManager mKeyguardManager;
+    @Mock
+    private ActivityManager mActivityManager;
 
 
     @Before
@@ -112,7 +120,8 @@
         mVolumeController = new TestableVolumeDialogControllerImpl(mContext,
                 mBroadcastDispatcher, mRingerModeTracker, mThreadFactory, mAudioManager,
                 mNotificationManager, mVibrator, mIAudioService, mAccessibilityManager,
-                mPackageManager, mWakefullnessLifcycle, mCaptioningManager, mCallback);
+                mPackageManager, mWakefullnessLifcycle, mCaptioningManager, mKeyguardManager,
+                mActivityManager, mCallback);
         mVolumeController.setEnableDialogs(true, true);
     }
 
@@ -129,7 +138,8 @@
         when(mWakefullnessLifcycle.getWakefulness()).thenReturn(
                 WakefulnessLifecycle.WAKEFULNESS_AWAKE);
         mVolumeController.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI);
-        verify(mCallback, never()).onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED);
+        verify(mCallback, never()).onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED, false,
+                LOCK_TASK_MODE_NONE);
     }
 
     @Test
@@ -138,7 +148,8 @@
         when(mWakefullnessLifcycle.getWakefulness()).thenReturn(
                 WakefulnessLifecycle.WAKEFULNESS_AWAKE);
         mVolumeController.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI);
-        verify(mCallback, times(1)).onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED);
+        verify(mCallback, times(1)).onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED, false,
+                LOCK_TASK_MODE_NONE);
     }
 
     @Test
@@ -151,7 +162,8 @@
         when(mWakefullnessLifcycle.getWakefulness()).thenReturn(
                 WakefulnessLifecycle.WAKEFULNESS_GOING_TO_SLEEP);
         mVolumeController.onVolumeChangedW(0, AudioManager.FLAG_SHOW_UI);
-        verify(mCallback, times(1)).onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED);
+        verify(mCallback, times(1)).onShowRequested(Events.SHOW_REASON_VOLUME_CHANGED, false,
+                LOCK_TASK_MODE_NONE);
     }
 
     @Test
@@ -188,10 +200,13 @@
                 PackageManager packageManager,
                 WakefulnessLifecycle wakefulnessLifecycle,
                 CaptioningManager captioningManager,
+                KeyguardManager keyguardManager,
+                ActivityManager activityManager,
                 C callback) {
             super(context, broadcastDispatcher, ringerModeTracker, theadFactory, audioManager,
                     notificationManager, optionalVibrator, iAudioService, accessibilityManager,
-                    packageManager, wakefulnessLifecycle, captioningManager);
+                    packageManager, wakefulnessLifecycle, captioningManager, keyguardManager,
+                    activityManager);
             mCallbacks = callback;
 
             ArgumentCaptor<WakefulnessLifecycle.Observer> observerCaptor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 9493456..312db2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -38,6 +38,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
@@ -85,6 +86,8 @@
     MediaOutputDialogFactory mMediaOutputDialogFactory;
     @Mock
     ActivityStarter mActivityStarter;
+    @Mock
+    InteractionJankMonitor mInteractionJankMonitor;
 
     @Before
     public void setup() throws Exception {
@@ -99,7 +102,8 @@
                 mDeviceProvisionedController,
                 mConfigurationController,
                 mMediaOutputDialogFactory,
-                mActivityStarter);
+                mActivityStarter,
+                mInteractionJankMonitor);
         mDialog.init(0, null);
         State state = createShellState();
         mDialog.onStateChangedH(state);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java
index de2efc7..8e4f184 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/QuickAccessWalletControllerTest.java
@@ -100,7 +100,6 @@
                 mContext,
                 MoreExecutors.directExecutor(),
                 MoreExecutors.directExecutor(),
-                MoreExecutors.directExecutor(),
                 mSecureSettings,
                 mQuickAccessWalletClient,
                 mClock);
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 238a4d3..7d4e27f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -66,6 +66,7 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.service.dreams.IDreamManager;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.ZenModeConfig;
@@ -365,6 +366,7 @@
                 mStatusBarService,
                 mWindowManager,
                 mWindowManagerShellWrapper,
+                mock(UserManager.class),
                 mLauncherApps,
                 mBubbleLogger,
                 mTaskStackListener,
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 dff89e0..a6327b9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
@@ -55,6 +55,7 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.service.dreams.IDreamManager;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.ZenModeConfig;
@@ -330,6 +331,7 @@
                 mStatusBarService,
                 mWindowManager,
                 mWindowManagerShellWrapper,
+                mock(UserManager.class),
                 mLauncherApps,
                 mBubbleLogger,
                 mTaskStackListener,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 9646edf..17e5778 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.pm.LauncherApps;
 import android.os.Handler;
+import android.os.UserManager;
 import android.view.WindowManager;
 
 import com.android.internal.statusbar.IStatusBarService;
@@ -53,6 +54,7 @@
             IStatusBarService statusBarService,
             WindowManager windowManager,
             WindowManagerShellWrapper windowManagerShellWrapper,
+            UserManager userManager,
             LauncherApps launcherApps,
             BubbleLogger bubbleLogger,
             TaskStackListenerImpl taskStackListener,
@@ -66,10 +68,10 @@
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
         super(context, data, Runnable::run, floatingContentCoordinator, dataRepository,
-                statusBarService, windowManager, windowManagerShellWrapper, launcherApps,
-                bubbleLogger, taskStackListener, shellTaskOrganizer, positioner, displayController,
-                oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler,
-                taskViewTransitions, syncQueue);
+                statusBarService, windowManager, windowManagerShellWrapper, userManager,
+                launcherApps, bubbleLogger, taskStackListener, shellTaskOrganizer, positioner,
+                displayController, oneHandedOptional, dragAndDropController, shellMainExecutor,
+                shellMainHandler, new SyncExecutor(), taskViewTransitions, syncQueue);
         setInflateSynchronously(true);
         initialize();
     }
diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
index ac1f022..674bc74 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values/dimens.xml
@@ -18,11 +18,11 @@
 -->
 <resources>
     <!-- Height of the bottom navigation / system bar. -->
-    <dimen name="navigation_bar_height">16dp</dimen>
+    <dimen name="navigation_bar_height">24dp</dimen>
     <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->
-    <dimen name="navigation_bar_height_landscape">16dp</dimen>
+    <dimen name="navigation_bar_height_landscape">24dp</dimen>
     <!-- Width of the navigation bar when it is placed vertically on the screen -->
-    <dimen name="navigation_bar_width">16dp</dimen>
+    <dimen name="navigation_bar_width">24dp</dimen>
     <!-- Height of the bottom navigation / system bar. -->
     <dimen name="navigation_bar_frame_height">48dp</dimen>
     <!-- The height of the bottom navigation gesture area. -->
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml
index ac1f022..674bc74 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values/dimens.xml
@@ -18,11 +18,11 @@
 -->
 <resources>
     <!-- Height of the bottom navigation / system bar. -->
-    <dimen name="navigation_bar_height">16dp</dimen>
+    <dimen name="navigation_bar_height">24dp</dimen>
     <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->
-    <dimen name="navigation_bar_height_landscape">16dp</dimen>
+    <dimen name="navigation_bar_height_landscape">24dp</dimen>
     <!-- Width of the navigation bar when it is placed vertically on the screen -->
-    <dimen name="navigation_bar_width">16dp</dimen>
+    <dimen name="navigation_bar_width">24dp</dimen>
     <!-- Height of the bottom navigation / system bar. -->
     <dimen name="navigation_bar_frame_height">48dp</dimen>
     <!-- The height of the bottom navigation gesture area. -->
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml
index ac1f022..674bc74 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values/dimens.xml
@@ -18,11 +18,11 @@
 -->
 <resources>
     <!-- Height of the bottom navigation / system bar. -->
-    <dimen name="navigation_bar_height">16dp</dimen>
+    <dimen name="navigation_bar_height">24dp</dimen>
     <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->
-    <dimen name="navigation_bar_height_landscape">16dp</dimen>
+    <dimen name="navigation_bar_height_landscape">24dp</dimen>
     <!-- Width of the navigation bar when it is placed vertically on the screen -->
-    <dimen name="navigation_bar_width">16dp</dimen>
+    <dimen name="navigation_bar_width">24dp</dimen>
     <!-- Height of the bottom navigation / system bar. -->
     <dimen name="navigation_bar_frame_height">48dp</dimen>
     <!-- The height of the bottom navigation gesture area. -->
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml
index ac1f022..674bc74 100644
--- a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml
+++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values/dimens.xml
@@ -18,11 +18,11 @@
 -->
 <resources>
     <!-- Height of the bottom navigation / system bar. -->
-    <dimen name="navigation_bar_height">16dp</dimen>
+    <dimen name="navigation_bar_height">24dp</dimen>
     <!-- Height of the bottom navigation bar in portrait; often the same as @dimen/navigation_bar_height -->
-    <dimen name="navigation_bar_height_landscape">16dp</dimen>
+    <dimen name="navigation_bar_height_landscape">24dp</dimen>
     <!-- Width of the navigation bar when it is placed vertically on the screen -->
-    <dimen name="navigation_bar_width">16dp</dimen>
+    <dimen name="navigation_bar_width">24dp</dimen>
     <!-- Height of the bottom navigation / system bar. -->
     <dimen name="navigation_bar_frame_height">48dp</dimen>
     <!-- The height of the bottom navigation gesture area. -->
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 6d3620f..ca0a780 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -1965,15 +1965,6 @@
                 }
             }
         }
-        final AutofillId[] fieldClassificationIds = lastResponse.getFieldClassificationIds();
-
-        if (!hasAtLeastOneDataset && fieldClassificationIds == null) {
-            if (sVerbose) {
-                Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets nor fields "
-                        + "classification ids)");
-            }
-            return;
-        }
 
         for (int i = 0; i < mViewStates.size(); i++) {
             final ViewState viewState = mViewStates.valueAt(i);
@@ -2022,6 +2013,7 @@
                         }
                         continue;
                     }
+
                     // Check if value match a dataset.
                     if (hasAtLeastOneDataset) {
                         for (int j = 0; j < responseCount; j++) {
@@ -2078,7 +2070,6 @@
                             } // else
                         } // for j
                     }
-
                 } // else
             } // else
         }
diff --git a/services/autofill/java/com/android/server/autofill/ui/CustomScrollView.java b/services/autofill/java/com/android/server/autofill/ui/CustomScrollView.java
index 14bd7d7..9e8b222 100644
--- a/services/autofill/java/com/android/server/autofill/ui/CustomScrollView.java
+++ b/services/autofill/java/com/android/server/autofill/ui/CustomScrollView.java
@@ -18,7 +18,9 @@
 import static com.android.server.autofill.Helper.sDebug;
 
 import android.content.Context;
+import android.content.res.Configuration;
 import android.graphics.Point;
+import android.provider.DeviceConfig;
 import android.util.AttributeSet;
 import android.util.Slog;
 import android.util.TypedValue;
@@ -34,24 +36,59 @@
 
     private static final String TAG = "CustomScrollView";
 
+    /**
+     * Sets the max percent of screen that the autofill save dialog can take up in height
+     * when the device is in portrait orientation.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_SAVE_DIALOG_PORTRAIT_BODY_HEIGHT_MAX_PERCENT =
+            "autofill_save_dialog_portrait_body_height_max_percent";
+
+    /**
+     * Sets the max percent of screen that the autofill save dialog can take up in height
+     * when the device is in landscape orientation.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_SAVE_DIALOG_LANDSCAPE_BODY_HEIGHT_MAX_PERCENT =
+            "autofill_save_dialog_landscape_body_height_max_percent";
+
     private int mWidth = -1;
     private int mHeight = -1;
+    private int mMaxPortraitBodyHeightPercent = 20;
+    private int mMaxLandscapeBodyHeightPercent = 20;
 
     public CustomScrollView(Context context) {
         super(context);
+        setMaxBodyHeightPercent();
     }
 
     public CustomScrollView(Context context, AttributeSet attrs) {
         super(context, attrs);
+        setMaxBodyHeightPercent();
     }
 
     public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+        setMaxBodyHeightPercent();
     }
 
     public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
+        setMaxBodyHeightPercent();
+    }
+
+    private void setMaxBodyHeightPercent() {
+        mMaxPortraitBodyHeightPercent = DeviceConfig.getInt(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_SAVE_DIALOG_PORTRAIT_BODY_HEIGHT_MAX_PERCENT,
+                mMaxPortraitBodyHeightPercent);
+        mMaxLandscapeBodyHeightPercent = DeviceConfig.getInt(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_SAVE_DIALOG_LANDSCAPE_BODY_HEIGHT_MAX_PERCENT,
+                mMaxLandscapeBodyHeightPercent);
     }
 
     @Override
@@ -72,20 +109,37 @@
     private void calculateDimensions() {
         if (mHeight != -1) return;
 
-        final TypedValue typedValue = new TypedValue();
         final Point point = new Point();
         final Context context = getContext();
         context.getDisplayNoVerify().getSize(point);
-        context.getTheme().resolveAttribute(R.attr.autofillSaveCustomSubtitleMaxHeight,
-                typedValue, true);
-        final View child = getChildAt(0);
-        final int childHeight = child.getMeasuredHeight();
-        final int maxHeight = (int) typedValue.getFraction(point.y, point.y);
 
-        mHeight = Math.min(childHeight, maxHeight);
+        final View content = getChildAt(0);
+        final int contentHeight = content.getMeasuredHeight();
+        int displayHeight = point.y;
+
+        int configBasedMaxHeight = (getResources().getConfiguration().orientation
+                    == Configuration.ORIENTATION_LANDSCAPE)
+                ? (int) (mMaxLandscapeBodyHeightPercent * displayHeight / 100)
+                : (int) (mMaxPortraitBodyHeightPercent * displayHeight / 100);
+        mHeight = configBasedMaxHeight > 0
+                ? Math.min(contentHeight, configBasedMaxHeight)
+                : Math.min(contentHeight, getAttrBasedMaxHeight(context, displayHeight));
+
         if (sDebug) {
-            Slog.d(TAG, "calculateDimensions(): maxHeight=" + maxHeight
-                    + ", childHeight=" + childHeight + ", w=" + mWidth + ", h=" + mHeight);
+            Slog.d(TAG, "calculateDimensions():"
+                    + " mMaxPortraitBodyHeightPercent=" + mMaxPortraitBodyHeightPercent
+                    + ", mMaxLandscapeBodyHeightPercent=" + mMaxLandscapeBodyHeightPercent
+                    + ", configBasedMaxHeight=" + configBasedMaxHeight
+                    + ", attrBasedMaxHeight=" + getAttrBasedMaxHeight(context, displayHeight)
+                    + ", contentHeight=" + contentHeight
+                    + ", w=" + mWidth + ", h=" + mHeight);
         }
     }
+
+    private int getAttrBasedMaxHeight(Context context, int displayHeight) {
+        final TypedValue maxHeightAttrTypedValue = new TypedValue();
+        context.getTheme().resolveAttribute(R.attr.autofillSaveCustomSubtitleMaxHeight,
+                maxHeightAttrTypedValue, true);
+        return (int) maxHeightAttrTypedValue.getFraction(displayHeight, displayHeight);
+    }
 }
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java b/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java
index bc5cb02..99526b7 100644
--- a/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java
+++ b/services/backup/backuplib/java/com/android/server/backup/transport/TransportStatusCallback.java
@@ -26,7 +26,7 @@
 
 public class TransportStatusCallback extends ITransportStatusCallback.Stub {
     private static final String TAG = "TransportStatusCallback";
-    private static final int TIMEOUT_MILLIS = 600 * 1000; // 10 minutes.
+    private static final int TIMEOUT_MILLIS = 300 * 1000; // 5 minutes.
     private static final int OPERATION_STATUS_DEFAULT = 0;
 
     private final int mOperationTimeout;
diff --git a/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java b/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java
index 2eae6af..222d779 100644
--- a/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java
+++ b/services/cloudsearch/java/com/android/server/cloudsearch/CloudSearchPerUserService.java
@@ -147,7 +147,10 @@
                 }
             });
             if (sessionInfo.linkToDeath()) {
-                mCallbackQueue.put(requestId, sessionInfo);
+                CloudSearchCallbackInfo removedInfo = mCallbackQueue.put(requestId, sessionInfo);
+                if (removedInfo != null) {
+                    removedInfo.destroy();
+                }
             } else {
                 // destroy the session if calling process is already dead
                 onDestroyLocked(requestId);
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index 7a5fa62..570e4e6 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -182,7 +182,7 @@
         // 2b.1. Populate the request with required info.
         request.setPackageName(packageName);
         request.setUserId(userId);
-        request.setSkipPrompt(mayAssociateWithoutPrompt(request, packageName, userId));
+        request.setSkipPrompt(mayAssociateWithoutPrompt(packageName, userId));
 
         // 2b.2. Prepare extras and create an Intent.
         final Bundle extras = new Bundle();
@@ -321,18 +321,7 @@
         }
     };
 
-    private boolean mayAssociateWithoutPrompt(@NonNull AssociationRequest request,
-            @NonNull String packageName, @UserIdInt int userId) {
-        final String deviceProfile = request.getDeviceProfile();
-        if (deviceProfile != null) {
-            final boolean isRoleHolder = Binder.withCleanCallingIdentity(
-                    () -> isRoleHolder(mContext, userId, packageName, deviceProfile));
-            if (isRoleHolder) {
-                // Don't need to collect user's consent since app already holds the role.
-                return true;
-            }
-        }
-
+    private boolean mayAssociateWithoutPrompt(@NonNull String packageName, @UserIdInt int userId) {
         // Below we check if the requesting package is allowlisted (usually by the OEM) for creating
         // CDM associations without user confirmation (prompt).
         // For this we'll check to config arrays:
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 9b2554f..0bfe282 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -78,6 +78,7 @@
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
 import android.os.ShellCallback;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArraySet;
@@ -121,8 +122,10 @@
 
     private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
     private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
+    private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW =
+            "debug.cdm.cdmservice.removal_time_window";
 
-    private static final long ASSOCIATION_CLEAN_UP_TIME_WINDOW = DAYS.toMillis(3 * 30); // 3 months
+    private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90);
 
     private PersistentDataStore mPersistentStore;
     private final PersistUserStateHandler mUserPersistenceHandler;
@@ -211,8 +214,8 @@
             mPackageMonitor.register(context, FgThread.get().getLooper(), UserHandle.ALL, true);
             mDevicePresenceMonitor.init(context);
         } else if (phase == PHASE_BOOT_COMPLETED) {
-            // Run the Association CleanUp job service daily.
-            AssociationCleanUpService.schedule(getContext());
+            // Run the Inactive Association Removal job service daily.
+            InactiveAssociationsRemovalService.schedule(getContext());
         }
     }
 
@@ -410,17 +413,20 @@
         mCompanionAppController.onPackagesChanged(userId);
     }
 
-    // Revoke associations if the selfManaged companion device does not connect for 3
-    // months for specific profile.
-    private void associationCleanUp(String profile) {
+    // Revoke associations if the selfManaged companion device does not connect for 3 months.
+    void removeInactiveSelfManagedAssociations() {
+        final long currentTime = System.currentTimeMillis();
+        long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1);
+        if (removalWindow <= 0) {
+            // 0 or negative values indicate that the sysprop was never set or should be ignored.
+            removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT;
+        }
+
         for (AssociationInfo ai : mAssociationStore.getAssociations()) {
-            if (ai.isSelfManaged()
-                    && profile.equals(ai.getDeviceProfile())
-                    && System.currentTimeMillis() - ai.getLastTimeConnectedMs()
-                    >= ASSOCIATION_CLEAN_UP_TIME_WINDOW) {
-                Slog.i(TAG, "Removing the association for associationId: "
-                        + ai.getId()
-                        + " due to the device does not connect for 3 months.");
+            if (!ai.isSelfManaged()) continue;
+            final boolean isInactive =  currentTime - ai.getLastTimeConnectedMs() >= removalWindow;
+            if (isInactive) {
+                Slog.i(TAG, "Removing inactive self-managed association: " + ai.getId());
                 disassociateInternal(ai.getId());
             }
         }
@@ -649,6 +655,12 @@
 
         private void registerDevicePresenceListenerActive(String packageName, String deviceAddress,
                 boolean active) throws RemoteException {
+            if (DEBUG) {
+                Log.i(TAG, "registerDevicePresenceListenerActive()"
+                        + " active=" + active
+                        + " deviceAddress=" + deviceAddress);
+            }
+
             getContext().enforceCallingOrSelfPermission(
                     android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE,
                     "[un]registerDevicePresenceListenerService");
@@ -664,6 +676,12 @@
                         + " for user " + userId));
             }
 
+            // If already at specified state, then no-op.
+            if (active == association.isNotifyOnDeviceNearby()) {
+                if (DEBUG) Log.d(TAG, "Device presence listener is already at desired state.");
+                return;
+            }
+
             // AssociationInfo class is immutable: create a new AssociationInfo object with updated
             // flag.
             association = AssociationInfo.builder(association)
@@ -674,7 +692,17 @@
             // an application sets/unsets the mNotifyOnDeviceNearby flag.
             mAssociationStore.updateAssociation(association);
 
-            // TODO(b/218615198): correctly handle the case when the device is currently present.
+            // If device is already present, then trigger callback.
+            if (active && mDevicePresenceMonitor.isDevicePresent(association.getId())) {
+                if (DEBUG) Log.d(TAG, "Device is already present. Triggering callback.");
+                onDeviceAppearedInternal(association.getId());
+            }
+
+            // If last listener is unregistered, then unbind application.
+            if (!active && !shouldBindPackage(userId, packageName)) {
+                if (DEBUG) Log.d(TAG, "Last listener unregistered. Unbinding application.");
+                mCompanionAppController.unbindCompanionApplication(userId, packageName);
+            }
         }
 
         @Override
@@ -1056,10 +1084,10 @@
         return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
     }
 
-    private class LocalService extends CompanionDeviceManagerServiceInternal {
+    private class LocalService implements CompanionDeviceManagerServiceInternal {
         @Override
-        public void associationCleanUp(String profile) {
-            CompanionDeviceManagerService.this.associationCleanUp(profile);
+        public void removeInactiveSelfManagedAssociations() {
+            CompanionDeviceManagerService.this.removeInactiveSelfManagedAssociations();
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
index 326fefe..3649240 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
@@ -18,12 +18,10 @@
 
 /**
  * Companion Device Manager Local System Service Interface.
- *
- * @hide Only for use within the system server.
  */
-public abstract class CompanionDeviceManagerServiceInternal {
+interface CompanionDeviceManagerServiceInternal {
     /**
-     * @see CompanionDeviceManagerService#associationCleanUp
+     * @see CompanionDeviceManagerService#removeInactiveSelfManagedAssociations
      */
-    public abstract void associationCleanUp(String profile);
+    void removeInactiveSelfManagedAssociations();
 }
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index 6a19a42..434d283 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -17,6 +17,7 @@
 package com.android.server.companion;
 
 import android.companion.AssociationInfo;
+import android.os.Binder;
 import android.os.ShellCommand;
 import android.util.Log;
 import android.util.Slog;
@@ -96,6 +97,16 @@
                     mDevicePresenceMonitor.simulateDeviceDisappeared(associationId);
                     break;
 
+                case "remove-inactive-associations": {
+                    // This command should trigger the same "clean-up" job as performed by the
+                    // InactiveAssociationsRemovalService JobService. However, since the
+                    // InactiveAssociationsRemovalService run as system, we want to run this
+                    // as system (not as shell/root) as well.
+                    Binder.withCleanCallingIdentity(
+                            mService::removeInactiveSelfManagedAssociations);
+                }
+                break;
+
                 default:
                     return handleDefaultCommands(cmd);
             }
@@ -142,6 +153,12 @@
         pw.println("      invoked for the same device (same ASSOCIATION_ID) no longer than");
         pw.println("      60 seconds ago.");
         pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+
+        pw.println("  remove-inactive-associations");
+        pw.println("      Remove self-managed associations that have not been active ");
+        pw.println("      for a long time (90 days or as configured via ");
+        pw.println("      \"debug.cdm.cdmservice.cleanup_time_window\" system property). ");
+        pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
     }
 
     private int getNextIntArgRequired() {
diff --git a/services/companion/java/com/android/server/companion/AssociationCleanUpService.java b/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java
similarity index 68%
rename from services/companion/java/com/android/server/companion/AssociationCleanUpService.java
rename to services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java
index 55246e1..3482863 100644
--- a/services/companion/java/com/android/server/companion/AssociationCleanUpService.java
+++ b/services/companion/java/com/android/server/companion/InactiveAssociationsRemovalService.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -24,10 +24,8 @@
 import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
 import android.app.job.JobService;
-import android.companion.AssociationRequest;
 import android.content.ComponentName;
 import android.content.Context;
-import android.os.AsyncTask;
 import android.util.Slog;
 
 import com.android.server.LocalServices;
@@ -37,26 +35,24 @@
  * The job will be executed only if the device is charging and in idle mode due to the application
  * will be killed if association/role are revoked.
  */
-public class AssociationCleanUpService extends JobService {
-    private static final int JOB_ID = AssociationCleanUpService.class.hashCode();
+public class InactiveAssociationsRemovalService extends JobService {
+    private static final int JOB_ID = InactiveAssociationsRemovalService.class.hashCode();
     private static final long ONE_DAY_INTERVAL = DAYS.toMillis(1);
 
     @Override
     public boolean onStartJob(final JobParameters params) {
-        Slog.i(TAG, "Execute the Association CleanUp job");
-        // Special policy for APP_STREAMING role that need to revoke associations if the device
-        // does not connect for 3 months.
-        AsyncTask.execute(() -> {
-            LocalServices.getService(CompanionDeviceManagerServiceInternal.class)
-                    .associationCleanUp(AssociationRequest.DEVICE_PROFILE_APP_STREAMING);
-            jobFinished(params, false);
-        });
+        Slog.i(TAG, "Execute the Association Removal job");
+        // Special policy for selfManaged that need to revoke associations if the device
+        // does not connect for 90 days.
+        LocalServices.getService(CompanionDeviceManagerServiceInternal.class)
+                .removeInactiveSelfManagedAssociations();
+        jobFinished(params, false);
         return true;
     }
 
     @Override
     public boolean onStopJob(final JobParameters params) {
-        Slog.i(TAG, "Association cleanup job stopped; id=" + params.getJobId()
+        Slog.i(TAG, "Association removal job stopped; id=" + params.getJobId()
                 + ", reason="
                 + JobParameters.getInternalReasonCodeDescription(
                 params.getInternalStopReasonCode()));
@@ -64,10 +60,10 @@
     }
 
     static void schedule(Context context) {
-        Slog.i(TAG, "Scheduling the Association Cleanup job");
+        Slog.i(TAG, "Scheduling the Association Removal job");
         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
         final JobInfo job = new JobInfo.Builder(JOB_ID,
-                new ComponentName(context, AssociationCleanUpService.class))
+                new ComponentName(context, InactiveAssociationsRemovalService.class))
                 .setRequiresCharging(true)
                 .setRequiresDeviceIdle(true)
                 .setPeriodic(ONE_DAY_INTERVAL)
@@ -75,3 +71,4 @@
         jobScheduler.schedule(job);
     }
 }
+
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 80182d2..dc7573e 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -37,6 +37,7 @@
 import android.util.Slog;
 import android.view.Display;
 import android.view.InputDevice;
+import android.view.WindowManager;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -83,22 +84,26 @@
     private final NativeWrapper mNativeWrapper;
     private final DisplayManagerInternal mDisplayManagerInternal;
     private final InputManagerInternal mInputManagerInternal;
+    private final WindowManager mWindowManager;
     private final DeviceCreationThreadVerifier mThreadVerifier;
 
-    InputController(@NonNull Object lock, @NonNull Handler handler) {
-        this(lock, new NativeWrapper(), handler,
+    InputController(@NonNull Object lock, @NonNull Handler handler,
+            @NonNull WindowManager windowManager) {
+        this(lock, new NativeWrapper(), handler, windowManager,
                 // Verify that virtual devices are not created on the handler thread.
                 () -> !handler.getLooper().isCurrentThread());
     }
 
     @VisibleForTesting
     InputController(@NonNull Object lock, @NonNull NativeWrapper nativeWrapper,
-            @NonNull Handler handler, @NonNull DeviceCreationThreadVerifier threadVerifier) {
+            @NonNull Handler handler, @NonNull WindowManager windowManager,
+            @NonNull DeviceCreationThreadVerifier threadVerifier) {
         mLock = lock;
         mHandler = handler;
         mNativeWrapper = nativeWrapper;
         mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
         mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
+        mWindowManager = windowManager;
         mThreadVerifier = threadVerifier;
     }
 
@@ -209,6 +214,15 @@
         mInputManagerInternal.setDisplayEligibilityForPointerCapture(displayId, isEligible);
     }
 
+    void setLocalIme(int displayId) {
+        // WM throws a SecurityException if the display is untrusted.
+        if ((mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
+                == Display.FLAG_TRUSTED) {
+            mWindowManager.setDisplayImePolicy(displayId,
+                    WindowManager.DISPLAY_IME_POLICY_LOCAL);
+        }
+    }
+
     @GuardedBy("mLock")
     private void updateActivePointerDisplayIdLocked() {
         InputDeviceDescriptor mostRecentlyCreatedMouse = null;
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 9802b97..638b3ae 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -62,6 +62,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
+import android.view.WindowManager;
 import android.widget.Toast;
 import android.window.DisplayWindowPolicyController;
 
@@ -167,7 +168,9 @@
         mParams = params;
         if (inputController == null) {
             mInputController = new InputController(
-                    mVirtualDeviceLock, context.getMainThreadHandler());
+                    mVirtualDeviceLock,
+                    context.getMainThreadHandler(),
+                    context.getSystemService(WindowManager.class));
         } else {
             mInputController = inputController;
         }
@@ -537,6 +540,7 @@
             mInputController.setPointerAcceleration(1f, displayId);
             mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false,
                     displayId);
+            mInputController.setLocalIme(displayId);
 
             // Since we're being called in the middle of the display being created, we post a
             // task to grab the wakelock instead of doing it synchronously here, to avoid
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 61d784e..ad6e7db 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -730,7 +730,7 @@
                         String serviceName = mServiceNameResolver.getServiceName(userId);
                         ContentCaptureMetricsLogger.writeServiceEvent(
                                 EVENT__DATA_SHARE_ERROR_CONCURRENT_REQUEST,
-                                serviceName, request.getPackageName());
+                                serviceName);
                         clientAdapter.error(
                                 ContentCaptureManager.DATA_SHARE_ERROR_CONCURRENT_REQUEST);
                     } catch (RemoteException e) {
@@ -1303,8 +1303,7 @@
         private void logServiceEvent(int eventType) {
             int userId = UserHandle.getCallingUserId();
             String serviceName = mParentService.mServiceNameResolver.getServiceName(userId);
-            ContentCaptureMetricsLogger.writeServiceEvent(eventType, serviceName,
-                    mDataShareRequest.getPackageName());
+            ContentCaptureMetricsLogger.writeServiceEvent(eventType, serviceName);
         }
     }
 }
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java
index 7ea4eff..10bec64 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureMetricsLogger.java
@@ -34,72 +34,47 @@
     }
 
     /** @hide */
-    public static void writeServiceEvent(int eventType, @NonNull String serviceName,
-            @Nullable String targetPackage) {
+    public static void writeServiceEvent(int eventType, @NonNull String serviceName) {
+        // we should not logging the application package name
         FrameworkStatsLog.write(FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS, eventType,
-                serviceName, targetPackage);
-    }
-
-    /** @hide */
-    public static void writeServiceEvent(int eventType, @NonNull ComponentName service,
-            @Nullable ComponentName target) {
-        writeServiceEvent(eventType, ComponentName.flattenToShortString(service),
-                ComponentName.flattenToShortString(target));
-    }
-
-    /** @hide */
-    public static void writeServiceEvent(int eventType, @NonNull ComponentName service,
-            @Nullable String targetPackage) {
-        writeServiceEvent(eventType, ComponentName.flattenToShortString(service), targetPackage);
+                serviceName, /* componentName= */ null, 0, 0);
     }
 
     /** @hide */
     public static void writeServiceEvent(int eventType, @NonNull ComponentName service) {
-        writeServiceEvent(eventType, ComponentName.flattenToShortString(service), null);
+        writeServiceEvent(eventType, ComponentName.flattenToShortString(service));
     }
 
     /** @hide */
     public static void writeSetWhitelistEvent(@Nullable ComponentName service,
             @Nullable List<String> packages, @Nullable List<ComponentName> activities) {
         final String serviceName = ComponentName.flattenToShortString(service);
-        StringBuilder stringBuilder = new StringBuilder();
-        if (packages != null && packages.size() > 0) {
-            final int size = packages.size();
-            stringBuilder.append(packages.get(0));
-            for (int i = 1; i < size; i++) {
-                stringBuilder.append(" ");
-                stringBuilder.append(packages.get(i));
-            }
-        }
-        if (activities != null && activities.size() > 0) {
-            stringBuilder.append(" ");
-            stringBuilder.append(activities.get(0).flattenToShortString());
-            final int size = activities.size();
-            for (int i = 1; i < size; i++) {
-                stringBuilder.append(" ");
-                stringBuilder.append(activities.get(i).flattenToShortString());
-            }
-        }
+        int packageCount = packages != null ? packages.size() : 0;
+        int activityCount = activities != null ? activities.size() : 0;
+        // we should not logging the application package name
+        // log the allow list package and activity count instead
         FrameworkStatsLog.write(FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS,
                 FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__SET_WHITELIST,
-                serviceName, stringBuilder.toString());
+                serviceName, /* allowListStr= */ null, packageCount, activityCount);
     }
 
     /** @hide */
     public static void writeSessionEvent(int sessionId, int event, int flags,
-            @NonNull ComponentName service, @Nullable ComponentName app, boolean isChildSession) {
+            @NonNull ComponentName service, boolean isChildSession) {
+        // we should not logging the application package name
         FrameworkStatsLog.write(FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS, sessionId, event,
                 flags, ComponentName.flattenToShortString(service),
-                ComponentName.flattenToShortString(app), isChildSession);
+            /* componentName= */ null, isChildSession);
     }
 
     /** @hide */
     public static void writeSessionFlush(int sessionId, @NonNull ComponentName service,
-            @Nullable ComponentName app, @NonNull FlushMetrics fm,
-            @NonNull ContentCaptureOptions options, int flushReason) {
+            @NonNull FlushMetrics fm, @NonNull ContentCaptureOptions options,
+            int flushReason) {
+        // we should not logging the application package name
         FrameworkStatsLog.write(FrameworkStatsLog.CONTENT_CAPTURE_FLUSHED, sessionId,
                 ComponentName.flattenToShortString(service),
-                ComponentName.flattenToShortString(app), fm.sessionStarted, fm.sessionFinished,
+                /* componentName= */ null, fm.sessionStarted, fm.sessionFinished,
                 fm.viewAppearedCount, fm.viewDisappearedCount, fm.viewTextChangedCount,
                 options.maxBufferSize, options.idleFlushingFrequencyMs,
                 options.textChangeFlushingFrequencyMs, flushReason);
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index 822a42b..9bc1cee 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -330,7 +330,7 @@
             writeSessionEvent(sessionId,
                     FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
                     STATE_DISABLED | STATE_NO_SERVICE, serviceComponentName,
-                    componentName, /* isChildSession= */ false);
+                    /* isChildSession= */ false);
             return;
         }
         if (serviceComponentName == null) {
@@ -354,7 +354,7 @@
             writeSessionEvent(sessionId,
                     FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
                     STATE_DISABLED | STATE_NOT_WHITELISTED, serviceComponentName,
-                    componentName, /* isChildSession= */ false);
+                    /* isChildSession= */ false);
             return;
         }
 
@@ -368,7 +368,7 @@
             writeSessionEvent(sessionId,
                     FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
                     STATE_DISABLED | STATE_DUPLICATED_ID,
-                    serviceComponentName, componentName, /* isChildSession= */ false);
+                    serviceComponentName, /* isChildSession= */ false);
             return;
         }
 
@@ -385,7 +385,7 @@
             writeSessionEvent(sessionId,
                     FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__SESSION_NOT_CREATED,
                     STATE_DISABLED | STATE_NO_SERVICE, serviceComponentName,
-                    componentName, /* isChildSession= */ false);
+                    /* isChildSession= */ false);
             return;
         }
 
@@ -740,7 +740,7 @@
         @Override
         public void writeSessionFlush(int sessionId, ComponentName app, FlushMetrics flushMetrics,
                 ContentCaptureOptions options, int flushReason) {
-            ContentCaptureMetricsLogger.writeSessionFlush(sessionId, getServiceComponentName(), app,
+            ContentCaptureMetricsLogger.writeSessionFlush(sessionId, getServiceComponentName(),
                     flushMetrics, options, flushReason);
         }
 
diff --git a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
index 08e6a05..1efe55a 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
@@ -119,8 +119,7 @@
         // Metrics logging.
         writeSessionEvent(sessionId,
                 FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__ON_SESSION_STARTED,
-                initialState, getComponentName(), context.getActivityComponent(),
-                /* is_child_session= */ false);
+                initialState, getComponentName(), /* is_child_session= */ false);
     }
 
     /**
@@ -132,8 +131,7 @@
         // Metrics logging.
         writeSessionEvent(sessionId,
                 FrameworkStatsLog.CONTENT_CAPTURE_SESSION_EVENTS__EVENT__ON_SESSION_FINISHED,
-                /* flags= */ 0, getComponentName(), /* app= */ null,
-                /* is_child_session= */ false);
+                /* flags= */ 0, getComponentName(), /* is_child_session= */ false);
     }
 
     /**
@@ -158,7 +156,7 @@
         scheduleAsyncRequest((s) -> s.onDataShared(request, dataShareCallback));
         writeServiceEvent(
                 FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_DATA_SHARE_REQUEST,
-                mComponentName, request.getPackageName());
+                mComponentName);
     }
 
     /**
diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java
index 06f698e..ed61100 100644
--- a/services/core/java/android/content/pm/PackageManagerInternal.java
+++ b/services/core/java/android/content/pm/PackageManagerInternal.java
@@ -334,10 +334,14 @@
 
     /**
      * Retrieve all receivers that can handle a broadcast of the given intent.
+     * @param filterCallingUid The results will be filtered in the context of this UID instead
+     *                         of the calling UID.
+     * @param forSend true if the invocation is intended for sending broadcasts. The value
+     *                of this parameter affects how packages are filtered.
      */
     public abstract List<ResolveInfo> queryIntentReceivers(Intent intent,
             String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags,
-            int filterCallingUid, int userId);
+            int filterCallingUid, int userId, boolean forSend);
 
     /**
      * Retrieve all services that can be performed for the given intent.
@@ -371,10 +375,10 @@
             int deviceOwnerUserId, String deviceOwner, SparseArray<String> profileOwners);
 
     /**
-     * Called by Owners to set the package names protected by the device owner.
+     * Marks packages as protected for a given user or all users in case of USER_ALL.
      */
-    public abstract void setDeviceOwnerProtectedPackages(
-            String deviceOwnerPackageName, List<String> packageNames);
+    public abstract void setOwnerProtectedPackages(
+            @UserIdInt int userId, @NonNull List<String> packageNames);
 
     /**
      * Returns {@code true} if a given package can't be wiped. Otherwise, returns {@code false}.
diff --git a/services/core/java/com/android/server/CircularQueue.java b/services/core/java/com/android/server/CircularQueue.java
index aac6752..4538078 100644
--- a/services/core/java/com/android/server/CircularQueue.java
+++ b/services/core/java/com/android/server/CircularQueue.java
@@ -16,6 +16,7 @@
 
 package com.android.server;
 
+import android.annotation.Nullable;
 import android.util.ArrayMap;
 
 import java.util.Collection;
@@ -43,16 +44,19 @@
     /**
      * Put a (key|value) pair in the CircularQueue. Only the key will be added to the queue. Value
      * will be added to the ArrayMap.
-     * @return {@code true} (as specified by {@link Collection#add})
+     * @return the most recently removed value if keys were removed, or {@code null} if no keys were
+     * removed.
      */
-    public boolean put(K key, V value) {
+    @Nullable
+    public V put(K key, V value) {
         super.add(key);
         mArrayMap.put(key, value);
+        V removedValue = null;
         while (size() > mLimit) {
             K removedKey = super.remove();
-            mArrayMap.remove(removedKey);
+            removedValue = mArrayMap.remove(removedKey);
         }
-        return true;
+        return removedValue;
     }
 
     /**
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index bc40170..5eec6e5 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -3880,9 +3880,12 @@
                     match = vol.isVisibleForWrite(userId)
                             || (includeSharedProfile && vol.isVisibleForWrite(userIdSharingMedia));
                 } else {
+                    // Return both read only and write only volumes. When includeSharedProfile is
+                    // true, all the volumes of userIdSharingMedia should be returned when queried
+                    // from the user it shares media with
                     match = vol.isVisibleForUser(userId)
                             || (!vol.isVisible() && includeInvisible && vol.getPath() != null)
-                            || (includeSharedProfile && vol.isVisibleForRead(userIdSharingMedia));
+                            || (includeSharedProfile && vol.isVisibleForUser(userIdSharingMedia));
                 }
                 if (!match) continue;
 
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 1033aea..7a52af6 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -3066,42 +3066,88 @@
             Binder.restoreCallingIdentity(ident);
         }
 
+        // Send the broadcast exactly once to all possible disjoint sets of apps.
+        // If the location master switch is on, broadcast the ServiceState 4 times:
+        // - Full ServiceState sent to apps with ACCESS_FINE_LOCATION and READ_PHONE_STATE
+        // - Full ServiceState sent to apps with ACCESS_FINE_LOCATION and
+        //   READ_PRIVILEGED_PHONE_STATE but not READ_PHONE_STATE
+        // - Sanitized ServiceState sent to apps with READ_PHONE_STATE but not ACCESS_FINE_LOCATION
+        // - Sanitized ServiceState sent to apps with READ_PRIVILEGED_PHONE_STATE but neither
+        //   READ_PHONE_STATE nor ACCESS_FINE_LOCATION
+        // If the location master switch is off, broadcast the ServiceState multiple times:
+        // - Full ServiceState sent to all apps permitted to bypass the location master switch if
+        //   they have either READ_PHONE_STATE or READ_PRIVILEGED_PHONE_STATE
+        // - Sanitized ServiceState sent to all other apps with READ_PHONE_STATE
+        // - Sanitized ServiceState sent to all other apps with READ_PRIVILEGED_PHONE_STATE but not
+        //   READ_PHONE_STATE
+        if (Binder.withCleanCallingIdentity(() ->
+                LocationAccessPolicy.isLocationModeEnabled(mContext, mContext.getUserId()))) {
+            Intent fullIntent = createServiceStateIntent(state, subId, phoneId, false);
+            mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(
+                    fullIntent,
+                    new String[]{Manifest.permission.READ_PHONE_STATE,
+                            Manifest.permission.ACCESS_FINE_LOCATION});
+            mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(
+                    fullIntent,
+                    new String[]{Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+                            Manifest.permission.ACCESS_FINE_LOCATION},
+                    new String[]{Manifest.permission.READ_PHONE_STATE});
+
+            Intent sanitizedIntent = createServiceStateIntent(state, subId, phoneId, true);
+            mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(
+                    sanitizedIntent,
+                    new String[]{Manifest.permission.READ_PHONE_STATE},
+                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION});
+            mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(
+                    sanitizedIntent,
+                    new String[]{Manifest.permission.READ_PRIVILEGED_PHONE_STATE},
+                    new String[]{Manifest.permission.READ_PHONE_STATE,
+                            Manifest.permission.ACCESS_FINE_LOCATION});
+        } else {
+            String[] locationBypassPackages = Binder.withCleanCallingIdentity(() ->
+                    LocationAccessPolicy.getLocationBypassPackages(mContext));
+            for (String locationBypassPackage : locationBypassPackages) {
+                Intent fullIntent = createServiceStateIntent(state, subId, phoneId, false);
+                fullIntent.setPackage(locationBypassPackage);
+                mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(
+                        fullIntent,
+                        new String[]{Manifest.permission.READ_PHONE_STATE});
+                mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(
+                        fullIntent,
+                        new String[]{Manifest.permission.READ_PRIVILEGED_PHONE_STATE},
+                        new String[]{Manifest.permission.READ_PHONE_STATE});
+            }
+
+            Intent sanitizedIntent = createServiceStateIntent(state, subId, phoneId, true);
+            mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(
+                    sanitizedIntent,
+                    new String[]{Manifest.permission.READ_PHONE_STATE},
+                    new String[]{/* no excluded permissions */},
+                    locationBypassPackages);
+            mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(
+                    sanitizedIntent,
+                    new String[]{Manifest.permission.READ_PRIVILEGED_PHONE_STATE},
+                    new String[]{Manifest.permission.READ_PHONE_STATE},
+                    locationBypassPackages);
+        }
+    }
+
+    private Intent createServiceStateIntent(ServiceState state, int subId, int phoneId,
+            boolean sanitizeLocation) {
         Intent intent = new Intent(Intent.ACTION_SERVICE_STATE);
         intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
         Bundle data = new Bundle();
-        state.fillInNotifierBundle(data);
+        if (sanitizeLocation) {
+            state.createLocationInfoSanitizedCopy(true).fillInNotifierBundle(data);
+        } else {
+            state.fillInNotifierBundle(data);
+        }
         intent.putExtras(data);
-        // Pass the subscription along with the intent.
         intent.putExtra(PHONE_CONSTANTS_SUBSCRIPTION_KEY, subId);
         intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId);
         intent.putExtra(PHONE_CONSTANTS_SLOT_KEY, phoneId);
         intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, phoneId);
-
-        // Send the broadcast twice -- once for all apps with READ_PHONE_STATE, then again
-        // for all apps with READ_PRIVILEGED_PHONE_STATE but not READ_PHONE_STATE.
-        // Do this again twice, the first time for apps with ACCESS_FINE_LOCATION, then again with
-        // the location-sanitized service state for all apps without ACCESS_FINE_LOCATION.
-        // This ensures that any app holding either READ_PRIVILEGED_PHONE_STATE or READ_PHONE_STATE
-        // get this broadcast exactly once, and we are not exposing location without permission.
-        mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(intent,
-                new String[] {Manifest.permission.READ_PHONE_STATE,
-                        Manifest.permission.ACCESS_FINE_LOCATION});
-        mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(intent,
-                new String[] {Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
-                        Manifest.permission.ACCESS_FINE_LOCATION},
-                new String[] {Manifest.permission.READ_PHONE_STATE});
-
-        // Replace bundle with location-sanitized ServiceState
-        data = new Bundle();
-        state.createLocationInfoSanitizedCopy(true).fillInNotifierBundle(data);
-        intent.putExtras(data);
-        mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(intent,
-                new String[] {Manifest.permission.READ_PHONE_STATE},
-                new String[] {Manifest.permission.ACCESS_FINE_LOCATION});
-        mContext.createContextAsUser(UserHandle.ALL, 0).sendBroadcastMultiplePermissions(intent,
-                new String[] {Manifest.permission.READ_PRIVILEGED_PHONE_STATE},
-                new String[] {Manifest.permission.READ_PHONE_STATE,
-                        Manifest.permission.ACCESS_FINE_LOCATION});
+        return intent;
     }
 
     private void broadcastSignalStrengthChanged(SignalStrength signalStrength, int phoneId,
diff --git a/services/core/java/com/android/server/VpnManagerService.java b/services/core/java/com/android/server/VpnManagerService.java
index c1d8e7b..d3ef6be 100644
--- a/services/core/java/com/android/server/VpnManagerService.java
+++ b/services/core/java/com/android/server/VpnManagerService.java
@@ -880,6 +880,38 @@
         }
     }
 
+    @Override
+    public boolean setAppExclusionList(int userId, String vpnPackage, List<String> excludedApps) {
+        enforceSettingsPermission();
+        enforceCrossUserPermission(userId);
+
+        synchronized (mVpns) {
+            final Vpn vpn = mVpns.get(userId);
+            if (vpn != null) {
+                return vpn.setAppExclusionList(vpnPackage, excludedApps);
+            } else {
+                logw("User " + userId + " has no Vpn configuration");
+                throw new IllegalStateException(
+                        "VPN for user " + userId + " not ready yet. Skipping setting the list");
+            }
+        }
+    }
+
+    @Override
+    public List<String> getAppExclusionList(int userId, String vpnPackage) {
+        enforceSettingsPermission();
+        enforceCrossUserPermission(userId);
+
+        synchronized (mVpns) {
+            final Vpn vpn = mVpns.get(userId);
+            if (vpn != null) {
+                return vpn.getAppExclusionList(vpnPackage);
+            } else {
+                logw("User " + userId + " has no Vpn configuration");
+                return null;
+            }
+        }
+    }
 
     @Override
     public void factoryReset() {
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index fbf6482..c678a67 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -19,6 +19,8 @@
 import static com.android.server.Watchdog.HandlerCheckerAndTimeout.withCustomTimeout;
 import static com.android.server.Watchdog.HandlerCheckerAndTimeout.withDefaultTimeout;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.IActivityController;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -43,6 +45,7 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.sysprop.WatchdogProperties;
+import android.util.Dumpable;
 import android.util.EventLog;
 import android.util.Log;
 import android.util.Slog;
@@ -63,6 +66,7 @@
 import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -76,7 +80,7 @@
 /**
  * This class calls its monitor every minute. Killing this process if they don't return
  **/
-public class Watchdog {
+public class Watchdog implements Dumpable {
     static final String TAG = "Watchdog";
 
     /** Debug flag. */
@@ -1028,4 +1032,10 @@
         }
         doSysRq('c');
     }
+
+    @Override
+    public void dump(@NonNull PrintWriter pw, @Nullable String[] args) {
+        pw.print("WatchdogTimeoutMillis=");
+        pw.println(mWatchdogTimeoutMillis);
+    }
 }
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 6fa3bc8..48b3d0e 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -6476,10 +6476,16 @@
         }
 
         if (ret == REASON_DENIED) {
-            final boolean isAllowedPackage =
-                    mAllowListWhileInUsePermissionInFgs.contains(callingPackage);
-            if (isAllowedPackage) {
-                ret = REASON_ALLOWLISTED_PACKAGE;
+            if (verifyPackage(callingPackage, callingUid)) {
+                final boolean isAllowedPackage =
+                        mAllowListWhileInUsePermissionInFgs.contains(callingPackage);
+                if (isAllowedPackage) {
+                    ret = REASON_ALLOWLISTED_PACKAGE;
+                }
+            } else {
+                EventLog.writeEvent(0x534e4554, "215003903", callingUid,
+                        "callingPackage:" + callingPackage + " does not belong to callingUid:"
+                                + callingUid);
             }
         }
 
@@ -6593,12 +6599,11 @@
         }
 
         final int uidState = mAm.getUidStateLocked(callingUid);
-        int callerTargetSdkVersion = INVALID_UID;
+        int callerTargetSdkVersion = -1;
         try {
-            ApplicationInfo ai = mAm.mContext.getPackageManager().getApplicationInfoAsUser(
-                    callingPackage, PackageManager.MATCH_KNOWN_PACKAGES, userId);
-            callerTargetSdkVersion = ai.targetSdkVersion;
-        } catch (PackageManager.NameNotFoundException e) {
+            callerTargetSdkVersion = mAm.mContext.getPackageManager()
+                    .getTargetSdkVersion(callingPackage);
+        } catch (PackageManager.NameNotFoundException ignored) {
         }
         final String debugInfo =
                 "[callingPackage: " + callingPackage
@@ -6883,4 +6888,19 @@
                 /* allowBackgroundActivityStarts */ false)
                 != REASON_DENIED;
     }
+
+    /**
+     * Checks if a given packageName belongs to a given uid.
+     * @param packageName the package of the caller
+     * @param uid the uid of the caller
+     * @return true or false
+     */
+    private boolean verifyPackage(String packageName, int uid) {
+        if (uid == ROOT_UID || uid == SYSTEM_UID) {
+            //System and Root are always allowed
+            return true;
+        }
+        return mAm.getPackageManagerInternal().isSameApp(packageName, uid,
+                UserHandle.getUserId(uid));
+    }
 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index dea8cc0..3a5a866 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1530,6 +1530,8 @@
     // Encapsulates the global setting "hidden_api_blacklist_exemptions"
     final HiddenApiSettings mHiddenApiBlacklist;
 
+    final SdkSandboxSettings mSdkSandboxSettings;
+
     private final PlatformCompat mPlatformCompat;
 
     PackageManagerInternal mPackageManagerInt;
@@ -2235,6 +2237,53 @@
         }
     }
 
+    /**
+     * Handles settings related to the enforcement of SDK sandbox restrictions.
+     */
+    static class SdkSandboxSettings implements DeviceConfig.OnPropertiesChangedListener {
+
+        private final Context mContext;
+        private final Object mLock = new Object();
+
+        @GuardedBy("mLock")
+        private boolean mEnforceBroadcastReceiverRestrictions;
+
+        /**
+         * Property to enforce broadcast receiver restrictions for SDK sandbox processes. If the
+         * value of this property is {@code true}, the restrictions will be enforced.
+         */
+        public static final String ENFORCE_BROADCAST_RECEIVER_RESTRICTIONS =
+                "enforce_broadcast_receiver_restrictions";
+
+        SdkSandboxSettings(Context context) {
+            mContext = context;
+        }
+
+        void registerObserver() {
+            synchronized (mLock) {
+                mEnforceBroadcastReceiverRestrictions = DeviceConfig.getBoolean(
+                        DeviceConfig.NAMESPACE_SDK_SANDBOX,
+                        ENFORCE_BROADCAST_RECEIVER_RESTRICTIONS, false);
+                DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SDK_SANDBOX,
+                        mContext.getMainExecutor(), this);
+            }
+        }
+
+        @Override
+        public void onPropertiesChanged(DeviceConfig.Properties properties) {
+            synchronized (mLock) {
+                mEnforceBroadcastReceiverRestrictions = properties.getBoolean(
+                        ENFORCE_BROADCAST_RECEIVER_RESTRICTIONS, false);
+            }
+        }
+
+        boolean isBroadcastReceiverRestrictionsEnforced() {
+            synchronized (mLock) {
+                return mEnforceBroadcastReceiverRestrictions;
+            }
+        }
+    }
+
     AppOpsManager getAppOpsManager() {
         if (mAppOpsManager == null) {
             mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
@@ -2287,6 +2336,7 @@
         mProcStartHandlerThread = null;
         mProcStartHandler = null;
         mHiddenApiBlacklist = null;
+        mSdkSandboxSettings = null;
         mFactoryTest = FACTORY_TEST_OFF;
         mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class);
         mInternal = new LocalService();
@@ -2406,6 +2456,7 @@
         mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
 
         mHiddenApiBlacklist = new HiddenApiSettings(mHandler, mContext);
+        mSdkSandboxSettings = new SdkSandboxSettings(mContext);
 
         Watchdog.getInstance().addMonitor(this);
         Watchdog.getInstance().addThread(mHandler);
@@ -2627,7 +2678,7 @@
     public void batterySendBroadcast(Intent intent) {
         synchronized (this) {
             broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null, null, null,
-                    OP_NONE, null, false, false, -1, SYSTEM_UID, Binder.getCallingUid(),
+                    null, OP_NONE, null, false, false, -1, SYSTEM_UID, Binder.getCallingUid(),
                     Binder.getCallingPid(), UserHandle.USER_ALL);
         }
     }
@@ -4241,7 +4292,7 @@
         intent.putExtra(Intent.EXTRA_UID, uid);
         intent.putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.getUserId(uid));
         broadcastIntentLocked(null, null, null, intent,
-                null, null, 0, null, null, null, null, OP_NONE,
+                null, null, 0, null, null, null, null, null, OP_NONE,
                 null, false, false, MY_PID, SYSTEM_UID, Binder.getCallingUid(),
                 Binder.getCallingPid(), UserHandle.getUserId(uid));
     }
@@ -4358,6 +4409,14 @@
                     + " but does not exist in that user");
             return;
         }
+
+        // Policy: certain classes of app are not subject to user-invoked stop
+        if (getPackageManagerInternal().isPackageStateProtected(packageName, userId)) {
+            Slog.w(TAG, "Asked to stop " + packageName + "/u" + userId
+                    + " but it is protected");
+            return;
+        }
+
         Slog.i(TAG, "Stopping app for user: " + packageName + "/" + userId);
 
         // A specific subset of the work done in forceStopPackageLocked(), because we are
@@ -4800,7 +4859,8 @@
                 thread.runIsolatedEntryPoint(
                         app.getIsolatedEntryPoint(), app.getIsolatedEntryPointArgs());
             } else if (instr2 != null) {
-                thread.bindApplication(processName, appInfo, app.sdkSandboxClientAppPackage,
+                thread.bindApplication(processName, appInfo,
+                        app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,
                         providerList,
                         instr2.mClass,
                         profilerInfo, instr2.mArguments,
@@ -4815,7 +4875,8 @@
                         app.getDisabledCompatChanges(), serializedSystemFontMap,
                         app.getStartElapsedTime(), app.getStartUptime());
             } else {
-                thread.bindApplication(processName, appInfo, app.sdkSandboxClientAppPackage,
+                thread.bindApplication(processName, appInfo,
+                        app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,
                         providerList, null, profilerInfo, null, null, null, testMode,
                         mBinderTransactionTrackingEnabled, enableTrackAllocation,
                         isRestrictedBackupMode || !normalMode, app.isPersistent(),
@@ -7911,6 +7972,7 @@
         final boolean alwaysFinishActivities =
                 Settings.Global.getInt(resolver, ALWAYS_FINISH_ACTIVITIES, 0) != 0;
         mHiddenApiBlacklist.registerObserver();
+        mSdkSandboxSettings.registerObserver();
         mPlatformCompat.registerContentObserver();
 
         mAppProfiler.retrieveSettings();
@@ -8122,7 +8184,7 @@
                             | Intent.FLAG_RECEIVER_FOREGROUND);
                     intent.putExtra(Intent.EXTRA_USER_HANDLE, currentUserId);
                     broadcastIntentLocked(null, null, null, intent,
-                            null, null, 0, null, null, null, null, OP_NONE,
+                            null, null, 0, null, null, null, null, null, OP_NONE,
                             null, false, false, MY_PID, SYSTEM_UID, callingUid, callingPid,
                             currentUserId);
                     intent = new Intent(Intent.ACTION_USER_STARTING);
@@ -8134,8 +8196,8 @@
                                 public void performReceive(Intent intent, int resultCode,
                                         String data, Bundle extras, boolean ordered, boolean sticky,
                                         int sendingUser) {}
-                            }, 0, null, null, new String[] {INTERACT_ACROSS_USERS}, null, OP_NONE,
-                            null, true, false, MY_PID, SYSTEM_UID, callingUid, callingPid,
+                            }, 0, null, null, new String[] {INTERACT_ACROSS_USERS}, null, null,
+                            OP_NONE, null, true, false, MY_PID, SYSTEM_UID, callingUid, callingPid,
                             UserHandle.USER_ALL);
                 } catch (Throwable e) {
                     Slog.wtf(TAG, "Failed sending first user broadcasts", e);
@@ -12940,7 +13002,7 @@
         // Allow Sandbox process to register only unexported receivers.
         if ((flags & Context.RECEIVER_NOT_EXPORTED) != 0) {
             enforceNotIsolatedCaller("registerReceiver");
-        } else {
+        } else if (mSdkSandboxSettings.isBroadcastReceiverRestrictionsEnforced()) {
             enforceNotIsolatedOrSdkSandboxCaller("registerReceiver");
         }
         ArrayList<Intent> stickyIntents = null;
@@ -13165,8 +13227,8 @@
                     Intent intent = allSticky.get(i);
                     BroadcastQueue queue = broadcastQueueForIntent(intent);
                     BroadcastRecord r = new BroadcastRecord(queue, intent, null,
-                            null, null, -1, -1, false, null, null, null, OP_NONE, null, receivers,
-                            null, 0, null, null, false, true, true, -1, false, null,
+                            null, null, -1, -1, false, null, null, null, null, OP_NONE, null,
+                            receivers, null, 0, null, null, false, true, true, -1, false, null,
                             false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */);
                     queue.enqueueParallelBroadcastLocked(r);
                     queue.scheduleBroadcastsLocked();
@@ -13248,8 +13310,8 @@
                     UserManager.DISALLOW_DEBUGGING_FEATURES, user)) {
                 continue;
             }
-            List<ResolveInfo> newReceivers = mPackageManagerInt
-                    .queryIntentReceivers(intent, resolvedType, pmFlags, callingUid, user);
+            List<ResolveInfo> newReceivers = mPackageManagerInt.queryIntentReceivers(
+                    intent, resolvedType, pmFlags, callingUid, user, true /* forSend */);
             if (user != UserHandle.USER_SYSTEM && newReceivers != null) {
                 // If this is not the system user, we need to check for
                 // any receivers that should be filtered out.
@@ -13265,8 +13327,9 @@
             if (newReceivers != null) {
                 for (int i = newReceivers.size() - 1; i >= 0; i--) {
                     final ResolveInfo ri = newReceivers.get(i);
-                    final Resolution<ResolveInfo> resolution = mComponentAliasResolver
-                            .resolveReceiver(intent, ri, resolvedType, pmFlags, user, callingUid);
+                    final Resolution<ResolveInfo> resolution =
+                            mComponentAliasResolver.resolveReceiver(intent, ri, resolvedType,
+                                    pmFlags, user, callingUid, true /* forSend */);
                     if (resolution == null) {
                         // It was an alias, but the target was not found.
                         newReceivers.remove(i);
@@ -13421,12 +13484,14 @@
             String callerPackage, String callerFeatureId, Intent intent, String resolvedType,
             IIntentReceiver resultTo, int resultCode, String resultData,
             Bundle resultExtras, String[] requiredPermissions, String[] excludedPermissions,
-            int appOp, Bundle bOptions, boolean ordered, boolean sticky, int callingPid,
+            String[] excludedPackages, int appOp, Bundle bOptions, boolean ordered,
+            boolean sticky, int callingPid,
             int callingUid, int realCallingUid, int realCallingPid, int userId) {
         return broadcastIntentLocked(callerApp, callerPackage, callerFeatureId, intent,
                 resolvedType, resultTo, resultCode, resultData, resultExtras, requiredPermissions,
-                excludedPermissions, appOp, bOptions, ordered, sticky, callingPid, callingUid,
-                realCallingUid, realCallingPid, userId, false /* allowBackgroundActivityStarts */,
+                excludedPermissions, excludedPackages, appOp, bOptions, ordered, sticky, callingPid,
+                callingUid, realCallingUid, realCallingPid, userId,
+                false /* allowBackgroundActivityStarts */,
                 null /* tokenNeededForBackgroundActivityStarts */, null /* broadcastAllowList */);
     }
 
@@ -13435,7 +13500,7 @@
             @Nullable String callerFeatureId, Intent intent, String resolvedType,
             IIntentReceiver resultTo, int resultCode, String resultData,
             Bundle resultExtras, String[] requiredPermissions,
-            String[] excludedPermissions, int appOp, Bundle bOptions,
+            String[] excludedPermissions, String[] excludedPackages, int appOp, Bundle bOptions,
             boolean ordered, boolean sticky, int callingPid, int callingUid,
             int realCallingUid, int realCallingPid, int userId,
             boolean allowBackgroundActivityStarts,
@@ -13543,13 +13608,8 @@
             }
 
             if (brOptions.getIdForResponseEvent() > 0) {
-                // STOPSHIP (206518114): Temporarily check for PACKAGE_USAGE_STATS permission as
-                // well until the clients switch to using the new permission.
-                if (checkPermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS,
-                        callingPid, callingUid) != PERMISSION_GRANTED) {
-                    enforceUsageStatsPermission(callerPackage, callingUid, callingPid,
-                            "recordResponseEventWhileInBackground()");
-                }
+                enforcePermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS,
+                        callingPid, callingUid, "recordResponseEventWhileInBackground");
             }
         }
 
@@ -14042,10 +14102,10 @@
             final BroadcastQueue queue = broadcastQueueForIntent(intent);
             BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
                     callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
-                    requiredPermissions, excludedPermissions, appOp, brOptions, registeredReceivers,
-                    resultTo, resultCode, resultData, resultExtras, ordered, sticky, false, userId,
-                    allowBackgroundActivityStarts, backgroundActivityStartsToken,
-                    timeoutExempt);
+                    requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
+                    registeredReceivers, resultTo, resultCode, resultData, resultExtras, ordered,
+                    sticky, false, userId, allowBackgroundActivityStarts,
+                    backgroundActivityStartsToken, timeoutExempt);
             if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing parallel broadcast " + r);
             final boolean replaced = replacePending
                     && (queue.replaceParallelBroadcastLocked(r) != null);
@@ -14140,7 +14200,7 @@
             BroadcastQueue queue = broadcastQueueForIntent(intent);
             BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
                     callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
-                    requiredPermissions, excludedPermissions, appOp, brOptions,
+                    requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
                     receivers, resultTo, resultCode, resultData, resultExtras,
                     ordered, sticky, false, userId, allowBackgroundActivityStarts,
                     backgroundActivityStartsToken, timeoutExempt);
@@ -14269,14 +14329,16 @@
             String[] requiredPermissions, int appOp, Bundle bOptions,
             boolean serialized, boolean sticky, int userId) {
         return broadcastIntentWithFeature(caller, null, intent, resolvedType, resultTo, resultCode,
-                resultData, resultExtras, requiredPermissions, null, appOp, bOptions, serialized,
-                sticky, userId);
+                resultData, resultExtras, requiredPermissions, null, null, appOp, bOptions,
+                serialized, sticky, userId);
     }
 
+    @Override
     public final int broadcastIntentWithFeature(IApplicationThread caller, String callingFeatureId,
             Intent intent, String resolvedType, IIntentReceiver resultTo,
             int resultCode, String resultData, Bundle resultExtras,
-            String[] requiredPermissions, String[] excludedPermissions, int appOp, Bundle bOptions,
+            String[] requiredPermissions, String[] excludedPermissions,
+            String[] excludedPackages, int appOp, Bundle bOptions,
             boolean serialized, boolean sticky, int userId) {
         enforceNotIsolatedCaller("broadcastIntent");
         synchronized(this) {
@@ -14291,8 +14353,8 @@
                 return broadcastIntentLocked(callerApp,
                         callerApp != null ? callerApp.info.packageName : null, callingFeatureId,
                         intent, resolvedType, resultTo, resultCode, resultData, resultExtras,
-                        requiredPermissions, excludedPermissions, appOp, bOptions, serialized,
-                        sticky, callingPid, callingUid, callingUid, callingPid, userId);
+                        requiredPermissions, excludedPermissions, excludedPackages, appOp, bOptions,
+                        serialized, sticky, callingPid, callingUid, callingUid, callingPid, userId);
             } finally {
                 Binder.restoreCallingIdentity(origId);
             }
@@ -14315,7 +14377,7 @@
             try {
                 return broadcastIntentLocked(null, packageName, featureId, intent, resolvedType,
                         resultTo, resultCode, resultData, resultExtras, requiredPermissions, null,
-                        OP_NONE, bOptions, serialized, sticky, -1, uid, realCallingUid,
+                        null, OP_NONE, bOptions, serialized, sticky, -1, uid, realCallingUid,
                         realCallingPid, userId, allowBackgroundActivityStarts,
                         backgroundActivityStartsToken, broadcastAllowList);
             } finally {
@@ -16834,10 +16896,11 @@
                     return ActivityManagerService.this.broadcastIntentLocked(null /*callerApp*/,
                             null /*callerPackage*/, null /*callingFeatureId*/, intent,
                             null /*resolvedType*/, resultTo, 0 /*resultCode*/, null /*resultData*/,
-                            null /*resultExtras*/, requiredPermissions, null, AppOpsManager.OP_NONE,
-                            bOptions /*options*/, serialized, false /*sticky*/, callingPid,
-                            callingUid, callingUid, callingPid, userId,
-                            false /*allowBackgroundStarts*/,
+                            null /*resultExtras*/, requiredPermissions,
+                            null /*excludedPermissions*/, null /*excludedPackages*/,
+                            AppOpsManager.OP_NONE, bOptions /*options*/, serialized,
+                            false /*sticky*/, callingPid, callingUid, callingUid, callingPid,
+                            userId, false /*allowBackgroundStarts*/,
                             null /*tokenNeededForBackgroundActivityStarts*/, appIdAllowList);
                 } finally {
                     Binder.restoreCallingIdentity(origId);
@@ -16973,7 +17036,7 @@
                         | Intent.FLAG_RECEIVER_FOREGROUND
                         | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
                 broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null, null,
-                        null, OP_NONE, null, false, false, MY_PID, SYSTEM_UID,
+                        null, null, OP_NONE, null, false, false, MY_PID, SYSTEM_UID,
                         Binder.getCallingUid(), Binder.getCallingPid(), UserHandle.USER_ALL);
                 if ((changes & ActivityInfo.CONFIG_LOCALE) != 0) {
                     intent = new Intent(Intent.ACTION_LOCALE_CHANGED);
@@ -16988,8 +17051,8 @@
                             TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
                             PowerExemptionManager.REASON_LOCALE_CHANGED, "");
                     broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null, null,
-                            null, OP_NONE, bOptions.toBundle(), false, false, MY_PID, SYSTEM_UID,
-                            Binder.getCallingUid(), Binder.getCallingPid(),
+                            null, null, OP_NONE, bOptions.toBundle(), false, false, MY_PID,
+                            SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(),
                             UserHandle.USER_ALL);
                 }
 
@@ -17004,8 +17067,9 @@
                     String[] permissions =
                             new String[] { android.Manifest.permission.INSTALL_PACKAGES };
                     broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null,
-                            permissions, null, OP_NONE, null, false, false, MY_PID, SYSTEM_UID,
-                            Binder.getCallingUid(), Binder.getCallingPid(), UserHandle.USER_ALL);
+                            permissions, null, null, OP_NONE, null, false, false, MY_PID,
+                            SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(),
+                            UserHandle.USER_ALL);
                 }
             }
         }
@@ -17029,8 +17093,8 @@
                 }
 
                 broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null, null,
-                        null, OP_NONE, null, false, false, -1, SYSTEM_UID, Binder.getCallingUid(),
-                        Binder.getCallingPid(), UserHandle.USER_ALL);
+                        null, null, OP_NONE, null, false, false, -1, SYSTEM_UID,
+                        Binder.getCallingUid(), Binder.getCallingPid(), UserHandle.USER_ALL);
             }
         }
 
@@ -17195,17 +17259,17 @@
         }
 
         @Override
-        public boolean isUidCurrentlyInstrumented(int uid) {
+        public int getInstrumentationSourceUid(int uid) {
             synchronized (mProcLock) {
                 for (int i = mActiveInstrumentation.size() - 1; i >= 0; i--) {
                     ActiveInstrumentation activeInst = mActiveInstrumentation.get(i);
                     if (!activeInst.mFinished && activeInst.mTargetInfo != null
                             && activeInst.mTargetInfo.uid == uid) {
-                        return true;
+                        return activeInst.mSourceUid;
                     }
                 }
             }
-            return false;
+            return INVALID_UID;
         }
 
         @Override
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 402491d..2c2579f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -804,8 +804,8 @@
         pw.flush();
         Bundle bundle = mBroadcastOptions == null ? null : mBroadcastOptions.toBundle();
         mInterface.broadcastIntentWithFeature(null, null, intent, null, receiver, 0, null, null,
-                requiredPermissions, null, android.app.AppOpsManager.OP_NONE, bundle, true, false,
-                mUserId);
+                requiredPermissions, null, null, android.app.AppOpsManager.OP_NONE, bundle, true,
+                false, mUserId);
         if (!mAsync) {
             receiver.waitForFinish();
         }
@@ -3561,6 +3561,9 @@
             pw.println("     Enable/disable rate limit on FGS notification deferral policy.");
             pw.println("  force-stop [--user <USER_ID> | all | current] <PACKAGE>");
             pw.println("      Completely stop the given application package.");
+            pw.println("  stop-app [--user <USER_ID> | all | current] <PACKAGE>");
+            pw.println("      Stop an app and all of its services.  Unlike `force-stop` this does");
+            pw.println("      not cancel the app's scheduled alarms and jobs.");
             pw.println("  crash [--user <USER_ID>] <PACKAGE|PID>");
             pw.println("      Induce a VM crash in the specified package or process");
             pw.println("  kill [--user <USER_ID> | all | current] <PACKAGE>");
diff --git a/services/core/java/com/android/server/am/AppBatteryTracker.java b/services/core/java/com/android/server/am/AppBatteryTracker.java
index c09bb2d..1a566a9 100644
--- a/services/core/java/com/android/server/am/AppBatteryTracker.java
+++ b/services/core/java/com/android/server/am/AppBatteryTracker.java
@@ -1258,6 +1258,16 @@
                 + "current_drain_event_duration_based_threshold_enabled";
 
         /**
+         * Whether or not we should move the app into the restricted bucket level if its background
+         * battery usage goes beyond the threshold. Note this different from the flag
+         * {@link AppRestrictionController.ConstantsObserver#KEY_BG_AUTO_RESTRICT_ABUSIVE_APPS}
+         * which is to control the overall auto bg restrictions.
+         */
+        static final String KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED =
+                DEVICE_CONFIG_SUBNAMESPACE_PREFIX
+                + "current_drain_auto_restrict_abusive_apps_enabled";
+
+        /**
          * The types of battery drain we're checking on each app; if the sum of the battery drain
          * exceeds the threshold, it'll be moved to restricted standby bucket; the type here
          * must be one of, or combination of {@link #BATTERY_USAGE_TYPE_BACKGROUND},
@@ -1353,6 +1363,11 @@
         final boolean mDefaultBgCurrentDrainEventDurationBasedThresholdEnabled;
 
         /**
+         * Default value to {@link #mBgCurrentDrainAutoRestrictAbusiveAppsEnabled}.
+         */
+        final boolean mDefaultBgCurrentDrainAutoRestrictAbusiveAppsEnabled;
+
+        /**
          * Default value to {@link #mBgCurrentDrainRestrictedBucketTypes}.
          */
         final int mDefaultCurrentDrainTypesToRestrictedBucket;
@@ -1430,6 +1445,11 @@
         volatile boolean mBgCurrentDrainEventDurationBasedThresholdEnabled;
 
         /**
+         * @see #KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED.
+         */
+        volatile boolean mBgCurrentDrainAutoRestrictAbusiveAppsEnabled;
+
+        /**
          * @see #KEY_BG_CURRENT_DRAIN_TYPES_TO_RESTRICTED_BUCKET.
          */
         volatile int mBgCurrentDrainRestrictedBucketTypes;
@@ -1520,6 +1540,8 @@
                     R.integer.config_bg_current_drain_location_min_duration) * 1_000;
             mDefaultBgCurrentDrainEventDurationBasedThresholdEnabled = resources.getBoolean(
                     R.bool.config_bg_current_drain_event_duration_based_threshold_enabled);
+            mDefaultBgCurrentDrainAutoRestrictAbusiveAppsEnabled = resources.getBoolean(
+                    R.bool.config_bg_current_drain_auto_restrict_abusive_apps);
             mDefaultCurrentDrainTypesToRestrictedBucket = resources.getInteger(
                     R.integer.config_bg_current_drain_types_to_restricted_bucket);
             mDefaultBgCurrentDrainTypesToBgRestricted = resources.getInteger(
@@ -1569,6 +1591,9 @@
                 case KEY_BG_CURRENT_DRAIN_POWER_COMPONENTS:
                     updateCurrentDrainThreshold();
                     break;
+                case KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED:
+                    updateBgCurrentDrainAutoRestrictAbusiveAppsEnabled();
+                    break;
                 case KEY_BG_CURRENT_DRAIN_WINDOW:
                     updateCurrentDrainWindow();
                     break;
@@ -1701,6 +1726,13 @@
                     DEFAULT_BG_CURRENT_DRAIN_DECOUPLE_THRESHOLD);
         }
 
+        private void updateBgCurrentDrainAutoRestrictAbusiveAppsEnabled() {
+            mBgCurrentDrainAutoRestrictAbusiveAppsEnabled = DeviceConfig.getBoolean(
+                    DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                    KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED,
+                    mDefaultBgCurrentDrainAutoRestrictAbusiveAppsEnabled);
+        }
+
         @Override
         public void onSystemReady() {
             mBatteryFullChargeMah =
@@ -1714,6 +1746,7 @@
             updateCurrentDrainEventDurationBasedThresholdEnabled();
             updateCurrentDrainExemptedTypes();
             updateCurrentDrainDecoupleThresholds();
+            updateBgCurrentDrainAutoRestrictAbusiveAppsEnabled();
         }
 
         @Override
@@ -1728,9 +1761,12 @@
                 if (pair != null) {
                     final long lastInteractionTime = mLastInteractionTime.get(uid, 0L);
                     final long[] ts = pair.first;
-                    final int restrictedLevel = ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET]
-                            > (lastInteractionTime + mBgCurrentDrainInteractionGracePeriodMs)
-                            && mTracker.mAppRestrictionController.isAutoRestrictAbusiveAppEnabled()
+                    final boolean noInteractionRecently = ts[TIME_STAMP_INDEX_RESTRICTED_BUCKET]
+                            > (lastInteractionTime + mBgCurrentDrainInteractionGracePeriodMs);
+                    final boolean canRestrict =
+                            mTracker.mAppRestrictionController.isAutoRestrictAbusiveAppEnabled()
+                            && mBgCurrentDrainAutoRestrictAbusiveAppsEnabled;
+                    final int restrictedLevel = noInteractionRecently && canRestrict
                             ? RESTRICTION_LEVEL_RESTRICTED_BUCKET
                             : RESTRICTION_LEVEL_ADAPTIVE_BUCKET;
                     if (maxLevel > RESTRICTION_LEVEL_BACKGROUND_RESTRICTED) {
@@ -2066,6 +2102,10 @@
                 pw.print('=');
                 pw.println(mBgCurrentDrainEventDurationBasedThresholdEnabled);
                 pw.print(prefix);
+                pw.print(KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED);
+                pw.print('=');
+                pw.println(mBgCurrentDrainAutoRestrictAbusiveAppsEnabled);
+                pw.print(prefix);
                 pw.print(KEY_BG_CURRENT_DRAIN_TYPES_TO_RESTRICTED_BUCKET);
                 pw.print('=');
                 pw.println(batteryUsageTypesToString(mBgCurrentDrainRestrictedBucketTypes));
diff --git a/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java
index 84969aa..702526a 100644
--- a/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java
+++ b/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java
@@ -320,12 +320,11 @@
     }
 
     @Override
-    public Future<?> scheduleSyncDueToProcessStateChange(long delayMillis) {
+    public void scheduleSyncDueToProcessStateChange(int flags, long delayMillis) {
         synchronized (BatteryExternalStatsWorker.this) {
             mProcessStateSync = scheduleDelayedSyncLocked(mProcessStateSync,
-                    () -> scheduleSync("procstate-change", UPDATE_ON_PROC_STATE_CHANGE),
+                    () -> scheduleSync("procstate-change", flags),
                     delayMillis);
-            return mProcessStateSync;
         }
     }
 
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index dd7fb84..d2e40c5 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -860,6 +860,21 @@
             }
         }
 
+        // Check that the receiver does *not* belong to any of the excluded packages
+        if (!skip && r.excludedPackages != null && r.excludedPackages.length > 0) {
+            if (ArrayUtils.contains(r.excludedPackages, filter.packageName)) {
+                Slog.w(TAG, "Skipping delivery of excluded package "
+                        + r.intent.toString()
+                        + " to " + filter.receiverList.app
+                        + " (pid=" + filter.receiverList.pid
+                        + ", uid=" + filter.receiverList.uid + ")"
+                        + " excludes package " + filter.packageName
+                        + " due to sender " + r.callerPackage
+                        + " (uid " + r.callingUid + ")");
+                skip = true;
+            }
+        }
+
         // If the broadcast also requires an app op check that as well.
         if (!skip && r.appOp != AppOpsManager.OP_NONE
                 && mService.getAppOpsManager().noteOpNoThrow(r.appOp,
@@ -1721,6 +1736,19 @@
             }
         }
 
+        // Check that the receiver does *not* belong to any of the excluded packages
+        if (!skip && r.excludedPackages != null && r.excludedPackages.length > 0) {
+            if (ArrayUtils.contains(r.excludedPackages, component.getPackageName())) {
+                Slog.w(TAG, "Skipping delivery of excluded package "
+                        + r.intent + " to "
+                        + component.flattenToShortString()
+                        + " excludes package " + component.getPackageName()
+                        + " due to sender " + r.callerPackage
+                        + " (uid " + r.callingUid + ")");
+                skip = true;
+            }
+        }
+
         if (!skip && info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID &&
                 r.requiredPermissions != null && r.requiredPermissions.length > 0) {
             for (int i = 0; i < r.requiredPermissions.length; i++) {
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 5343af2..19ffc17 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -75,6 +75,7 @@
     final String resolvedType; // the resolved data type
     final String[] requiredPermissions; // permissions the caller has required
     final String[] excludedPermissions; // permissions to exclude
+    final String[] excludedPackages; // packages to exclude
     final int appOp;        // an app op that is associated with this broadcast
     final BroadcastOptions options; // BroadcastOptions supplied by caller
     final List receivers;   // contains BroadcastFilter and ResolveInfo
@@ -162,6 +163,10 @@
             pw.print(prefix); pw.print("excludedPermissions=");
             pw.print(Arrays.toString(excludedPermissions));
         }
+        if (excludedPackages != null && excludedPackages.length > 0) {
+            pw.print(prefix); pw.print("excludedPackages=");
+            pw.print(Arrays.toString(excludedPackages));
+        }
         if (options != null) {
             pw.print(prefix); pw.print("options="); pw.println(options.toBundle());
         }
@@ -260,7 +265,8 @@
             Intent _intent, ProcessRecord _callerApp, String _callerPackage,
             @Nullable String _callerFeatureId, int _callingPid, int _callingUid,
             boolean _callerInstantApp, String _resolvedType,
-            String[] _requiredPermissions, String[] _excludedPermissions, int _appOp,
+            String[] _requiredPermissions, String[] _excludedPermissions,
+            String[] _excludedPackages, int _appOp,
             BroadcastOptions _options, List _receivers, IIntentReceiver _resultTo, int _resultCode,
             String _resultData, Bundle _resultExtras, boolean _serialized, boolean _sticky,
             boolean _initialSticky, int _userId, boolean allowBackgroundActivityStarts,
@@ -280,6 +286,7 @@
         resolvedType = _resolvedType;
         requiredPermissions = _requiredPermissions;
         excludedPermissions = _excludedPermissions;
+        excludedPackages = _excludedPackages;
         appOp = _appOp;
         options = _options;
         receivers = _receivers;
@@ -321,6 +328,7 @@
         resolvedType = from.resolvedType;
         requiredPermissions = from.requiredPermissions;
         excludedPermissions = from.excludedPermissions;
+        excludedPackages = from.excludedPackages;
         appOp = from.appOp;
         options = from.options;
         receivers = from.receivers;
@@ -381,9 +389,10 @@
         // build a new BroadcastRecord around that single-target list
         BroadcastRecord split = new BroadcastRecord(queue, intent, callerApp, callerPackage,
                 callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
-                requiredPermissions, excludedPermissions, appOp, options, splitReceivers, resultTo,
-                resultCode, resultData, resultExtras, ordered, sticky, initialSticky, userId,
-                allowBackgroundActivityStarts, mBackgroundActivityStartsToken, timeoutExempt);
+                requiredPermissions, excludedPermissions, excludedPackages, appOp, options,
+                splitReceivers, resultTo, resultCode, resultData, resultExtras, ordered, sticky,
+                initialSticky, userId, allowBackgroundActivityStarts,
+                mBackgroundActivityStartsToken, timeoutExempt);
         split.enqueueTime = this.enqueueTime;
         split.enqueueRealTime = this.enqueueRealTime;
         split.enqueueClockTime = this.enqueueClockTime;
@@ -459,7 +468,7 @@
         for (int i = 0; i < uidSize; i++) {
             final BroadcastRecord br = new BroadcastRecord(queue, intent, callerApp, callerPackage,
                     callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
-                    requiredPermissions, excludedPermissions, appOp, options,
+                    requiredPermissions, excludedPermissions, excludedPackages, appOp, options,
                     uid2receiverList.valueAt(i), null /* _resultTo */,
                     resultCode, resultData, resultExtras, ordered, sticky, initialSticky, userId,
                     allowBackgroundActivityStarts, mBackgroundActivityStartsToken, timeoutExempt);
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index e49497e..c88d82c 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -341,10 +341,24 @@
                 }
     };
 
+    // Compaction Stats
     private int mSomeCompactionCount;
     private int mFullCompactionCount;
     private int mPersistentCompactionCount;
     private int mBfgsCompactionCount;
+    private long mSomeCompactRequest;
+    private long mFullCompactRequest;
+    private long mPersistentCompactRequest;
+    private long mBfgsCompactRequest;
+    private long mProcCompactionsRequested;
+    private long mProcCompactionsPerformed;
+    private long mProcCompactionsNoPidThrottled;
+    private long mProcCompactionsOomAdjThrottled;
+    private long mProcCompactionsTimeThrottled;
+    private long mProcCompactionsRSSThrottled;
+    private long mProcCompactionsMiscThrottled;
+    private long mSystemCompactionsPerformed;
+
     private final ProcessDependencies mProcessDependencies;
     private final ProcLocksReader mProcLocksReader;
 
@@ -436,9 +450,31 @@
             pw.println("  "  + KEY_COMPACT_PROC_STATE_THROTTLE + "="
                     + Arrays.toString(mProcStateThrottle.toArray(new Integer[0])));
 
-            pw.println("  " + mSomeCompactionCount + " some, " + mFullCompactionCount
+            pw.println(" Requested:  " + mSomeCompactRequest + " some, " + mFullCompactRequest
+                    + " full, " + mPersistentCompactRequest + " persistent, "
+                    + mBfgsCompactRequest + " BFGS compactions.");
+            pw.println(" Performed: " + mSomeCompactionCount + " some, " + mFullCompactionCount
                     + " full, " + mPersistentCompactionCount + " persistent, "
                     + mBfgsCompactionCount + " BFGS compactions.");
+            pw.println(" Process Compactions Requested: " + mProcCompactionsRequested);
+            pw.println(" Process Compactions Performed: " + mProcCompactionsPerformed);
+            long compactionsThrottled = mProcCompactionsRequested - mProcCompactionsPerformed;
+            pw.println(" Process Compactions Throttled: " + compactionsThrottled);
+            double compactThrottlePercentage =
+                    (compactionsThrottled / (double) mProcCompactionsRequested) * 100.0;
+            pw.println(" Process Compactions Throttle Percentage: " + compactThrottlePercentage);
+            pw.println("        NoPid Throttled: " + mProcCompactionsNoPidThrottled);
+            pw.println("        OomAdj Throttled: " + mProcCompactionsOomAdjThrottled);
+            pw.println("        Time Throttled: " + mProcCompactionsTimeThrottled);
+            pw.println("        RSS Throttled: " + mProcCompactionsRSSThrottled);
+            pw.println("        Misc Throttled: " + mProcCompactionsMiscThrottled);
+            long unaccountedThrottled = compactionsThrottled - mProcCompactionsNoPidThrottled
+                    - mProcCompactionsOomAdjThrottled - mProcCompactionsTimeThrottled
+                    - mProcCompactionsRSSThrottled - mProcCompactionsMiscThrottled;
+            // Any throttle that was not part of the previous categories
+            pw.println("        Unaccounted Throttled: " + unaccountedThrottled);
+
+            pw.println(" System Compactions Performed: " + mSystemCompactionsPerformed);
 
             pw.println("  Tracking last compaction stats for " + mLastCompactionStats.size()
                     + " processes.");
@@ -479,6 +515,7 @@
     @GuardedBy("mProcLock")
     void compactAppSome(ProcessRecord app, boolean force) {
         app.mOptRecord.setReqCompactAction(COMPACT_PROCESS_SOME);
+        ++mSomeCompactRequest;
         compactApp(app, force, "some");
     }
 
@@ -519,6 +556,7 @@
                     " compactAppFull requested for " + app.processName + " force: " + force
                             + " oomAdjEnteredCached: " + oomAdjEnteredCached);
         }
+        ++mFullCompactRequest;
         // Apply OOM adj score throttle for Full App Compaction.
         if (force || oomAdjEnteredCached) {
             app.mOptRecord.setReqCompactAction(COMPACT_PROCESS_FULL);
@@ -535,6 +573,7 @@
     @GuardedBy("mProcLock")
     void compactAppPersistent(ProcessRecord app) {
         app.mOptRecord.setReqCompactAction(COMPACT_PROCESS_PERSISTENT);
+        ++mPersistentCompactRequest;
         compactApp(app, false, "Persistent");
     }
 
@@ -573,6 +612,7 @@
 
     @GuardedBy("mProcLock")
     void compactAppBfgs(ProcessRecord app) {
+        ++mBfgsCompactRequest;
         app.mOptRecord.setReqCompactAction(COMPACT_PROCESS_BFGS);
         compactApp(app, false, " Bfgs");
     }
@@ -1195,7 +1235,8 @@
                 && (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
+        } else if (oldAdj < ProcessList.CACHED_APP_MIN_ADJ
+                && newAdj >= ProcessList.CACHED_APP_MIN_ADJ
                 && newAdj <= ProcessList.CACHED_APP_MAX_ADJ) {
             // Perform a major compaction when any app enters cached
             compactAppFull(app, false);
@@ -1223,14 +1264,16 @@
                 break;
         }
 
-        // Downgrade compaction if facing swap memory pressure
+        // Downgrade compaction under swap memory pressure
         if (resolvedAction == COMPACT_ACTION_FULL) {
-            double swapUsagePercent = getFreeSwapPercent();
-            if (swapUsagePercent < COMPACT_DOWNGRADE_FREE_SWAP_THRESHOLD) {
-                Slog.d(TAG_AM,
-                        "Downgraded compaction to file only due to low swap."
-                                + " Swap Free% " + swapUsagePercent);
+            double swapFreePercent = getFreeSwapPercent();
+            if (swapFreePercent < COMPACT_DOWNGRADE_FREE_SWAP_THRESHOLD) {
                 resolvedAction = COMPACT_ACTION_FILE;
+                if (DEBUG_COMPACTION) {
+                    Slog.d(TAG_AM,
+                            "Downgraded compaction to file only due to low swap."
+                                    + " Swap Free% " + swapFreePercent);
+                }
             }
         }
 
@@ -1445,28 +1488,33 @@
                         lastCompactTime = opt.getLastCompactTime();
                     }
 
-                    int resolvedAction = resolveCompactionAction(requestedAction);
+                    ++mProcCompactionsRequested;
                     long[] rssBefore;
                     if (pid == 0) {
                         // not a real process, either one being launched or one being killed
                         if (DEBUG_COMPACTION) {
                             Slog.d(TAG_AM, "Compaction failed, pid is 0");
                         }
+                        ++mProcCompactionsNoPidThrottled;
                         return;
                     }
 
                     if (!forceCompaction) {
-                        if (shouldOomAdjThrottleCompaction(proc, resolvedAction)) {
+                        if (shouldOomAdjThrottleCompaction(proc, requestedAction)) {
+                            ++mProcCompactionsOomAdjThrottled;
                             return;
                         }
                         if (shouldTimeThrottleCompaction(proc, start, requestedAction)) {
+                            ++mProcCompactionsTimeThrottled;
                             return;
                         }
-                        if (shouldThrottleMiscCompaction(proc, procState, resolvedAction)) {
+                        if (shouldThrottleMiscCompaction(proc, procState, requestedAction)) {
+                            ++mProcCompactionsMiscThrottled;
                             return;
                         }
                         rssBefore = mProcessDependencies.getRss(pid);
-                        if (shouldRssThrottleCompaction(resolvedAction, pid, name, rssBefore)) {
+                        if (shouldRssThrottleCompaction(requestedAction, pid, name, rssBefore)) {
+                            ++mProcCompactionsRSSThrottled;
                             return;
                         }
                     } else {
@@ -1494,11 +1542,14 @@
                         default:
                             break;
                     }
+
+                    int resolvedAction = resolveCompactionAction(requestedAction);
                     action = compactActionIntToString(resolvedAction);
 
                     try {
                         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                                 "Compact " + action + ": " + name);
+                        ++mProcCompactionsPerformed;
                         long zramFreeKbBefore = Debug.getZramFreeKb();
                         mProcessDependencies.performCompaction(action, pid);
                         long[] rssAfter = mProcessDependencies.getRss(pid);
@@ -1550,6 +1601,7 @@
                     break;
                 }
                 case COMPACT_SYSTEM_MSG: {
+                    ++mSystemCompactionsPerformed;
                     Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "compactSystem");
                     compactSystem();
                     Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
diff --git a/services/core/java/com/android/server/am/ComponentAliasResolver.java b/services/core/java/com/android/server/am/ComponentAliasResolver.java
index 2db3b15..01735a7 100644
--- a/services/core/java/com/android/server/am/ComponentAliasResolver.java
+++ b/services/core/java/com/android/server/am/ComponentAliasResolver.java
@@ -483,7 +483,7 @@
     @Nullable
     public Resolution<ResolveInfo> resolveReceiver(@NonNull Intent intent,
             @NonNull ResolveInfo receiver, @Nullable String resolvedType,
-            int packageFlags, int userId, int callingUid) {
+            int packageFlags, int userId, int callingUid, boolean forSend) {
         // Resolve this alias.
         final Resolution<ComponentName> resolution = resolveComponentAlias(() ->
                 receiver.activityInfo.getComponentName());
@@ -506,8 +506,8 @@
         i.setPackage(null);
         i.setComponent(resolution.getTarget());
 
-        List<ResolveInfo> resolved = pmi.queryIntentReceivers(i,
-                resolvedType, packageFlags, callingUid, userId);
+        List<ResolveInfo> resolved = pmi.queryIntentReceivers(
+                i, resolvedType, packageFlags, callingUid, userId, forSend);
         if (resolved == null || resolved.size() == 0) {
             // Target component not found.
             Slog.w(TAG, "Alias target " + target.flattenToShortString() + " not found");
diff --git a/services/core/java/com/android/server/am/PreBootBroadcaster.java b/services/core/java/com/android/server/am/PreBootBroadcaster.java
index 7562098..35f91ba 100644
--- a/services/core/java/com/android/server/am/PreBootBroadcaster.java
+++ b/services/core/java/com/android/server/am/PreBootBroadcaster.java
@@ -124,7 +124,7 @@
                 REASON_PRE_BOOT_COMPLETED, "");
         synchronized (mService) {
             mService.broadcastIntentLocked(null, null, null, mIntent, null, this, 0, null, null,
-                    null, null, AppOpsManager.OP_NONE, bOptions.toBundle(), true,
+                    null, null, null, AppOpsManager.OP_NONE, bOptions.toBundle(), true,
                     false, ActivityManagerService.MY_PID,
                     Process.SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(), mUserId);
         }
diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java
index b4ff870..ac5cb2d 100644
--- a/services/core/java/com/android/server/am/ProcessRecord.java
+++ b/services/core/java/com/android/server/am/ProcessRecord.java
@@ -87,6 +87,7 @@
     final String processName;   // name of the process
     final String sdkSandboxClientAppPackage; // if this is an sdk sandbox process, name of the
                                              // app package for which it is running
+    final String sdkSandboxClientAppVolumeUuid; // uuid of the app for which the sandbox is running
 
     /**
      * Overall state of process's uid.
@@ -535,6 +536,11 @@
         userId = UserHandle.getUserId(_uid);
         processName = _processName;
         sdkSandboxClientAppPackage = _sdkSandboxClientAppPackage;
+        if (isSdkSandbox) {
+            sdkSandboxClientAppVolumeUuid = getClientInfoForSdkSandbox().volumeUuid;
+        } else {
+            sdkSandboxClientAppVolumeUuid = null;
+        }
         mPersistent = false;
         mRemoved = false;
         mProfile = new ProcessProfileRecord(this);
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index c043773..7ffea26 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -3203,8 +3203,8 @@
             synchronized (mService) {
                 return mService.broadcastIntentLocked(null, null, null, intent, resolvedType,
                         resultTo, resultCode, resultData, resultExtras, requiredPermissions, null,
-                        appOp, bOptions, ordered, sticky, callingPid, callingUid, realCallingUid,
-                        realCallingPid, userId);
+                        null, appOp, bOptions, ordered, sticky, callingPid, callingUid,
+                        realCallingUid, realCallingPid, userId);
             }
         }
 
diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationService.java b/services/core/java/com/android/server/apphibernation/AppHibernationService.java
index d239c02..27ce493 100644
--- a/services/core/java/com/android/server/apphibernation/AppHibernationService.java
+++ b/services/core/java/com/android/server/apphibernation/AppHibernationService.java
@@ -501,6 +501,7 @@
                     null /* resultExtras */,
                     requiredPermissions,
                     null /* excludedPermissions */,
+                    null /* excludedPackages */,
                     OP_NONE,
                     null /* bOptions */,
                     false /* serialized */,
@@ -519,6 +520,7 @@
                     null /* resultExtras */,
                     requiredPermissions,
                     null /* excludedPermissions */,
+                    null /* excludedPackages */,
                     OP_NONE,
                     null /* bOptions */,
                     false /* serialized */,
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 36afb36..b56654f 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -43,6 +43,7 @@
 import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
 import static android.app.AppOpsManager.OP_NONE;
 import static android.app.AppOpsManager.OP_PLAY_AUDIO;
+import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD;
 import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED;
@@ -2368,7 +2369,8 @@
         ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class);
         boolean isSelfRequest = (filter & FILTER_BY_UID) != 0 && uid == Binder.getCallingUid();
         if (!isSelfRequest) {
-            boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(Binder.getCallingUid());
+            boolean isCallerInstrumented =
+                    ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID;
             boolean isCallerSystem = Binder.getCallingPid() == Process.myPid();
             boolean isCallerPermissionController;
             try {
@@ -3256,7 +3258,7 @@
             return AppOpsManager.MODE_IGNORED;
         }
         synchronized (this) {
-            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass)) {
+            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, true)) {
                 return AppOpsManager.MODE_IGNORED;
             }
             code = AppOpsManager.opToSwitch(code);
@@ -3481,7 +3483,7 @@
 
             final int switchCode = AppOpsManager.opToSwitch(code);
             final UidState uidState = ops.uidState;
-            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass)) {
+            if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, false)) {
                 attributedOp.rejected(uidState.state, flags);
                 scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags,
                         AppOpsManager.MODE_IGNORED);
@@ -3848,7 +3850,7 @@
         // the data gated by OP_RECORD_AUDIO.
         //
         // TODO: Revert this change before Android 12.
-        if (code == OP_RECORD_AUDIO_HOTWORD) {
+        if (code == OP_RECORD_AUDIO_HOTWORD || code == OP_RECEIVE_AMBIENT_TRIGGER_AUDIO) {
             int result = checkOperation(OP_RECORD_AUDIO, uid, packageName);
             if (result != AppOpsManager.MODE_ALLOWED) {
                 return new SyncNotedAppOp(result, code, attributionTag, packageName);
@@ -3995,7 +3997,8 @@
             final Op op = getOpLocked(ops, code, uid, true);
             final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
             final UidState uidState = ops.uidState;
-            isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass);
+            isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass,
+                    false);
             final int switchCode = AppOpsManager.opToSwitch(code);
             // If there is a non-default per UID policy (we set UID op mode only if
             // non-default) it takes over, otherwise use the per package policy.
@@ -4832,7 +4835,7 @@
     }
 
     private boolean isOpRestrictedLocked(int uid, int code, String packageName,
-            String attributionTag, @Nullable RestrictionBypass appBypass) {
+            String attributionTag, @Nullable RestrictionBypass appBypass, boolean isCheckOp) {
         int restrictionSetCount = mOpGlobalRestrictions.size();
 
         for (int i = 0; i < restrictionSetCount; i++) {
@@ -4849,7 +4852,8 @@
             // For each client, check that the given op is not restricted, or that the given
             // package is exempt from the restriction.
             ClientUserRestrictionState restrictionState = mOpUserRestrictions.valueAt(i);
-            if (restrictionState.hasRestriction(code, packageName, attributionTag, userHandle)) {
+            if (restrictionState.hasRestriction(code, packageName, attributionTag, userHandle,
+                    isCheckOp)) {
                 RestrictionBypass opBypass = opAllowSystemBypassRestriction(code);
                 if (opBypass != null) {
                     // If we are the system, bypass user restrictions for certain codes
@@ -6924,7 +6928,8 @@
     @Override
     public @Nullable RuntimeAppOpAccessMessage collectRuntimeAppOpAccessMessage() {
         ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class);
-        boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(Binder.getCallingUid());
+        boolean isCallerInstrumented =
+                ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID;
         boolean isCallerSystem = Binder.getCallingPid() == Process.myPid();
         if (!isCallerSystem && !isCallerInstrumented) {
             return null;
@@ -7221,7 +7226,7 @@
         }
 
         public boolean hasRestriction(int restriction, String packageName, String attributionTag,
-                int userId) {
+                int userId, boolean isCheckOp) {
             if (perUserRestrictions == null) {
                 return false;
             }
@@ -7240,6 +7245,9 @@
                 return true;
             }
 
+            if (isCheckOp) {
+                return !perUserExclusions.includes(packageName);
+            }
             return !perUserExclusions.contains(packageName, attributionTag);
         }
 
@@ -7406,7 +7414,8 @@
                 int numRestrictions = mOpUserRestrictions.size();
                 for (int i = 0; i < numRestrictions; i++) {
                     if (mOpUserRestrictions.valueAt(i)
-                            .hasRestriction(code, pkg, attributionTag, user.getIdentifier())) {
+                            .hasRestriction(code, pkg, attributionTag, user.getIdentifier(),
+                                    false)) {
                         number++;
                     }
                 }
diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java
index 8de515d..158092f 100644
--- a/services/core/java/com/android/server/appop/DiscreteRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java
@@ -34,6 +34,7 @@
 import static android.app.AppOpsManager.OP_NONE;
 import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA;
 import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE;
+import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
 import static android.app.AppOpsManager.flagsToString;
 import static android.app.AppOpsManager.getUidStateName;
@@ -133,7 +134,7 @@
     private static final String PROPERTY_DISCRETE_OPS_LIST = "discrete_history_ops_cslist";
     private static final String DEFAULT_DISCRETE_OPS = OP_FINE_LOCATION + "," + OP_COARSE_LOCATION
             + "," + OP_CAMERA + "," + OP_RECORD_AUDIO + "," + OP_PHONE_CALL_MICROPHONE + ","
-            + OP_PHONE_CALL_CAMERA;
+            + OP_PHONE_CALL_CAMERA + "," + OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
     private static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis();
     private static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis();
     private static final long DEFAULT_DISCRETE_HISTORY_QUANTIZATION =
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index aed63ce..f0fe2dd 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -341,8 +341,7 @@
     private static final int MSG_DISPATCH_AUDIO_MODE = 40;
     private static final int MSG_ROUTING_UPDATED = 41;
     private static final int MSG_INIT_HEADTRACKING_SENSORS = 42;
-    // commented out for now, will be reused for other SA persisting
-    //private static final int MSG_PERSIST_SPATIAL_AUDIO_ENABLED = 43;
+    private static final int MSG_PERSIST_SPATIAL_AUDIO_DEVICE_SETTINGS = 43;
     private static final int MSG_ADD_ASSISTANT_SERVICE_UID = 44;
     private static final int MSG_REMOVE_ASSISTANT_SERVICE_UID = 45;
     private static final int MSG_UPDATE_ACTIVE_ASSISTANT_SERVICE_UID = 46;
@@ -4146,19 +4145,8 @@
     {
         streamType = mStreamVolumeAlias[streamType];
 
-        if (streamType == AudioSystem.STREAM_MUSIC) {
-            flags = updateFlagsForTvPlatform(flags);
-            synchronized (mHdmiClientLock) {
-                // Don't display volume UI on a TV Playback device when using absolute volume
-                if (mHdmiCecVolumeControlEnabled && mHdmiPlaybackClient != null
-                        && (isAbsoluteVolumeDevice(device)
-                        || isA2dpAbsoluteVolumeDevice(device))) {
-                    flags &= ~AudioManager.FLAG_SHOW_UI;
-                }
-            }
-            if (isFullVolumeDevice(device)) {
-                flags &= ~AudioManager.FLAG_SHOW_UI;
-            }
+        if (streamType == AudioSystem.STREAM_MUSIC && isFullVolumeDevice(device)) {
+            flags &= ~AudioManager.FLAG_SHOW_UI;
         }
         mVolumeController.postVolumeChanged(streamType, flags);
     }
@@ -6648,6 +6636,11 @@
         // verify arguments
         Objects.requireNonNull(device);
         AudioManager.enforceValidVolumeBehavior(deviceVolumeBehavior);
+        sVolumeLogger.log(new AudioEventLogger.StringEvent("setDeviceVolumeBehavior: dev:"
+                + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:"
+                + device.getAddress() + " behavior:"
+                + AudioDeviceVolumeManager.volumeBehaviorName(deviceVolumeBehavior)
+                + " pack:" + pkgName).printLog(TAG));
         if (pkgName == null) {
             pkgName = "";
         }
@@ -8128,8 +8121,7 @@
                     break;
 
                 case MSG_INIT_SPATIALIZER:
-                    mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect);
-                    mSpatializerHelper.setFeatureEnabled(mHasSpatializerEffect);
+                    onInitSpatializer();
                     mAudioEventWakeLock.release();
                     break;
 
@@ -8137,6 +8129,10 @@
                     mSpatializerHelper.onInitSensors();
                     break;
 
+                case MSG_PERSIST_SPATIAL_AUDIO_DEVICE_SETTINGS:
+                    onPersistSpatialAudioDeviceSettings();
+                    break;
+
                 case MSG_CHECK_MUSIC_ACTIVE:
                     onCheckMusicActive((String) msg.obj);
                     break;
@@ -8363,7 +8359,7 @@
     private void avrcpSupportsAbsoluteVolume(String address, boolean support) {
         // address is not used for now, but may be used when multiple a2dp devices are supported
         sVolumeLogger.log(new AudioEventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr="
-                + address + " support=" + support));
+                + address + " support=" + support).printLog(TAG));
         mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support);
         setAvrcpAbsoluteVolumeSupported(support);
     }
@@ -9105,6 +9101,45 @@
                 /*arg1*/ 0, /*arg2*/ 0, TAG, /*delay*/ 0);
     }
 
+    void onInitSpatializer() {
+        final String settings = mSettings.getSecureStringForUser(mContentResolver,
+                Settings.Secure.SPATIAL_AUDIO_ENABLED, UserHandle.USER_CURRENT);
+        if (settings == null) {
+            Log.e(TAG, "error reading spatial audio device settings");
+        } else {
+            Log.v(TAG, "restoring spatial audio device settings: " + settings);
+            mSpatializerHelper.setSADeviceSettings(settings);
+        }
+        mSpatializerHelper.init(/*effectExpected*/ mHasSpatializerEffect);
+        mSpatializerHelper.setFeatureEnabled(mHasSpatializerEffect);
+    }
+
+    /**
+     * post a message to persist the spatial audio device settings.
+     * Message is delayed by 1s on purpose in case of successive changes in quick succession (at
+     * init time for instance)
+     * Note this method is made public to work around a Mockito bug where it needs to be public
+     * in order to be mocked by a test a the same package
+     * (see https://code.google.com/archive/p/mockito/issues/127)
+     */
+    public void persistSpatialAudioDeviceSettings() {
+        sendMsg(mAudioHandler,
+                MSG_PERSIST_SPATIAL_AUDIO_DEVICE_SETTINGS,
+                SENDMSG_REPLACE, /*arg1*/ 0, /*arg2*/ 0, TAG,
+                /*delay*/ 1000);
+    }
+
+    void onPersistSpatialAudioDeviceSettings() {
+        final String settings = mSpatializerHelper.getSADeviceSettings();
+        Log.v(TAG, "saving spatial audio device settings: " + settings);
+        boolean res = mSettings.putSecureStringForUser(mContentResolver,
+                Settings.Secure.SPATIAL_AUDIO_ENABLED,
+                settings, UserHandle.USER_CURRENT);
+        if (!res) {
+            Log.e(TAG, "error saving spatial audio device settings: " + settings);
+        }
+    }
+
     //==========================================================================================
     private boolean readCameraSoundForced() {
         return SystemProperties.getBoolean("audio.camerasound.force", false) ||
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 4ac611b..c61bad4 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -40,6 +40,7 @@
 import android.media.SpatializerHeadTrackingMode;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseIntArray;
@@ -48,6 +49,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.UUID;
 
 /**
@@ -70,12 +72,6 @@
     private @Nullable SensorManager mSensorManager;
 
     //------------------------------------------------------------
-    /** head tracker sensor name */
-    // TODO: replace with generic head tracker sensor name.
-    //       the current implementation refers to the "google" namespace but will be replaced
-    //       by an android name at the next API level revision, it is not Google-specific.
-    private static final String HEADTRACKER_SENSOR =
-            "com.google.hardware.sensor.hid_dynamic.headtracker";
 
     private static final SparseIntArray SPAT_MODE_FOR_DEVICE_TYPE = new SparseIntArray(15) {
         {
@@ -247,6 +243,9 @@
                 }
                 mActualHeadTrackingMode =
                         headTrackingModeTypeToSpatializerInt(spat.getActualHeadTrackingMode());
+            } else {
+                mDesiredHeadTrackingModeWhenEnabled = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
+                mDesiredHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
             }
 
             byte[] spatModes = spat.getSupportedModes();
@@ -281,18 +280,17 @@
             // for both transaural / binaural, we are not forcing enablement as the init() method
             // could have been called another time after boot in case of audioserver restart
             if (mTransauralSupported) {
-                // TODO deal with persisted values
+                // not force-enabling as this device might already be in the device list
                 addCompatibleAudioDevice(
                         new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""),
                                 false /*forceEnable*/);
             }
             if (mBinauralSupported) {
-                // TODO deal with persisted values
+                // not force-enabling as this device might already be in the device list
                 addCompatibleAudioDevice(
                         new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""),
                                 false /*forceEnable*/);
             }
-            // TODO read persisted states
         } catch (RemoteException e) {
             resetCapabilities();
         } finally {
@@ -539,11 +537,12 @@
         }
         if (!isInList) {
             final SADeviceState dev = new SADeviceState(deviceType,
-                    wireless ? ada.getAddress() : null);
+                    wireless ? ada.getAddress() : "");
             dev.mEnabled = true;
             mSADevices.add(dev);
         }
         onRoutingUpdated();
+        mAudioService.persistSpatialAudioDeviceSettings();
     }
 
     synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
@@ -559,6 +558,7 @@
             }
         }
         onRoutingUpdated();
+        mAudioService.persistSpatialAudioDeviceSettings();
     }
 
     /**
@@ -631,7 +631,7 @@
         }
         if (!knownDevice) {
             mSADevices.add(new SADeviceState(ada.getType(), ada.getAddress()));
-            //### TODO persist list
+            mAudioService.persistSpatialAudioDeviceSettings();
         }
     }
 
@@ -1051,6 +1051,10 @@
     }
 
     synchronized void setHeadTrackerEnabled(boolean enabled, @NonNull AudioDeviceAttributes ada) {
+        if (!mIsHeadTrackingSupported) {
+            Log.v(TAG, "no headtracking support, ignoring setHeadTrackerEnabled to " + enabled
+                    + " for " + ada);
+        }
         final int deviceType = ada.getType();
         final boolean wireless = isWireless(deviceType);
 
@@ -1065,6 +1069,7 @@
                 }
                 Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada);
                 deviceState.mHeadTrackerEnabled = enabled;
+                mAudioService.persistSpatialAudioDeviceSettings();
                 break;
             }
         }
@@ -1077,6 +1082,10 @@
     }
 
     synchronized boolean hasHeadTracker(@NonNull AudioDeviceAttributes ada) {
+        if (!mIsHeadTrackingSupported) {
+            Log.v(TAG, "no headtracking support, hasHeadTracker always false for " + ada);
+            return false;
+        }
         final int deviceType = ada.getType();
         final boolean wireless = isWireless(deviceType);
 
@@ -1096,6 +1105,10 @@
      * @return true if the head tracker is enabled, false otherwise or if device not found
      */
     synchronized boolean setHasHeadTracker(@NonNull AudioDeviceAttributes ada) {
+        if (!mIsHeadTrackingSupported) {
+            Log.v(TAG, "no headtracking support, setHasHeadTracker always false for " + ada);
+            return false;
+        }
         final int deviceType = ada.getType();
         final boolean wireless = isWireless(deviceType);
 
@@ -1103,7 +1116,10 @@
             if (deviceType == deviceState.mDeviceType
                     && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
                     || !wireless) {
-                deviceState.mHasHeadTracker = true;
+                if (!deviceState.mHasHeadTracker) {
+                    deviceState.mHasHeadTracker = true;
+                    mAudioService.persistSpatialAudioDeviceSettings();
+                }
                 return deviceState.mHeadTrackerEnabled;
             }
         }
@@ -1112,6 +1128,10 @@
     }
 
     synchronized boolean isHeadTrackerEnabled(@NonNull AudioDeviceAttributes ada) {
+        if (!mIsHeadTrackingSupported) {
+            Log.v(TAG, "no headtracking support, isHeadTrackerEnabled always false for " + ada);
+            return false;
+        }
         final int deviceType = ada.getType();
         final boolean wireless = isWireless(deviceType);
 
@@ -1463,16 +1483,45 @@
         }
     }
 
-    private static final class SADeviceState {
+    /*package*/ static final class SADeviceState {
         final @AudioDeviceInfo.AudioDeviceType int mDeviceType;
-        final @Nullable String mDeviceAddress; // non-null for wireless devices
+        final @NonNull String mDeviceAddress;
         boolean mEnabled = true;               // by default, SA is enabled on any device
         boolean mHasHeadTracker = false;
         boolean mHeadTrackerEnabled = true;    // by default, if head tracker is present, use it
+        static final String SETTING_FIELD_SEPARATOR = ",";
+        static final String SETTING_DEVICE_SEPARATOR_CHAR = "|";
+        static final String SETTING_DEVICE_SEPARATOR = "\\|";
 
-        SADeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, @Nullable String address) {
+        SADeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, @NonNull String address) {
             mDeviceType = deviceType;
-            mDeviceAddress = address;
+            mDeviceAddress = Objects.requireNonNull(address);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            // type check and cast
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            final SADeviceState sads = (SADeviceState) obj;
+            return mDeviceType == sads.mDeviceType
+                    && mDeviceAddress.equals(sads.mDeviceAddress)
+                    && mEnabled == sads.mEnabled
+                    && mHasHeadTracker == sads.mHasHeadTracker
+                    && mHeadTrackerEnabled == sads.mHeadTrackerEnabled;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mDeviceType, mDeviceAddress, mEnabled, mHasHeadTracker,
+                    mHeadTrackerEnabled);
         }
 
         @Override
@@ -1480,6 +1529,64 @@
             return "type:" + mDeviceType + " addr:" + mDeviceAddress + " enabled:" + mEnabled
                     + " HT:" + mHasHeadTracker + " HTenabled:" + mHeadTrackerEnabled;
         }
+
+        String toPersistableString() {
+            return (new StringBuilder().append(mDeviceType)
+                    .append(SETTING_FIELD_SEPARATOR).append(mDeviceAddress)
+                    .append(SETTING_FIELD_SEPARATOR).append(mEnabled ? "1" : "0")
+                    .append(SETTING_FIELD_SEPARATOR).append(mHasHeadTracker ? "1" : "0")
+                    .append(SETTING_FIELD_SEPARATOR).append(mHeadTrackerEnabled ? "1" : "0")
+                    .toString());
+        }
+
+        static @Nullable SADeviceState fromPersistedString(@Nullable String persistedString) {
+            if (persistedString == null) {
+                return null;
+            }
+            if (persistedString.isEmpty()) {
+                return null;
+            }
+            String[] fields = TextUtils.split(persistedString, SETTING_FIELD_SEPARATOR);
+            if (fields.length != 5) {
+                // expecting all fields, fewer may mean corruption, ignore those settings
+                return null;
+            }
+            try {
+                final int deviceType = Integer.parseInt(fields[0]);
+                final SADeviceState deviceState = new SADeviceState(deviceType, fields[1]);
+                deviceState.mEnabled = Integer.parseInt(fields[2]) == 1;
+                deviceState.mHasHeadTracker = Integer.parseInt(fields[3]) == 1;
+                deviceState.mHeadTrackerEnabled = Integer.parseInt(fields[4]) == 1;
+                return deviceState;
+            } catch (NumberFormatException e) {
+                Log.e(TAG, "unable to parse setting for SADeviceState: " + persistedString, e);
+                return null;
+            }
+        }
+    }
+
+    /*package*/ synchronized String getSADeviceSettings() {
+        // expected max size of each String for each SADeviceState is 25 (accounting for separator)
+        final StringBuilder settingsBuilder = new StringBuilder(mSADevices.size() * 25);
+        for (int i = 0; i < mSADevices.size(); i++) {
+            settingsBuilder.append(mSADevices.get(i).toPersistableString());
+            if (i != mSADevices.size() - 1) {
+                settingsBuilder.append(SADeviceState.SETTING_DEVICE_SEPARATOR_CHAR);
+            }
+        }
+        return settingsBuilder.toString();
+    }
+
+    /*package*/ synchronized void setSADeviceSettings(@NonNull String persistedSettings) {
+        String[] devSettings = TextUtils.split(Objects.requireNonNull(persistedSettings),
+                SADeviceState.SETTING_DEVICE_SEPARATOR);
+        // small list, not worth overhead of Arrays.stream(devSettings)
+        for (String setting : devSettings) {
+            SADeviceState devState = SADeviceState.fromPersistedString(setting);
+            if (devState != null) {
+                mSADevices.add(devState);
+            }
+        }
     }
 
     private static String spatStateString(int state) {
@@ -1522,24 +1629,24 @@
     private int getHeadSensorHandleUpdateTracker() {
         int headHandle = -1;
         UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(ROUTING_DEVICES[0]);
-        List<Sensor> sensors = new ArrayList<Sensor>(0);
-        sensors.addAll(mSensorManager.getDynamicSensorList(Sensor.TYPE_HEAD_TRACKER));
-        sensors.addAll(mSensorManager.getDynamicSensorList(Sensor.TYPE_DEVICE_PRIVATE_BASE));
+        // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion
+        // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR
+        // and Sensor.TYPE_GAME_ROTATION_VECTOR are supported internally by
+        // SensorPoseProvider).
+        // Note: this is a dynamic sensor list right now.
+        List<Sensor> sensors = mSensorManager.getDynamicSensorList(Sensor.TYPE_HEAD_TRACKER);
         for (Sensor sensor : sensors) {
-            if (sensor.getType() == Sensor.TYPE_HEAD_TRACKER
-                    || sensor.getStringType().equals(HEADTRACKER_SENSOR)) {
-                UUID uuid = sensor.getUuid();
-                if (uuid.equals(routingDeviceUuid)) {
-                    headHandle = sensor.getHandle();
-                    if (!setHasHeadTracker(ROUTING_DEVICES[0])) {
-                        headHandle = -1;
-                    }
-                    break;
+            final UUID uuid = sensor.getUuid();
+            if (uuid.equals(routingDeviceUuid)) {
+                headHandle = sensor.getHandle();
+                if (!setHasHeadTracker(ROUTING_DEVICES[0])) {
+                    headHandle = -1;
                 }
-                if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
-                    headHandle = sensor.getHandle();
-                    break;
-                }
+                break;
+            }
+            if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
+                headHandle = sensor.getHandle();
+                // we do not break, perhaps we find a head tracker on device.
             }
         }
         return headHandle;
@@ -1563,4 +1670,11 @@
         AudioService.sSpatialLogger.loglog(msg, AudioEventLogger.Event.ALOGE, TAG);
         return msg;
     }
+
+    //------------------------------------------------
+    // for testing purposes only
+
+    /*package*/ void clearSADevices() {
+        mSADevices.clear();
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index 0e2582c..0d75521 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -39,6 +39,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -544,4 +545,37 @@
                 throw new IllegalArgumentException("Unknown strength: " + strength);
         }
     }
+
+    /**
+     * Checks if a client package is running in the background.
+     *
+     * @param clientPackage The name of the package to be checked.
+     * @return Whether the client package is running in background
+     */
+    public static boolean isBackground(String clientPackage) {
+        Slog.v(TAG, "Checking if the authenticating is in background,"
+                + " clientPackage:" + clientPackage);
+        final List<ActivityManager.RunningTaskInfo> tasks =
+                ActivityTaskManager.getInstance().getTasks(Integer.MAX_VALUE);
+
+        if (tasks == null || tasks.isEmpty()) {
+            Slog.d(TAG, "No running tasks reported");
+            return true;
+        }
+
+        for (ActivityManager.RunningTaskInfo taskInfo : tasks) {
+            final ComponentName topActivity = taskInfo.topActivity;
+            if (topActivity != null) {
+                final String topPackage = topActivity.getPackageName();
+                if (topPackage.contentEquals(clientPackage) && taskInfo.isVisible()) {
+                    return false;
+                } else {
+                    Slog.i(TAG, "Running task, top: " + topPackage
+                            + ", isVisible: " + taskInfo.isVisible());
+                }
+            }
+        }
+
+        return true;
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index 1002229..4eb6d38 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -19,10 +19,8 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.TaskStackListener;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.hardware.biometrics.BiometricAuthenticator;
@@ -42,7 +40,6 @@
 import com.android.server.biometrics.log.BiometricLogger;
 
 import java.util.ArrayList;
-import java.util.List;
 import java.util.function.Supplier;
 
 /**
@@ -202,25 +199,7 @@
         if (!mAllowBackgroundAuthentication && authenticated
                 && !Utils.isKeyguard(getContext(), getOwnerString())
                 && !Utils.isSystem(getContext(), getOwnerString())) {
-            final List<ActivityManager.RunningTaskInfo> tasks =
-                    mActivityTaskManager.getTasks(1);
-            if (tasks == null || tasks.isEmpty()) {
-                Slog.e(TAG, "No running tasks reported");
-                isBackgroundAuth = true;
-            } else {
-                final ComponentName topActivity = tasks.get(0).topActivity;
-                if (topActivity == null) {
-                    Slog.e(TAG, "Unable to get top activity");
-                    isBackgroundAuth = true;
-                } else {
-                    final String topPackage = topActivity.getPackageName();
-                    if (!topPackage.contentEquals(getOwnerString())) {
-                        Slog.e(TAG, "Background authentication detected, top: " + topPackage
-                                + ", client: " + getOwnerString());
-                        isBackgroundAuth = true;
-                    }
-                }
-            }
+            isBackgroundAuth = Utils.isBackground(getOwnerString());
         }
 
         // Fail authentication if we can't confirm the client activity is on top.
@@ -465,7 +444,6 @@
     @Override
     public void cancel() {
         super.cancel();
-
         if (mTaskStackListener != null) {
             mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index 4e03ee9..e0900b9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -104,22 +104,17 @@
                         Slog.e(getTag(), "Task stack changed for client: " + client);
                         continue;
                     }
-                    if (Utils.isKeyguard(mContext, client.getOwnerString())) {
+                    if (Utils.isKeyguard(mContext, client.getOwnerString())
+                            || Utils.isSystem(mContext, client.getOwnerString())) {
                         continue; // Keyguard is always allowed
                     }
 
-                    final List<ActivityManager.RunningTaskInfo> runningTasks =
-                            mActivityTaskManager.getTasks(1);
-                    if (!runningTasks.isEmpty()) {
-                        final String topPackage =
-                                runningTasks.get(0).topActivity.getPackageName();
-                        if (!topPackage.contentEquals(client.getOwnerString())
-                                && !client.isAlreadyDone()) {
-                            Slog.e(getTag(), "Stopping background authentication, top: "
-                                    + topPackage + " currentClient: " + client);
-                            mSensors.valueAt(i).getScheduler().cancelAuthenticationOrDetection(
-                                    client.getToken(), client.getRequestId());
-                        }
+                    if (Utils.isBackground(client.getOwnerString())
+                            && !client.isAlreadyDone()) {
+                        Slog.e(getTag(), "Stopping background authentication,"
+                                + " currentClient: " + client);
+                        mSensors.valueAt(i).getScheduler().cancelAuthenticationOrDetection(
+                                client.getToken(), client.getRequestId());
                     }
                 }
             });
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
index de0a36a..bf7a62a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceResetLockoutClient.java
@@ -88,6 +88,10 @@
         mCallback.onClientFinished(this, true /* success */);
     }
 
+    public boolean interruptsPrecedingClients() {
+        return true;
+    }
+
     /**
      * Reset the local lockout state and notify any listeners.
      *
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceResetLockoutClient.java
index 6e74d36..f29b9e8 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceResetLockoutClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceResetLockoutClient.java
@@ -65,6 +65,10 @@
         startHalOperation();
     }
 
+    public boolean interruptsPrecedingClients() {
+        return true;
+    }
+
     @Override
     protected void startHalOperation() {
         try {
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 a600f08..f16af78 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
@@ -122,18 +122,12 @@
                         continue; // Keyguard is always allowed
                     }
 
-                    final List<ActivityManager.RunningTaskInfo> runningTasks =
-                            mActivityTaskManager.getTasks(1);
-                    if (!runningTasks.isEmpty()) {
-                        final String topPackage =
-                                runningTasks.get(0).topActivity.getPackageName();
-                        if (!topPackage.contentEquals(client.getOwnerString())
-                                && !client.isAlreadyDone()) {
-                            Slog.e(getTag(), "Stopping background authentication, top: "
-                                    + topPackage + " currentClient: " + client);
-                            mSensors.valueAt(i).getScheduler().cancelAuthenticationOrDetection(
-                                    client.getToken(), client.getRequestId());
-                        }
+                    if (Utils.isBackground(client.getOwnerString())
+                            && !client.isAlreadyDone()) {
+                        Slog.e(getTag(), "Stopping background authentication,"
+                                + " currentClient: " + client);
+                        mSensors.valueAt(i).getScheduler().cancelAuthenticationOrDetection(
+                                client.getToken(), client.getRequestId());
                     }
                 }
             });
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
index f90cba7..c8148df 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintResetLockoutClient.java
@@ -82,6 +82,10 @@
         }
     }
 
+    public boolean interruptsPrecedingClients() {
+        return true;
+    }
+
     void onLockoutCleared() {
         resetLocalLockoutStateToNone(getSensorId(), getTargetUserId(), mLockoutCache,
                 mLockoutResetDispatcher);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
index 78a30e8..2a3f34a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java
@@ -142,17 +142,12 @@
                     return; // Keyguard is always allowed
                 }
 
-                final List<ActivityManager.RunningTaskInfo> runningTasks =
-                        mActivityTaskManager.getTasks(1);
-                if (!runningTasks.isEmpty()) {
-                    final String topPackage = runningTasks.get(0).topActivity.getPackageName();
-                    if (!topPackage.contentEquals(client.getOwnerString())
-                            && !client.isAlreadyDone()) {
-                        Slog.e(TAG, "Stopping background authentication, top: "
-                                + topPackage + " currentClient: " + client);
-                        mScheduler.cancelAuthenticationOrDetection(
-                                client.getToken(), client.getRequestId());
-                    }
+                if (Utils.isBackground(client.getOwnerString())
+                        && !client.isAlreadyDone()) {
+                    Slog.e(TAG, "Stopping background authentication,"
+                            + " currentClient: " + client);
+                    mScheduler.cancelAuthenticationOrDetection(
+                            client.getToken(), client.getRequestId());
                 }
             });
         }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintResetLockoutClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintResetLockoutClient.java
index 559ca06..843fcc8 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintResetLockoutClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintResetLockoutClient.java
@@ -50,6 +50,10 @@
         callback.onClientFinished(this, true /* success */);
     }
 
+    public boolean interruptsPrecedingClients() {
+        return true;
+    }
+
     @Override
     public int getProtoEnum() {
         return BiometricsProto.CM_RESET_LOCKOUT;
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 3f00244..61350bb 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -380,12 +380,12 @@
             }
             checkDataOwner(clip, intendingUid);
             synchronized (mLock) {
-                scheduleAutoClear(userId);
+                scheduleAutoClear(userId, intendingUid);
                 setPrimaryClipInternalLocked(clip, intendingUid, sourcePackage);
             }
         }
 
-        private void scheduleAutoClear(@UserIdInt int userId) {
+        private void scheduleAutoClear(@UserIdInt int userId, int intendingUid) {
             final long oldIdentity = Binder.clearCallingIdentity();
             try {
                 if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_CLIPBOARD,
@@ -393,7 +393,7 @@
                     mClipboardClearHandler.removeEqualMessages(ClipboardClearHandler.MSG_CLEAR,
                             userId);
                     Message clearMessage = Message.obtain(mClipboardClearHandler,
-                            ClipboardClearHandler.MSG_CLEAR, userId, 0, userId);
+                            ClipboardClearHandler.MSG_CLEAR, userId, intendingUid, userId);
                     mClipboardClearHandler.sendMessageDelayed(clearMessage,
                             getTimeoutForAutoClear());
                 }
@@ -446,7 +446,7 @@
                 showAccessNotificationLocked(pkg, intendingUid, intendingUserId, clipboard);
                 notifyTextClassifierLocked(clipboard, pkg, intendingUid);
                 if (clipboard.primaryClip != null) {
-                    scheduleAutoClear(userId);
+                    scheduleAutoClear(userId, intendingUid);
                 }
                 return clipboard.primaryClip;
             }
@@ -554,11 +554,12 @@
                 switch (msg.what) {
                     case MSG_CLEAR:
                         final int userId = msg.arg1;
+                        final int intendingUid = msg.arg2;
                         synchronized (mLock) {
                             if (getClipboardLocked(userId).primaryClip != null) {
                                 FrameworkStatsLog.write(FrameworkStatsLog.CLIPBOARD_CLEARED,
                                         FrameworkStatsLog.CLIPBOARD_CLEARED__SOURCE__AUTO_CLEAR);
-                                setPrimaryClipInternalLocked(null, Binder.getCallingUid(), null);
+                                setPrimaryClipInternalLocked(null, intendingUid, null);
                             }
                         }
                         break;
diff --git a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
index 28c7cad..ab3b250 100644
--- a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
+++ b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
@@ -18,6 +18,7 @@
 
 import android.annotation.Nullable;
 import android.content.ClipData;
+import android.os.PersistableBundle;
 import android.os.SystemProperties;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -25,6 +26,7 @@
 import android.system.VmSocketAddress;
 import android.util.Slog;
 
+import java.io.EOFException;
 import java.io.FileDescriptor;
 import java.io.InterruptedIOException;
 import java.net.SocketException;
@@ -58,11 +60,11 @@
         return mPipe;
     }
 
-    private synchronized boolean openPipe() {
-        if (mPipe != null) {
-            return true;
-        }
+    private synchronized void setPipeFD(final FileDescriptor fd) {
+        mPipe = fd;
+    }
 
+    private static FileDescriptor openPipeImpl() {
         try {
             final FileDescriptor fd = Os.socket(OsConstants.AF_VSOCK, OsConstants.SOCK_STREAM, 0);
 
@@ -70,39 +72,42 @@
                 Os.connect(fd, new VmSocketAddress(HOST_PORT, OsConstants.VMADDR_CID_HOST));
 
                 final byte[] handshake = createOpenHandshake();
-                Os.write(fd, handshake, 0, handshake.length);
-                mPipe = fd;
-                return true;
+                writeFully(fd, handshake, 0, handshake.length);
+                return fd;
             } catch (ErrnoException | SocketException | InterruptedIOException e) {
                 Os.close(fd);
             }
         } catch (ErrnoException e) {
         }
 
-        return false;
+        return null;
     }
 
-    private synchronized void closePipe() {
-        try {
-            final FileDescriptor fd = mPipe;
-            mPipe = null;
-            if (fd != null) {
-                Os.close(fd);
-            }
-        } catch (ErrnoException ignore) {
+    private static FileDescriptor openPipe() throws InterruptedException {
+        FileDescriptor fd = openPipeImpl();
+
+        // There's no guarantee that QEMU pipes will be ready at the moment
+        // this method is invoked. We simply try to get the pipe open and
+        // retry on failure indefinitely.
+        while (fd == null) {
+            Thread.sleep(100);
+            fd = openPipeImpl();
         }
+
+        return fd;
     }
 
-    private byte[] receiveMessage() throws ErrnoException, InterruptedIOException {
+    private static byte[] receiveMessage(final FileDescriptor fd) throws ErrnoException,
+            InterruptedIOException, EOFException {
         final byte[] lengthBits = new byte[4];
-        Os.read(mPipe, lengthBits, 0, lengthBits.length);
+        readFully(fd, lengthBits, 0, lengthBits.length);
 
         final ByteBuffer bb = ByteBuffer.wrap(lengthBits);
         bb.order(ByteOrder.LITTLE_ENDIAN);
         final int msgLen = bb.getInt();
 
         final byte[] msg = new byte[msgLen];
-        Os.read(mPipe, msg, 0, msg.length);
+        readFully(fd, msg, 0, msg.length);
 
         return msg;
     }
@@ -115,35 +120,46 @@
         bb.order(ByteOrder.LITTLE_ENDIAN);
         bb.putInt(msg.length);
 
-        Os.write(fd, lengthBits, 0, lengthBits.length);
-        Os.write(fd, msg, 0, msg.length);
+        writeFully(fd, lengthBits, 0, lengthBits.length);
+        writeFully(fd, msg, 0, msg.length);
     }
 
     EmulatorClipboardMonitor(final Consumer<ClipData> setAndroidClipboard) {
         this.mHostMonitorThread = new Thread(() -> {
+            FileDescriptor fd = null;
+
             while (!Thread.interrupted()) {
                 try {
-                    // There's no guarantee that QEMU pipes will be ready at the moment
-                    // this method is invoked. We simply try to get the pipe open and
-                    // retry on failure indefinitely.
-                    while (!openPipe()) {
-                        Thread.sleep(100);
+                    if (fd == null) {
+                        fd = openPipe();
+                        setPipeFD(fd);
                     }
 
-                    final byte[] receivedData = receiveMessage();
+                    final byte[] receivedData = receiveMessage(fd);
 
                     final String str = new String(receivedData);
                     final ClipData clip = new ClipData("host clipboard",
                                                        new String[]{"text/plain"},
                                                        new ClipData.Item(str));
+                    final PersistableBundle bundle = new PersistableBundle();
+                    bundle.putBoolean("com.android.systemui.SUPPRESS_CLIPBOARD_OVERLAY", true);
+                    clip.getDescription().setExtras(bundle);
 
                     if (LOG_CLIBOARD_ACCESS) {
                         Slog.i(TAG, "Setting the guest clipboard to '" + str + "'");
                     }
                     setAndroidClipboard.accept(clip);
-                } catch (ErrnoException | InterruptedIOException e) {
-                    closePipe();
-                } catch (InterruptedException | IllegalArgumentException e) {
+                } catch (ErrnoException | EOFException | InterruptedIOException
+                         | InterruptedException e) {
+                    setPipeFD(null);
+
+                    try {
+                        Os.close(fd);
+                    } catch (ErrnoException e2) {
+                        // ignore
+                    }
+
+                    fd = null;
                 }
             }
         });
@@ -153,33 +169,70 @@
 
     @Override
     public void accept(final @Nullable ClipData clip) {
+        final FileDescriptor fd = getPipeFD();
+        if (fd != null) {
+            setHostClipboard(fd, getClipString(clip));
+        }
+    }
+
+    private String getClipString(final @Nullable ClipData clip) {
         if (clip == null) {
-            setHostClipboardImpl("");
-        } else if (clip.getItemCount() > 0) {
-            final CharSequence text = clip.getItemAt(0).getText();
-            if (text != null) {
-                setHostClipboardImpl(text.toString());
+            return "";
+        }
+
+        if (clip.getItemCount() == 0) {
+            return "";
+        }
+
+        final CharSequence text = clip.getItemAt(0).getText();
+        if (text == null) {
+            return "";
+        }
+
+        return text.toString();
+    }
+
+    private static void setHostClipboard(final FileDescriptor fd, final String value) {
+        Thread t = new Thread(() -> {
+            if (LOG_CLIBOARD_ACCESS) {
+                Slog.i(TAG, "Setting the host clipboard to '" + value + "'");
+            }
+
+            try {
+                sendMessage(fd, value.getBytes());
+            } catch (ErrnoException | InterruptedIOException e) {
+                Slog.e(TAG, "Failed to set host clipboard " + e.getMessage());
+            } catch (IllegalArgumentException e) {
+            }
+        });
+        t.start();
+    }
+
+    private static void readFully(final FileDescriptor fd,
+                                  final byte[] buf, int offset, int size)
+                                  throws ErrnoException, InterruptedIOException, EOFException {
+        while (size > 0) {
+            final int r = Os.read(fd, buf, offset, size);
+            if (r > 0) {
+                offset += r;
+                size -= r;
+            } else {
+                throw new EOFException();
             }
         }
     }
 
-    private void setHostClipboardImpl(final String value) {
-        final FileDescriptor pipeFD = getPipeFD();
-
-        if (pipeFD != null) {
-            Thread t = new Thread(() -> {
-                if (LOG_CLIBOARD_ACCESS) {
-                    Slog.i(TAG, "Setting the host clipboard to '" + value + "'");
-                }
-
-                try {
-                    sendMessage(pipeFD, value.getBytes());
-                } catch (ErrnoException | InterruptedIOException e) {
-                    Slog.e(TAG, "Failed to set host clipboard " + e.getMessage());
-                } catch (IllegalArgumentException e) {
-                }
-            });
-            t.start();
+    private static void writeFully(final FileDescriptor fd,
+                                   final byte[] buf, int offset, int size)
+                                   throws ErrnoException, InterruptedIOException {
+        while (size > 0) {
+            final int r = Os.write(fd, buf, offset, size);
+            if (r > 0) {
+                offset += r;
+                size -= r;
+            } else {
+                throw new ErrnoException("write", OsConstants.EIO);
+            }
         }
     }
 }
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 68a53f1..089a924 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -27,6 +27,8 @@
 import static android.os.PowerWhitelistManager.REASON_VPN;
 import static android.os.UserHandle.PER_USER_RANGE;
 
+import static com.android.server.vcn.util.PersistableBundleUtils.STRING_DESERIALIZER;
+
 import static java.util.Objects.requireNonNull;
 
 import android.Manifest;
@@ -97,6 +99,7 @@
 import android.os.Looper;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -127,6 +130,7 @@
 import com.android.server.DeviceIdleInternal;
 import com.android.server.LocalServices;
 import com.android.server.net.BaseNetworkObserver;
+import com.android.server.vcn.util.PersistableBundleUtils;
 
 import libcore.io.IoUtils;
 
@@ -174,6 +178,8 @@
     private static final String VPN_PROVIDER_NAME_BASE = "VpnNetworkProvider:";
     private static final boolean LOGD = true;
     private static final String ANDROID_KEYSTORE_PROVIDER = "AndroidKeyStore";
+    /** Key containing prefix of vpn app excluded list */
+    @VisibleForTesting static final String VPN_APP_EXCLUDED = "VPN_APP_EXCLUDED_";
 
     // Length of time (in milliseconds) that an app hosting an always-on VPN is placed on
     // the device idle allowlist during service launch and VPN bootstrap.
@@ -209,7 +215,6 @@
     private final NetworkInfo mNetworkInfo;
     private int mLegacyState;
     @VisibleForTesting protected String mPackage;
-    private String mSessionKey;
     private int mOwnerUID;
     private boolean mIsPackageTargetingAtLeastQ;
     @VisibleForTesting
@@ -1534,11 +1539,17 @@
     }
 
     // Note: Return type guarantees results are deduped and sorted, which callers require.
+    // This method also adds the SDK sandbox UIDs corresponding to the applications by default,
+    // since apps are generally not aware of them, yet they should follow the VPN configuration
+    // of the app they belong to.
     private SortedSet<Integer> getAppsUids(List<String> packageNames, int userId) {
         SortedSet<Integer> uids = new TreeSet<>();
         for (String app : packageNames) {
             int uid = getAppUid(app, userId);
             if (uid != -1) uids.add(uid);
+            if (Process.isApplicationUid(uid)) {
+                uids.add(Process.toSdkSandboxUid(uid));
+            }
         }
         return uids;
     }
@@ -1991,9 +2002,7 @@
     public synchronized int getActiveVpnType() {
         if (!mNetworkInfo.isConnectedOrConnecting()) return VpnManager.TYPE_VPN_NONE;
         if (mVpnRunner == null) return VpnManager.TYPE_VPN_SERVICE;
-        return mVpnRunner instanceof IkeV2VpnRunner
-                ? VpnManager.TYPE_VPN_PLATFORM
-                : VpnManager.TYPE_VPN_LEGACY;
+        return isIkev2VpnRunner() ? VpnManager.TYPE_VPN_PLATFORM : VpnManager.TYPE_VPN_LEGACY;
     }
 
     private void updateAlwaysOnNotification(DetailedState networkState) {
@@ -2531,12 +2540,15 @@
         @Nullable private IpSecTunnelInterface mTunnelIface;
         @Nullable private IkeSession mSession;
         @Nullable private Network mActiveNetwork;
+        private final String mSessionKey;
 
         IkeV2VpnRunner(@NonNull Ikev2VpnProfile profile) {
             super(TAG);
             mProfile = profile;
             mIpSecManager = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
-            mNetworkCallback = new VpnIkev2Utils.Ikev2VpnNetworkCallback(TAG, this);
+            // Pass mExecutor into Ikev2VpnNetworkCallback and make sure that IkeV2VpnRunnerCallback
+            // will be called by the mExecutor thread.
+            mNetworkCallback = new VpnIkev2Utils.Ikev2VpnNetworkCallback(TAG, this, mExecutor);
             mSessionKey = UUID.randomUUID().toString();
         }
 
@@ -2630,6 +2642,8 @@
 
                     mConfig.underlyingNetworks = new Network[] {network};
 
+                    mConfig.disallowedApplications = getAppExclusionList(mPackage);
+
                     networkAgent = mNetworkAgent;
 
                     // The below must be done atomically with the mConfig update, otherwise
@@ -2691,73 +2705,68 @@
          * <p>The Ikev2VpnRunner will unconditionally switch to the new network, killing the old IKE
          * state in the process, and starting a new IkeSession instance.
          *
-         * <p>This method is called multiple times over the lifetime of the Ikev2VpnRunner, and is
-         * called on the ConnectivityService thread. Thus, the actual work MUST be proxied to the
-         * mExecutor thread in order to ensure consistency of the Ikev2VpnRunner fields.
+         * <p>This method MUST always be called on the mExecutor thread in order to ensure
+         * consistency of the Ikev2VpnRunner fields.
          */
         public void onDefaultNetworkChanged(@NonNull Network network) {
             Log.d(TAG, "Starting IKEv2/IPsec session on new network: " + network);
 
-            // Proxy to the Ikev2VpnRunner (single-thread) executor to ensure consistency in lieu
-            // of locking.
-            mExecutor.execute(() -> {
-                try {
-                    if (!mIsRunning) {
-                        Log.d(TAG, "onDefaultNetworkChanged after exit");
-                        return; // VPN has been shut down.
-                    }
-
-                    // Clear mInterface to prevent Ikev2VpnRunner being cleared when
-                    // interfaceRemoved() is called.
-                    mInterface = null;
-                    // Without MOBIKE, we have no way to seamlessly migrate. Close on old
-                    // (non-default) network, and start the new one.
-                    resetIkeState();
-                    mActiveNetwork = network;
-
-                    // Get Ike options from IkeTunnelConnectionParams if it's available in the
-                    // profile.
-                    final IkeTunnelConnectionParams ikeTunConnParams =
-                            mProfile.getIkeTunnelConnectionParams();
-                    final IkeSessionParams ikeSessionParams;
-                    final ChildSessionParams childSessionParams;
-                    if (ikeTunConnParams != null) {
-                        final IkeSessionParams.Builder builder = new IkeSessionParams.Builder(
-                                ikeTunConnParams.getIkeSessionParams()).setNetwork(network);
-                        ikeSessionParams = builder.build();
-                        childSessionParams = ikeTunConnParams.getTunnelModeChildSessionParams();
-                    } else {
-                        ikeSessionParams = VpnIkev2Utils.buildIkeSessionParams(
-                                mContext, mProfile, network);
-                        childSessionParams = VpnIkev2Utils.buildChildSessionParams(
-                                mProfile.getAllowedAlgorithms());
-                    }
-
-                    // TODO: Remove the need for adding two unused addresses with
-                    // IPsec tunnels.
-                    final InetAddress address = InetAddress.getLocalHost();
-                    mTunnelIface =
-                            mIpSecManager.createIpSecTunnelInterface(
-                                    address /* unused */,
-                                    address /* unused */,
-                                    network);
-                    NetdUtils.setInterfaceUp(mNetd, mTunnelIface.getInterfaceName());
-
-                    mSession = mIkev2SessionCreator.createIkeSession(
-                            mContext,
-                            ikeSessionParams,
-                            childSessionParams,
-                            mExecutor,
-                            new VpnIkev2Utils.IkeSessionCallbackImpl(
-                                    TAG, IkeV2VpnRunner.this, network),
-                            new VpnIkev2Utils.ChildSessionCallbackImpl(
-                                    TAG, IkeV2VpnRunner.this, network));
-                    Log.d(TAG, "Ike Session started for network " + network);
-                } catch (Exception e) {
-                    Log.i(TAG, "Setup failed for network " + network + ". Aborting", e);
-                    onSessionLost(network, e);
+            try {
+                if (!mIsRunning) {
+                    Log.d(TAG, "onDefaultNetworkChanged after exit");
+                    return; // VPN has been shut down.
                 }
-            });
+
+                // Clear mInterface to prevent Ikev2VpnRunner being cleared when
+                // interfaceRemoved() is called.
+                mInterface = null;
+                // Without MOBIKE, we have no way to seamlessly migrate. Close on old
+                // (non-default) network, and start the new one.
+                resetIkeState();
+                mActiveNetwork = network;
+
+                // Get Ike options from IkeTunnelConnectionParams if it's available in the
+                // profile.
+                final IkeTunnelConnectionParams ikeTunConnParams =
+                        mProfile.getIkeTunnelConnectionParams();
+                final IkeSessionParams ikeSessionParams;
+                final ChildSessionParams childSessionParams;
+                if (ikeTunConnParams != null) {
+                    final IkeSessionParams.Builder builder = new IkeSessionParams.Builder(
+                            ikeTunConnParams.getIkeSessionParams()).setNetwork(network);
+                    ikeSessionParams = builder.build();
+                    childSessionParams = ikeTunConnParams.getTunnelModeChildSessionParams();
+                } else {
+                    ikeSessionParams = VpnIkev2Utils.buildIkeSessionParams(
+                            mContext, mProfile, network);
+                    childSessionParams = VpnIkev2Utils.buildChildSessionParams(
+                            mProfile.getAllowedAlgorithms());
+                }
+
+                // TODO: Remove the need for adding two unused addresses with
+                // IPsec tunnels.
+                final InetAddress address = InetAddress.getLocalHost();
+                mTunnelIface =
+                        mIpSecManager.createIpSecTunnelInterface(
+                                address /* unused */,
+                                address /* unused */,
+                                network);
+                NetdUtils.setInterfaceUp(mNetd, mTunnelIface.getInterfaceName());
+
+                mSession = mIkev2SessionCreator.createIkeSession(
+                        mContext,
+                        ikeSessionParams,
+                        childSessionParams,
+                        mExecutor,
+                        new VpnIkev2Utils.IkeSessionCallbackImpl(
+                                TAG, IkeV2VpnRunner.this, network),
+                        new VpnIkev2Utils.ChildSessionCallbackImpl(
+                                TAG, IkeV2VpnRunner.this, network));
+                Log.d(TAG, "Ike Session started for network " + network);
+            } catch (Exception e) {
+                Log.i(TAG, "Setup failed for network " + network + ". Aborting", e);
+                onSessionLost(network, e);
+            }
         }
 
         /** Marks the state as FAILED, and disconnects. */
@@ -2876,7 +2885,6 @@
          */
         private void disconnectVpnRunner() {
             mActiveNetwork = null;
-            mSessionKey = null;
             mIsRunning = false;
 
             resetIkeState();
@@ -3306,7 +3314,7 @@
     }
 
     private boolean isCurrentIkev2VpnLocked(@NonNull String packageName) {
-        return isCurrentPreparedPackage(packageName) && mVpnRunner instanceof IkeV2VpnRunner;
+        return isCurrentPreparedPackage(packageName) && isIkev2VpnRunner();
     }
 
     /**
@@ -3360,6 +3368,16 @@
         return VpnProfile.decode("" /* Key unused */, encoded);
     }
 
+    private boolean isIkev2VpnRunner() {
+        return (mVpnRunner instanceof IkeV2VpnRunner);
+    }
+
+    @GuardedBy("this")
+    @Nullable
+    private String getSessionKeyLocked() {
+        return isIkev2VpnRunner() ? ((IkeV2VpnRunner) mVpnRunner).mSessionKey : null;
+    }
+
     /**
      * Starts an already provisioned VPN Profile, keyed by package name.
      *
@@ -3387,7 +3405,11 @@
             }
 
             startVpnProfilePrivileged(profile, packageName);
-            return mSessionKey;
+            if (!isIkev2VpnRunner()) {
+                throw new IllegalStateException("mVpnRunner shouldn't be null and should also be "
+                        + "an instance of Ikev2VpnRunner");
+            }
+            return getSessionKeyLocked();
         } finally {
             Binder.restoreCallingIdentity(token);
         }
@@ -3472,6 +3494,88 @@
         }
     }
 
+    private boolean storeAppExclusionList(@NonNull String packageName,
+            @NonNull List<String> excludedApps) {
+        byte[] data;
+        try {
+            final PersistableBundle bundle = PersistableBundleUtils.fromList(
+                    excludedApps, PersistableBundleUtils.STRING_SERIALIZER);
+            data = PersistableBundleUtils.toDiskStableBytes(bundle);
+        } catch (IOException e) {
+            Log.e(TAG, "problem writing into stream", e);
+            return false;
+        }
+
+        final long oldId = Binder.clearCallingIdentity();
+        try {
+            getVpnProfileStore().put(getVpnAppExcludedForPackage(packageName), data);
+        } finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+        return true;
+    }
+
+    @VisibleForTesting
+    String getVpnAppExcludedForPackage(String packageName) {
+        return VPN_APP_EXCLUDED + mUserId + "_" + packageName;
+    }
+
+    /**
+     * Set the application exclusion list for the specified VPN profile.
+     *
+     * @param packageName the package name of the app provisioning this profile
+     * @param excludedApps the list of excluded packages
+     *
+     * @return whether setting the list is successful or not
+     */
+    public synchronized boolean setAppExclusionList(@NonNull String packageName,
+            @NonNull List<String> excludedApps) {
+        enforceNotRestrictedUser();
+        if (!storeAppExclusionList(packageName, excludedApps)) return false;
+        // Re-build and update NetworkCapabilities via NetworkAgent.
+        if (mNetworkAgent != null) {
+            // Only update the platform VPN
+            if (isIkev2VpnRunner()) {
+                mConfig.disallowedApplications = List.copyOf(excludedApps);
+                mNetworkCapabilities = new NetworkCapabilities.Builder(mNetworkCapabilities)
+                        .setUids(createUserAndRestrictedProfilesRanges(
+                                mUserId, null /* allowedApplications */, excludedApps))
+                        .build();
+                mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Gets the application exclusion list for the specified VPN profile.
+     *
+     * @param packageName the package name of the app provisioning this profile
+     * @return the list of excluded packages for the specified VPN profile or empty list if there is
+     *         no provisioned VPN profile.
+     */
+    @NonNull
+    public synchronized List<String> getAppExclusionList(@NonNull String packageName) {
+        enforceNotRestrictedUser();
+
+        final long oldId = Binder.clearCallingIdentity();
+        try {
+            final byte[] bytes = getVpnProfileStore().get(getVpnAppExcludedForPackage(packageName));
+
+            if (bytes == null || bytes.length == 0) return new ArrayList<>();
+
+            final PersistableBundle bundle = PersistableBundleUtils.fromDiskStableBytes(bytes);
+            return PersistableBundleUtils.toList(bundle, STRING_DESERIALIZER);
+        } catch (IOException e) {
+            Log.e(TAG, "problem reading from stream", e);
+        }  finally {
+            Binder.restoreCallingIdentity(oldId);
+        }
+
+        return new ArrayList<>();
+    }
+
     private @VpnProfileState.State int getStateFromLegacyState(int legacyState) {
         switch (legacyState) {
             case LegacyVpnInfo.STATE_CONNECTING:
@@ -3490,11 +3594,8 @@
     }
 
     private VpnProfileState makeVpnProfileState() {
-        // TODO: mSessionKey will be moved to Ikev2VpnRunner once aosp/2007077 is merged, so after
-        //  merging aosp/2007077, here should check Ikev2VpnRunner is null or not. Session key will
-        //  be null if Ikev2VpnRunner is null.
-        return new VpnProfileState(getStateFromLegacyState(mLegacyState), mSessionKey, mAlwaysOn,
-                mLockdown);
+        return new VpnProfileState(getStateFromLegacyState(mLegacyState),
+                isIkev2VpnRunner() ? getSessionKeyLocked() : null, mAlwaysOn, mLockdown);
     }
 
     /**
diff --git a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java
index a0a596d..6982d60 100644
--- a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java
+++ b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java
@@ -86,6 +86,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
 
 /**
  * Utility class to build and convert IKEv2/IPsec parameters.
@@ -376,22 +377,25 @@
     static class Ikev2VpnNetworkCallback extends NetworkCallback {
         private final String mTag;
         private final Vpn.IkeV2VpnRunnerCallback mCallback;
+        private final ExecutorService mExecutor;
 
-        Ikev2VpnNetworkCallback(String tag, Vpn.IkeV2VpnRunnerCallback callback) {
+        Ikev2VpnNetworkCallback(String tag, Vpn.IkeV2VpnRunnerCallback callback,
+                ExecutorService executor) {
             mTag = tag;
             mCallback = callback;
+            mExecutor = executor;
         }
 
         @Override
         public void onAvailable(@NonNull Network network) {
             Log.d(mTag, "Starting IKEv2/IPsec session on new network: " + network);
-            mCallback.onDefaultNetworkChanged(network);
+            mExecutor.execute(() -> mCallback.onDefaultNetworkChanged(network));
         }
 
         @Override
         public void onLost(@NonNull Network network) {
             Log.d(mTag, "Tearing down; lost network: " + network);
-            mCallback.onSessionLost(network, null);
+            mExecutor.execute(() -> mCallback.onSessionLost(network, null));
         }
     }
 
diff --git a/services/core/java/com/android/server/content/ContentService.java b/services/core/java/com/android/server/content/ContentService.java
index 954b930..f93e06d 100644
--- a/services/core/java/com/android/server/content/ContentService.java
+++ b/services/core/java/com/android/server/content/ContentService.java
@@ -78,6 +78,7 @@
 import com.android.internal.os.BinderDeathDispatcher;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -1542,11 +1543,62 @@
             return ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET_WITH_TEMP;
         }
         if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND || isUidActive) {
+            FrameworkStatsLog.write(FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED,
+                    callingUid, getProcStateForStatsd(procState), isUidActive);
             return ContentResolver.SYNC_EXEMPTION_PROMOTE_BUCKET;
         }
         return ContentResolver.SYNC_EXEMPTION_NONE;
     }
 
+    private int getProcStateForStatsd(int procState) {
+        switch (procState) {
+            case ActivityManager.PROCESS_STATE_UNKNOWN:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__UNKNOWN;
+            case ActivityManager.PROCESS_STATE_PERSISTENT:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__PERSISTENT;
+            case ActivityManager.PROCESS_STATE_PERSISTENT_UI:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__PERSISTENT_UI;
+            case ActivityManager.PROCESS_STATE_TOP:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__TOP;
+            case ActivityManager.PROCESS_STATE_BOUND_TOP:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__BOUND_TOP;
+            case ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__FOREGROUND_SERVICE;
+            case ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE:
+                return FrameworkStatsLog
+                        .SYNC_EXEMPTION_OCCURRED__PROC_STATE__BOUND_FOREGROUND_SERVICE;
+            case ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__IMPORTANT_FOREGROUND;
+            case ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__IMPORTANT_BACKGROUND;
+            case ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__TRANSIENT_BACKGROUND;
+            case ActivityManager.PROCESS_STATE_BACKUP:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__BACKUP;
+            case ActivityManager.PROCESS_STATE_SERVICE:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__SERVICE;
+            case ActivityManager.PROCESS_STATE_RECEIVER:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__RECEIVER;
+            case ActivityManager.PROCESS_STATE_TOP_SLEEPING:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__TOP_SLEEPING;
+            case ActivityManager.PROCESS_STATE_HEAVY_WEIGHT:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__HEAVY_WEIGHT;
+            case ActivityManager.PROCESS_STATE_LAST_ACTIVITY:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__LAST_ACTIVITY;
+            case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__CACHED_ACTIVITY;
+            case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
+                return FrameworkStatsLog
+                        .SYNC_EXEMPTION_OCCURRED__PROC_STATE__CACHED_ACTIVITY_CLIENT;
+            case ActivityManager.PROCESS_STATE_CACHED_RECENT:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__CACHED_RECENT;
+            case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__CACHED_EMPTY;
+            default:
+                return FrameworkStatsLog.SYNC_EXEMPTION_OCCURRED__PROC_STATE__UNKNOWN;
+        }
+    }
+
     /** {@hide} */
     @VisibleForTesting
     public static final class ObserverNode {
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index 8035526..8de150a 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -948,19 +948,15 @@
         if (!isColorModeAvailable(colorMode)) {
             final int[] mappedColorModes = getContext().getResources().getIntArray(
                     R.array.config_mappedColorModes);
-            if (colorMode == COLOR_MODE_BOOSTED && mappedColorModes.length > COLOR_MODE_NATURAL
-                    && isColorModeAvailable(mappedColorModes[COLOR_MODE_NATURAL])) {
-                colorMode = COLOR_MODE_NATURAL;
-            } else if (colorMode == COLOR_MODE_SATURATED
-                    && mappedColorModes.length > COLOR_MODE_AUTOMATIC
-                    && isColorModeAvailable(mappedColorModes[COLOR_MODE_AUTOMATIC])) {
-                colorMode = COLOR_MODE_AUTOMATIC;
-            } else if (colorMode == COLOR_MODE_AUTOMATIC
-                    && mappedColorModes.length > COLOR_MODE_SATURATED
-                    && isColorModeAvailable(mappedColorModes[COLOR_MODE_SATURATED])) {
-                colorMode = COLOR_MODE_SATURATED;
+            if (colorMode != -1 && mappedColorModes.length > colorMode
+                    && isColorModeAvailable(mappedColorModes[colorMode])) {
+                colorMode = mappedColorModes[colorMode];
             } else {
-                colorMode = -1;
+                final int[] availableColorModes = getContext().getResources().getIntArray(
+                        R.array.config_availableColorModes);
+                if (availableColorModes.length > 0) {
+                    colorMode = availableColorModes[0];
+                }
             }
         }
 
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 4e1d899..63c5456 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -124,8 +124,10 @@
                     final boolean activityAllowed = activityType == ACTIVITY_TYPE_HOME
                             || activityType == ACTIVITY_TYPE_DREAM
                             || activityType == ACTIVITY_TYPE_ASSISTANT;
-                    if (mCurrentDreamToken != null && !mCurrentDreamIsWaking && !activityAllowed) {
-                        stopDreamInternal(false, "activity starting: " + activityInfo.name);
+                    if (mCurrentDreamToken != null && !mCurrentDreamIsWaking
+                            && !mCurrentDreamIsDozing && !activityAllowed) {
+                        requestAwakenInternal(
+                                "stopping dream due to activity start: " + activityInfo.name);
                     }
                 }
             };
@@ -229,13 +231,13 @@
         mPowerManager.nap(time);
     }
 
-    private void requestAwakenInternal() {
+    private void requestAwakenInternal(String reason) {
         // Treat an explicit request to awaken as user activity so that the
         // device doesn't immediately go to sleep if the timeout expired,
         // for example when being undocked.
         long time = SystemClock.uptimeMillis();
         mPowerManager.userActivity(time, false /*noChangeLights*/);
-        stopDreamInternal(false /*immediate*/, "request awaken");
+        stopDreamInternal(false /*immediate*/, reason);
     }
 
     private void finishSelfInternal(IBinder token, boolean immediate) {
@@ -715,7 +717,7 @@
 
             final long ident = Binder.clearCallingIdentity();
             try {
-                requestAwakenInternal();
+                requestAwakenInternal("request awaken");
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 1ea1457..9bce471f 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -810,35 +810,24 @@
         }
     }
 
-    /**
-     * Change ARC status into the given {@code enabled} status.
-     *
-     * @return {@code true} if ARC was in "Enabled" status
-     */
     @ServiceThreadOnly
-    boolean setArcStatus(boolean enabled) {
+    void enableArc(List<byte[]> supportedSads) {
         assertRunOnServiceThread();
+        HdmiLogger.debug("Set Arc Status[old:%b new:true]", mArcEstablished);
 
-        HdmiLogger.debug("Set Arc Status[old:%b new:%b]", mArcEstablished, enabled);
-        boolean oldStatus = mArcEstablished;
-        if (enabled) {
-            RequestSadAction action = new RequestSadAction(
-                    this, Constants.ADDR_AUDIO_SYSTEM,
-                    new RequestSadAction.RequestSadCallback() {
-                        @Override
-                        public void onRequestSadDone(List<byte[]> supportedSads) {
-                            enableAudioReturnChannel(enabled);
-                            notifyArcStatusToAudioService(enabled, supportedSads);
-                            mArcEstablished = enabled;
-                        }
-                    });
-            addAndStartAction(action);
-        } else {
-            enableAudioReturnChannel(enabled);
-            notifyArcStatusToAudioService(enabled, new ArrayList<>());
-            mArcEstablished = enabled;
-        }
-        return oldStatus;
+        enableAudioReturnChannel(true);
+        notifyArcStatusToAudioService(true, supportedSads);
+        mArcEstablished = true;
+    }
+
+    @ServiceThreadOnly
+    void disableArc() {
+        assertRunOnServiceThread();
+        HdmiLogger.debug("Set Arc Status[old:%b new:false]", mArcEstablished);
+
+        enableAudioReturnChannel(false);
+        notifyArcStatusToAudioService(false, new ArrayList<>());
+        mArcEstablished = false;
     }
 
     /**
@@ -1066,7 +1055,7 @@
     protected int handleTerminateArc(HdmiCecMessage message) {
         assertRunOnServiceThread();
         if (mService .isPowerStandbyOrTransient()) {
-            setArcStatus(false);
+            disableArc();
             return Constants.HANDLED;
         }
         // Do not check ARC configuration since the AVR might have been already removed.
@@ -1353,7 +1342,7 @@
         if (avr == null) {
             return;
         }
-        setArcStatus(false);
+        disableArc();
 
         // Seq #44.
         removeAllRunningArcAction();
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 9824b4e..f8a74f4 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -4176,7 +4176,11 @@
         List<AudioDeviceAttributes> streamMusicDevices =
                 getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES);
         if (streamMusicDevices.contains(getAvcAudioOutputDevice())) {
-            setStreamMusicVolume(volume, AudioManager.FLAG_ABSOLUTE_VOLUME);
+            int flags = AudioManager.FLAG_ABSOLUTE_VOLUME;
+            if (isTvDevice()) {
+                flags |= AudioManager.FLAG_SHOW_UI;
+            }
+            setStreamMusicVolume(volume, flags);
         }
     }
 
@@ -4190,8 +4194,11 @@
                 getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES);
         if (streamMusicDevices.contains(getAvcAudioOutputDevice())) {
             int direction = mute ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE;
-            getAudioManager().adjustStreamVolume(AudioManager.STREAM_MUSIC, direction,
-                    AudioManager.FLAG_ABSOLUTE_VOLUME);
+            int flags = AudioManager.FLAG_ABSOLUTE_VOLUME;
+            if (isTvDevice()) {
+                flags |= AudioManager.FLAG_SHOW_UI;
+            }
+            getAudioManager().adjustStreamVolume(AudioManager.STREAM_MUSIC, direction, flags);
         }
     }
 
diff --git a/services/core/java/com/android/server/hdmi/RequestArcAction.java b/services/core/java/com/android/server/hdmi/RequestArcAction.java
index c70101c..3d9a290 100644
--- a/services/core/java/com/android/server/hdmi/RequestArcAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestArcAction.java
@@ -63,7 +63,7 @@
                     finish();
                     return true;
                 } else if (originalOpcode == Constants.MESSAGE_REQUEST_ARC_INITIATION) {
-                    tv().setArcStatus(false);
+                    tv().disableArc();
                     finish();
                     return true;
                 }
diff --git a/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java b/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
index 4eb220f..3b7f1dd 100644
--- a/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
@@ -48,7 +48,7 @@
             public void onSendCompleted(int error) {
                 if (error != SendMessageResult.SUCCESS) {
                     // Turn off ARC status if <Request ARC Initiation> fails.
-                    tv().setArcStatus(false);
+                    tv().disableArc();
                     finish();
                 }
             }
diff --git a/services/core/java/com/android/server/hdmi/RequestSadAction.java b/services/core/java/com/android/server/hdmi/RequestSadAction.java
index 702c000..23aaf32 100644
--- a/services/core/java/com/android/server/hdmi/RequestSadAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestSadAction.java
@@ -181,13 +181,20 @@
             return true;
         }
         if (cmd.getOpcode() == Constants.MESSAGE_FEATURE_ABORT
-                && (cmd.getParams()[0] & 0xFF) == Constants.MESSAGE_REQUEST_SHORT_AUDIO_DESCRIPTOR
-                && (cmd.getParams()[1] & 0xFF) == Constants.ABORT_INVALID_OPERAND) {
-            // Queried SADs are not supported
-            mQueriedSadCount += MAX_SAD_PER_REQUEST;
-            mTimeoutRetry = 0;
-            querySad();
-            return true;
+                && (cmd.getParams()[0] & 0xFF)
+                == Constants.MESSAGE_REQUEST_SHORT_AUDIO_DESCRIPTOR) {
+            if ((cmd.getParams()[1] & 0xFF) == Constants.ABORT_UNRECOGNIZED_OPCODE) {
+                // SAD feature is not supported
+                wrapUpAndFinish();
+                return true;
+            }
+            if ((cmd.getParams()[1] & 0xFF) == Constants.ABORT_INVALID_OPERAND) {
+                // Queried SADs are not supported
+                mQueriedSadCount += MAX_SAD_PER_REQUEST;
+                mTimeoutRetry = 0;
+                querySad();
+                return true;
+            }
         }
         return false;
     }
@@ -211,9 +218,9 @@
                 querySad();
                 return;
             }
-            mQueriedSadCount += MAX_SAD_PER_REQUEST;
-            mTimeoutRetry = 0;
-            querySad();
+            // Don't query any other SADs if one of the SAD queries ran into the maximum amount of
+            // retries.
+            wrapUpAndFinish();
         }
     }
 
diff --git a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
index db93ad0..32e274e 100644
--- a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
+++ b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
@@ -20,6 +20,8 @@
 import android.hardware.tv.cec.V1_0.SendMessageResult;
 import android.util.Slog;
 
+import java.util.List;
+
 /**
  * Feature action that handles enabling/disabling of ARC transmission channel.
  * Once TV gets &lt;Initiate ARC&gt;, TV sends &lt;Report ARC Initiated&gt; to AV Receiver.
@@ -55,21 +57,31 @@
     boolean start() {
         // Seq #37.
         if (mEnabled) {
-            // Enable ARC status immediately before sending <Report Arc Initiated>.
-            // If AVR responds with <Feature Abort>, disable ARC status again.
-            // This is different from spec that says that turns ARC status to
-            // "Enabled" if <Report ARC Initiated> is acknowledged and no
-            // <Feature Abort> is received.
-            // But implemented this way to save the time having to wait for
-            // <Feature Abort>.
-            setArcStatus(true);
-            // If succeeds to send <Report ARC Initiated>, wait general timeout
-            // to check whether there is no <Feature Abort> for <Report ARC Initiated>.
-            mState = STATE_WAITING_TIMEOUT;
-            addTimer(mState, HdmiConfig.TIMEOUT_MS);
-            sendReportArcInitiated();
+            // Request SADs before enabling ARC
+            RequestSadAction action = new RequestSadAction(
+                    localDevice(), Constants.ADDR_AUDIO_SYSTEM,
+                    new RequestSadAction.RequestSadCallback() {
+                        @Override
+                        public void onRequestSadDone(List<byte[]> supportedSads) {
+                            // Enable ARC status immediately before sending <Report Arc Initiated>.
+                            // If AVR responds with <Feature Abort>, disable ARC status again.
+                            // This is different from spec that says that turns ARC status to
+                            // "Enabled" if <Report ARC Initiated> is acknowledged and no
+                            // <Feature Abort> is received.
+                            // But implemented this way to save the time having to wait for
+                            // <Feature Abort>.
+                            Slog.i(TAG, "Enabling ARC");
+                            tv().enableArc(supportedSads);
+                            // If succeeds to send <Report ARC Initiated>, wait general timeout to
+                            // check whether there is no <Feature Abort> for <Report ARC Initiated>.
+                            mState = STATE_WAITING_TIMEOUT;
+                            addTimer(mState, HdmiConfig.TIMEOUT_MS);
+                            sendReportArcInitiated();
+                        }
+                    });
+            addAndStartAction(action);
         } else {
-            setArcStatus(false);
+            disableArc();
             finish();
         }
         return true;
@@ -92,7 +104,7 @@
                     case SendMessageResult.NACK:
                         // If <Report ARC Initiated> is negatively ack'ed, disable ARC and
                         // send <Report ARC Terminated> directly.
-                        setArcStatus(false);
+                        disableArc();
                         HdmiLogger.debug("Failed to send <Report Arc Initiated>.");
                         finish();
                         break;
@@ -101,16 +113,12 @@
         });
     }
 
-    private void setArcStatus(boolean enabled) {
-        tv().setArcStatus(enabled);
-        Slog.i(TAG, "Change arc status to " + enabled);
+    private void disableArc() {
+        Slog.i(TAG, "Disabling ARC");
 
-        // If enabled before and set to "disabled" and send <Report Arc Terminated> to
-        // av reciever.
-        if (!enabled) {
-            sendCommand(HdmiCecMessageBuilder.buildReportArcTerminated(getSourceAddress(),
-                    mAvrAddress));
-        }
+        tv().disableArc();
+        sendCommand(HdmiCecMessageBuilder.buildReportArcTerminated(getSourceAddress(),
+                mAvrAddress));
     }
 
     @Override
@@ -124,7 +132,7 @@
             int originalOpcode = cmd.getParams()[0] & 0xFF;
             if (originalOpcode == Constants.MESSAGE_REPORT_ARC_INITIATED) {
                 HdmiLogger.debug("Feature aborted for <Report Arc Initiated>");
-                setArcStatus(false);
+                disableArc();
                 finish();
                 return true;
             }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index e4e9d1d..b624d43 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -20,6 +20,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityManagerInternal;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -860,6 +861,19 @@
 
     @Override // Binder call
     public boolean injectInputEvent(InputEvent event, int mode) {
+        return injectInputEventToTarget(event, mode, Process.INVALID_UID);
+    }
+
+    @Override // Binder call
+    public boolean injectInputEventToTarget(InputEvent event, int mode, int targetUid) {
+        if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS,
+                "injectInputEvent()", true /*checkInstrumentationSource*/)) {
+            throw new SecurityException(
+                    "Injecting input events requires the caller (or the source of the "
+                            + "instrumentation, if any) to have the INJECT_EVENTS permission.");
+        }
+        // We are not checking if targetUid matches the callingUid, since having the permission
+        // already means you can inject into any window.
         Objects.requireNonNull(event, "event must not be null");
         if (mode != InputEventInjectionSync.NONE
                 && mode != InputEventInjectionSync.WAIT_FOR_FINISHED
@@ -868,22 +882,39 @@
         }
 
         final int pid = Binder.getCallingPid();
-        final int uid = Binder.getCallingUid();
         final long ident = Binder.clearCallingIdentity();
+        final boolean injectIntoUid = targetUid != Process.INVALID_UID;
         final int result;
         try {
-            result = mNative.injectInputEvent(event, pid, uid, mode,
-                    INJECTION_TIMEOUT_MILLIS, WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT);
+            result = mNative.injectInputEvent(event, injectIntoUid,
+                    targetUid, mode, INJECTION_TIMEOUT_MILLIS,
+                    WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
         switch (result) {
-            case InputEventInjectionResult.PERMISSION_DENIED:
-                Slog.w(TAG, "Input event injection from pid " + pid + " permission denied.");
-                throw new SecurityException(
-                        "Injecting to another application requires INJECT_EVENTS permission");
             case InputEventInjectionResult.SUCCEEDED:
                 return true;
+            case InputEventInjectionResult.TARGET_MISMATCH:
+                if (!injectIntoUid) {
+                    throw new IllegalStateException("Injection should not result in TARGET_MISMATCH"
+                            + " when it is not targeted into to a specific uid.");
+                }
+                // TODO(b/228161340): Remove the fallback of targeting injection into all windows
+                //  when the caller has the injection permission.
+                // Explicitly maintain the same behavior as previous versions of Android, where
+                // injection is allowed into all windows if the caller has the INJECT_EVENTS
+                // permission, even if it is targeting a certain uid.
+                if (checkCallingPermission(android.Manifest.permission.INJECT_EVENTS,
+                        "injectInputEvent-target-mismatch-fallback")) {
+                    Slog.w(TAG, "Targeted input event was not directed at a window owned by uid "
+                            + targetUid + ". Falling back to injecting into all windows.");
+                    return injectInputEventToTarget(event, mode, Process.INVALID_UID);
+                }
+                throw new IllegalArgumentException(
+                    "Targeted input event injection from pid " + pid
+                            + " was not directed at a window owned by uid "
+                            + targetUid + ".");
             case InputEventInjectionResult.TIMED_OUT:
                 Slog.w(TAG, "Input event injection from pid " + pid + " timed out.");
                 return false;
@@ -2780,8 +2811,12 @@
             }
         }
     }
-
     private boolean checkCallingPermission(String permission, String func) {
+        return checkCallingPermission(permission, func, false /*checkInstrumentationSource*/);
+    }
+
+    private boolean checkCallingPermission(String permission, String func,
+            boolean checkInstrumentationSource) {
         // Quick check: if the calling permission is me, it's all okay.
         if (Binder.getCallingPid() == Process.myPid()) {
             return true;
@@ -2790,6 +2825,28 @@
         if (mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) {
             return true;
         }
+
+        if (checkInstrumentationSource) {
+            final ActivityManagerInternal ami =
+                    LocalServices.getService(ActivityManagerInternal.class);
+            Objects.requireNonNull(ami, "ActivityManagerInternal should not be null.");
+            final int instrumentationUid = ami.getInstrumentationSourceUid(Binder.getCallingUid());
+            if (instrumentationUid != Process.INVALID_UID) {
+                // Clear the calling identity when checking if the instrumentation source has
+                // permission because PackageManager will deny all permissions to some callers,
+                // such as instant apps.
+                final long token = Binder.clearCallingIdentity();
+                try {
+                    if (mContext.checkPermission(permission, -1 /*pid*/, instrumentationUid)
+                            == PackageManager.PERMISSION_GRANTED) {
+                        return true;
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(token);
+                }
+            }
+        }
+
         String msg = "Permission Denial: " + func + " from pid="
                 + Binder.getCallingPid()
                 + ", uid=" + Binder.getCallingUid()
@@ -3035,13 +3092,6 @@
 
     // Native callback.
     @SuppressWarnings("unused")
-    private boolean checkInjectEventsPermission(int injectorPid, int injectorUid) {
-        return mContext.checkPermission(android.Manifest.permission.INJECT_EVENTS,
-                injectorPid, injectorUid) == PackageManager.PERMISSION_GRANTED;
-    }
-
-    // Native callback.
-    @SuppressWarnings("unused")
     private void onPointerDownOutsideFocus(IBinder touchedToken) {
         mWindowManagerCallbacks.onPointerDownOutsideFocus(touchedToken);
     }
@@ -3501,12 +3551,17 @@
 
         @Override
         public void sendInputEvent(InputEvent event, int policyFlags) {
+            if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS,
+                    "sendInputEvent()")) {
+                throw new SecurityException(
+                        "The INJECT_EVENTS permission is required for injecting input events.");
+            }
             Objects.requireNonNull(event, "event must not be null");
 
             synchronized (mInputFilterLock) {
                 if (!mDisconnected) {
-                    mNative.injectInputEvent(event, 0, 0,
-                            InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0,
+                    mNative.injectInputEvent(event, false /* injectIntoUid */, -1 /* uid */,
+                            InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0 /* timeout */,
                             policyFlags | WindowManagerPolicy.FLAG_FILTERED);
                 }
             }
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 81882d2..9cf8073 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -70,7 +70,18 @@
 
     void setBlockUntrustedTouchesMode(int mode);
 
-    int injectInputEvent(InputEvent event, int pid, int uid, int syncMode,
+    /**
+     * Inject an input event into the system.
+     *
+     * @param event         the input event to inject
+     * @param injectIntoUid true if the event should target windows owned by uid, false otherwise
+     * @param uid           the uid whose windows should be targeted, if any
+     * @param syncMode      {@link android.os.InputEventInjectionSync}
+     * @param timeoutMillis timeout to wait for input injection to complete, in milliseconds
+     * @param policyFlags   defined in {@link android.view.WindowManagerPolicyConstants}
+     * @return {@link android.os.InputEventInjectionResult}
+     */
+    int injectInputEvent(InputEvent event, boolean injectIntoUid, int uid, int syncMode,
             int timeoutMillis, int policyFlags);
 
     VerifiedInputEvent verifyInputEvent(InputEvent event);
@@ -240,7 +251,8 @@
         public native void setBlockUntrustedTouchesMode(int mode);
 
         @Override
-        public native int injectInputEvent(InputEvent event, int pid, int uid, int syncMode,
+        public native int injectInputEvent(InputEvent event, boolean injectIntoUid, int uid,
+                int syncMode,
                 int timeoutMillis, int policyFlags);
 
         @Override
diff --git a/services/core/java/com/android/server/input/TEST_MAPPING b/services/core/java/com/android/server/input/TEST_MAPPING
new file mode 100644
index 0000000..9626d8d
--- /dev/null
+++ b/services/core/java/com/android/server/input/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "imports": [
+    {
+      "path": "frameworks/native/services/inputflinger"
+    }
+  ]
+}
diff --git a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
index ea99e79..f5c2bbc 100644
--- a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
@@ -1587,6 +1587,7 @@
         if (isGpsEnabled()) {
             setGpsEnabled(false);
             updateEnabled();
+            restartLocationRequest();
         }
     }
 
diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
index 718f98a..70b8689 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
@@ -200,7 +200,7 @@
         mHandler = new Handler(looper);
         mNiHandler = niHandler;
         mConnMgr = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
-        mSuplConnectivityCallback = createSuplConnectivityCallback();
+        mSuplConnectivityCallback = null;
     }
 
     /**
@@ -584,11 +584,21 @@
             networkRequestBuilder.setNetworkSpecifier(Integer.toString(mActiveSubId));
         }
         NetworkRequest networkRequest = networkRequestBuilder.build();
-        mConnMgr.requestNetwork(
-                networkRequest,
-                mSuplConnectivityCallback,
-                mHandler,
-                SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS);
+        // Make sure we only have a single request.
+        if (mSuplConnectivityCallback != null) {
+            mConnMgr.unregisterNetworkCallback(mSuplConnectivityCallback);
+        }
+        mSuplConnectivityCallback = createSuplConnectivityCallback();
+        try {
+            mConnMgr.requestNetwork(
+                    networkRequest,
+                    mSuplConnectivityCallback,
+                    mHandler,
+                    SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS);
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Failed to request network.", e);
+            handleReleaseSuplConnection(GPS_AGPS_DATA_CONN_FAILED);
+        }
     }
 
     private int getNetworkCapability(int agpsType) {
@@ -619,7 +629,10 @@
         }
 
         mAGpsDataConnectionState = AGPS_DATA_CONNECTION_CLOSED;
-        mConnMgr.unregisterNetworkCallback(mSuplConnectivityCallback);
+        if (mSuplConnectivityCallback != null) {
+            mConnMgr.unregisterNetworkCallback(mSuplConnectivityCallback);
+            mSuplConnectivityCallback = null;
+        }
         switch (agpsDataConnStatus) {
             case GPS_AGPS_DATA_CONN_FAILED:
                 native_agps_data_conn_failed();
diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java
index 21beb96..1bcc21e 100644
--- a/services/core/java/com/android/server/logcat/LogcatManagerService.java
+++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java
@@ -410,7 +410,8 @@
     }
 
     private void processNewLogAccessRequest(LogAccessClient client) {
-        boolean isInstrumented = mActivityManagerInternal.isUidCurrentlyInstrumented(client.mUid);
+        boolean isInstrumented = mActivityManagerInternal.getInstrumentationSourceUid(client.mUid)
+                != android.os.Process.INVALID_UID;
 
         // The instrumented apks only run for testing, so we don't check user permission.
         if (isInstrumented) {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 9042326..6dc1f37 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -656,7 +656,6 @@
 
     private int mWarnRemoteViewsSizeBytes;
     private int mStripRemoteViewsSizeBytes;
-    private boolean mForceUserSetOnUpgrade;
 
     private MetricsLogger mMetricsLogger;
     private NotificationChannelLogger mNotificationChannelLogger;
@@ -1982,7 +1981,6 @@
         }
     }
 
-    private LockPatternUtils mLockPatternUtils;
     private StrongAuthTracker mStrongAuthTracker;
 
     public NotificationManagerService(Context context) {
@@ -2211,7 +2209,6 @@
         mPlatformCompat = IPlatformCompat.Stub.asInterface(
                 ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
 
-        mLockPatternUtils = new LockPatternUtils(getContext());
         mStrongAuthTracker = new StrongAuthTracker(getContext());
         mUiHandler = new Handler(UiThread.get().getLooper());
         String[] extractorNames;
@@ -2284,6 +2281,7 @@
                 mNotificationChannelLogger,
                 mAppOps,
                 new SysUiStatsEvent.BuilderFactory());
+        mPreferencesHelper.updateFixedImportance(mUm.getUsers());
         mRankingHelper = new RankingHelper(getContext(),
                 mRankingHandler,
                 mPreferencesHelper,
@@ -2471,9 +2469,6 @@
 
         WorkerHandler handler = new WorkerHandler(Looper.myLooper());
 
-        mForceUserSetOnUpgrade = getContext().getResources().getBoolean(
-                R.bool.config_notificationForceUserSetOnUpgrade);
-
         init(handler, new RankingHandlerWorker(mRankingThread.getLooper()),
                 AppGlobals.getPackageManager(), getContext().getPackageManager(),
                 getLocalService(LightsManager.class),
@@ -2502,8 +2497,7 @@
                 LocalServices.getService(ActivityManagerInternal.class),
                 createToastRateLimiter(), new PermissionHelper(LocalServices.getService(
                         PermissionManagerServiceInternal.class), AppGlobals.getPackageManager(),
-                        AppGlobals.getPermissionManager(),
-                        mForceUserSetOnUpgrade),
+                        AppGlobals.getPermissionManager()),
                 LocalServices.getService(UsageStatsManagerInternal.class),
                 getContext().getSystemService(TelecomManager.class),
                 new NotificationChannelLoggerImpl());
@@ -2713,7 +2707,7 @@
                 bubbsExtractor.setShortcutHelper(mShortcutHelper);
             }
             registerNotificationPreferencesPullers();
-            mLockPatternUtils.registerStrongAuthTracker(mStrongAuthTracker);
+            new LockPatternUtils(getContext()).registerStrongAuthTracker(mStrongAuthTracker);
         } else if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
             // This observer will force an update when observe is called, causing us to
             // bind to listener services.
@@ -3632,6 +3626,12 @@
         }
 
         @Override
+        public boolean isImportanceLocked(String pkg, int uid) {
+            checkCallerIsSystem();
+            return mPreferencesHelper.isImportanceLocked(pkg, uid);
+        }
+
+        @Override
         public boolean canShowBadge(String pkg, int uid) {
             checkCallerIsSystem();
             return mPreferencesHelper.canShowBadge(pkg, uid);
@@ -6146,7 +6146,6 @@
                     pw.println("  mMaxPackageEnqueueRate=" + mMaxPackageEnqueueRate);
                     pw.println("  hideSilentStatusBar="
                             + mPreferencesHelper.shouldHideSilentStatusIcons());
-                    pw.println("  mForceUserSetOnUpgrade=" + mForceUserSetOnUpgrade);
                 }
                 pw.println("  mArchive=" + mArchive.toString());
                 mArchive.dumpImpl(pw, filter);
diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java
index b2fee1e..09ed567 100644
--- a/services/core/java/com/android/server/notification/PermissionHelper.java
+++ b/services/core/java/com/android/server/notification/PermissionHelper.java
@@ -16,7 +16,6 @@
 
 package com.android.server.notification;
 
-import static android.content.pm.PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED;
 import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
@@ -55,14 +54,12 @@
     private final PermissionManagerServiceInternal mPmi;
     private final IPackageManager mPackageManager;
     private final IPermissionManager mPermManager;
-    private final boolean mForceUserSetOnUpgrade;
 
     public PermissionHelper(PermissionManagerServiceInternal pmi, IPackageManager packageManager,
-            IPermissionManager permManager, boolean forceUserSetOnUpgrade) {
+            IPermissionManager permManager) {
         mPmi = pmi;
         mPackageManager = packageManager;
         mPermManager = permManager;
-        mForceUserSetOnUpgrade = forceUserSetOnUpgrade;
     }
 
     /**
@@ -72,8 +69,7 @@
     public boolean hasPermission(int uid) {
         final long callingId = Binder.clearCallingIdentity();
         try {
-            return mPmi.checkPostNotificationsPermissionGrantedOrLegacyAccess(uid)
-                    == PERMISSION_GRANTED;
+            return mPmi.checkUidPermission(uid, NOTIFICATION_PERMISSION) == PERMISSION_GRANTED;
         } finally {
             Binder.restoreCallingIdentity(callingId);
         }
@@ -153,21 +149,13 @@
     }
 
     /**
-     * @see setNotificationPermission(String, int, boolean, boolean, boolean)
-     */
-    public void setNotificationPermission(String packageName, @UserIdInt int userId, boolean grant,
-            boolean userSet) {
-        setNotificationPermission(packageName, userId, grant, userSet, false);
-    }
-
-    /**
      * Grants or revokes the notification permission for a given package/user. UserSet should
      * only be true if this method is being called to migrate existing user choice, because it
      * can prevent the user from seeing the in app permission dialog. Must not be called
      * with a lock held.
      */
     public void setNotificationPermission(String packageName, @UserIdInt int userId, boolean grant,
-            boolean userSet, boolean reviewRequired) {
+            boolean userSet) {
         final long callingId = Binder.clearCallingIdentity();
         try {
             // Do not change the permission if the package doesn't request it, do not change fixed
@@ -181,7 +169,7 @@
 
             boolean currentlyGranted = mPmi.checkPermission(packageName, NOTIFICATION_PERMISSION,
                     userId) != PackageManager.PERMISSION_DENIED;
-            if (grant && !reviewRequired && !currentlyGranted) {
+            if (grant && !currentlyGranted) {
                 mPermManager.grantRuntimePermission(packageName, NOTIFICATION_PERMISSION, userId);
             } else if (!grant && currentlyGranted) {
                 mPermManager.revokeRuntimePermission(packageName, NOTIFICATION_PERMISSION,
@@ -189,12 +177,10 @@
             }
             if (userSet) {
                 mPermManager.updatePermissionFlags(packageName, NOTIFICATION_PERMISSION,
-                        FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_REVIEW_REQUIRED,
-                        FLAG_PERMISSION_USER_SET, true, userId);
-            } else if (reviewRequired) {
+                        FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, userId);
+            } else {
                 mPermManager.updatePermissionFlags(packageName, NOTIFICATION_PERMISSION,
-                        FLAG_PERMISSION_REVIEW_REQUIRED, FLAG_PERMISSION_REVIEW_REQUIRED, true,
-                        userId);
+                        0, FLAG_PERMISSION_USER_SET, true, userId);
             }
         } catch (RemoteException e) {
             Slog.e(TAG, "Could not reach system server", e);
@@ -212,9 +198,8 @@
             return;
         }
         if (!isPermissionFixed(pkgPerm.packageName, pkgPerm.userId)) {
-            boolean userSet = mForceUserSetOnUpgrade ? true : pkgPerm.userModifiedSettings;
             setNotificationPermission(pkgPerm.packageName, pkgPerm.userId, pkgPerm.granted,
-                    userSet, !userSet);
+                    true /* userSet always true on upgrade */);
         }
     }
 
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 4e3fbaa..97133a5 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -42,8 +42,10 @@
 import android.app.NotificationManager;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
+import android.content.pm.UserInfo;
 import android.metrics.LogMaker;
 import android.os.Binder;
 import android.os.Build;
@@ -146,7 +148,6 @@
     static final boolean DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS = false;
     private static final boolean DEFAULT_SHOW_BADGE = true;
 
-    private static final boolean DEFAULT_OEM_LOCKED_IMPORTANCE  = false;
     private static final boolean DEFAULT_APP_LOCKED_IMPORTANCE  = false;
 
     static final boolean DEFAULT_BUBBLES_ENABLED = true;
@@ -193,8 +194,6 @@
 
     private boolean mAllowInvalidShortcuts = false;
 
-    private Map<String, List<String>> mOemLockedApps = new HashMap();
-
     public PreferencesHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
             ZenModeHelper zenHelper, PermissionHelper permHelper,
             NotificationChannelLogger notificationChannelLogger,
@@ -411,7 +410,7 @@
                     channel.populateFromXml(parser);
                 }
                 channel.setImportanceLockedByCriticalDeviceFunction(
-                        r.defaultAppLockedImportance);
+                        r.defaultAppLockedImportance || r.fixedImportance);
 
                 if (isShortcutOk(channel) && isDeletionOk(channel)) {
                     r.channels.put(id, channel);
@@ -484,14 +483,6 @@
             r.visibility = visibility;
             r.showBadge = showBadge;
             r.bubblePreference = bubblePreference;
-            if (mOemLockedApps.containsKey(r.pkg)) {
-                List<String> channels = mOemLockedApps.get(r.pkg);
-                if (channels == null || channels.isEmpty()) {
-                    r.oemLockedImportance = true;
-                } else {
-                    r.oemLockedChannels = channels;
-                }
-            }
 
             try {
                 createDefaultChannelIfNeededLocked(r);
@@ -818,6 +809,13 @@
         }
     }
 
+    boolean isImportanceLocked(String pkg, int uid) {
+        synchronized (mPackagePreferences) {
+            PackagePreferences r = getOrCreatePackagePreferencesLocked(pkg, uid);
+            return r.fixedImportance || r.defaultAppLockedImportance;
+        }
+    }
+
     @Override
     public boolean isGroupBlocked(String packageName, int uid, String groupId) {
         if (groupId == null) {
@@ -1008,7 +1006,7 @@
                 clearLockedFieldsLocked(channel);
 
                 channel.setImportanceLockedByCriticalDeviceFunction(
-                        r.defaultAppLockedImportance);
+                        r.defaultAppLockedImportance || r.fixedImportance);
 
                 if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
                     channel.setLockscreenVisibility(
@@ -1090,8 +1088,7 @@
                 updatedChannel.unlockFields(updatedChannel.getUserLockedFields());
             }
 
-            if ((mPermissionHelper.isPermissionFixed(r.pkg, UserHandle.getUserId(r.uid))
-                    || channel.isImportanceLockedByCriticalDeviceFunction())
+            if (channel.isImportanceLockedByCriticalDeviceFunction()
                     && !(channel.isBlockable() || channel.getImportance() == IMPORTANCE_NONE)) {
                 updatedChannel.setImportance(channel.getImportance());
             }
@@ -1267,6 +1264,28 @@
         mHideSilentStatusBarIcons = hide;
     }
 
+    public void updateFixedImportance(List<UserInfo> users) {
+        for (UserInfo user : users) {
+            List<PackageInfo> packages = mPm.getInstalledPackagesAsUser(
+                    PackageManager.PackageInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY),
+                    user.getUserHandle().getIdentifier());
+            for (PackageInfo pi : packages) {
+                boolean fixed = mPermissionHelper.isPermissionFixed(
+                        pi.packageName, user.getUserHandle().getIdentifier());
+                if (fixed) {
+                    synchronized (mPackagePreferences) {
+                        PackagePreferences p = getOrCreatePackagePreferencesLocked(
+                                pi.packageName, pi.applicationInfo.uid);
+                        p.fixedImportance = true;
+                        for (NotificationChannel channel : p.channels.values()) {
+                            channel.setImportanceLockedByCriticalDeviceFunction(true);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     public void updateDefaultApps(int userId, ArraySet<String> toRemove,
             ArraySet<Pair<String, Integer>> toAdd) {
         synchronized (mPackagePreferences) {
@@ -1274,8 +1293,10 @@
                 if (userId == UserHandle.getUserId(p.uid)) {
                     if (toRemove != null && toRemove.contains(p.pkg)) {
                         p.defaultAppLockedImportance = false;
-                        for (NotificationChannel channel : p.channels.values()) {
-                            channel.setImportanceLockedByCriticalDeviceFunction(false);
+                        if (!p.fixedImportance) {
+                            for (NotificationChannel channel : p.channels.values()) {
+                                channel.setImportanceLockedByCriticalDeviceFunction(false);
+                            }
                         }
                     }
                 }
@@ -1934,13 +1955,9 @@
                     pw.print(" defaultAppLocked=");
                     pw.print(r.defaultAppLockedImportance);
                 }
-                if (r.oemLockedImportance != DEFAULT_OEM_LOCKED_IMPORTANCE) {
-                    pw.print(" oemLocked=");
-                    pw.print(r.oemLockedImportance);
-                }
-                if (!r.oemLockedChannels.isEmpty()) {
-                    pw.print(" futureLockedChannels=");
-                    pw.print(r.oemLockedChannels);
+                if (r.fixedImportance != DEFAULT_APP_LOCKED_IMPORTANCE) {
+                    pw.print(" fixedImportance=");
+                    pw.print(r.fixedImportance);
                 }
                 pw.println();
                 for (NotificationChannel channel : r.channels.values()) {
@@ -2682,9 +2699,8 @@
         int lockedAppFields = DEFAULT_LOCKED_APP_FIELDS;
         // these fields are loaded on boot from a different source of truth and so are not
         // written to notification policy xml
-        boolean oemLockedImportance = DEFAULT_OEM_LOCKED_IMPORTANCE;
-        List<String> oemLockedChannels = new ArrayList<>();
         boolean defaultAppLockedImportance = DEFAULT_APP_LOCKED_IMPORTANCE;
+        boolean fixedImportance = DEFAULT_APP_LOCKED_IMPORTANCE;
 
         boolean hasSentInvalidMessage = false;
         boolean hasSentValidMessage = false;
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index beca714..1a6155b 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -121,17 +121,19 @@
         public final File apexDirectory;
         public final File preInstalledApexPath;
         public final File apexFile;
+        public final boolean activeApexChanged;
 
         private ActiveApexInfo(File apexDirectory, File preInstalledApexPath, File apexFile) {
-            this(null, apexDirectory, preInstalledApexPath, apexFile);
+            this(null, apexDirectory, preInstalledApexPath, apexFile, false);
         }
 
         private ActiveApexInfo(@Nullable String apexModuleName, File apexDirectory,
-                File preInstalledApexPath, File apexFile) {
+                File preInstalledApexPath, File apexFile, boolean activeApexChanged) {
             this.apexModuleName = apexModuleName;
             this.apexDirectory = apexDirectory;
             this.preInstalledApexPath = preInstalledApexPath;
             this.apexFile = apexFile;
+            this.activeApexChanged = activeApexChanged;
         }
 
         private ActiveApexInfo(ApexInfo apexInfo) {
@@ -140,7 +142,8 @@
                     new File(Environment.getApexDirectory() + File.separator
                             + apexInfo.moduleName),
                     new File(apexInfo.preinstalledModulePath),
-                    new File(apexInfo.modulePath));
+                    new File(apexInfo.modulePath),
+                    apexInfo.activeApexChanged);
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java
index 7004c73..78e7b0a 100644
--- a/services/core/java/com/android/server/pm/AppsFilterBase.java
+++ b/services/core/java/com/android/server/pm/AppsFilterBase.java
@@ -26,6 +26,7 @@
 import android.annotation.Nullable;
 import android.content.pm.SigningDetails;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.Process;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -53,7 +54,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Objects;
-import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * AppsFilter is the entity responsible for filtering visibility between apps based on declarations
@@ -68,6 +69,11 @@
     protected static final boolean DEBUG_LOGGING = false;
     public static final boolean DEBUG_TRACING = false;
 
+    // Allow some time for cache rebuilds.
+    protected static final int CACHE_REBUILD_DELAY_MIN_MS = 10000;
+    // With each new rebuild the delay doubles until it reaches max delay.
+    protected static final int CACHE_REBUILD_DELAY_MAX_MS = 10000;
+
     /**
      * This contains a list of app UIDs that are implicitly queryable because another app explicitly
      * interacted with it. For example, if application A starts a service in application B,
@@ -122,10 +128,10 @@
     protected SnapshotCache<WatchedSparseSetArray<Integer>> mQueryableViaUsesLibrarySnapshot;
 
     /**
-     * Executor for running reasonably short background tasks such as building the initial
+     * Handler for running reasonably short background tasks such as building the initial
      * visibility cache.
      */
-    protected Executor mBackgroundExecutor;
+    protected Handler mBackgroundHandler;
 
     /**
      * Pending full recompute of mQueriesViaComponent. Occurs when a package adds a new set of
@@ -133,7 +139,7 @@
      * computationally expensive recomputing.
      * Full recompute is done lazily at the point when we use mQueriesViaComponent to filter apps.
      */
-    protected boolean mQueriesViaComponentRequireRecompute = false;
+    protected AtomicBoolean mQueriesViaComponentRequireRecompute = new AtomicBoolean(false);
 
     /**
      * A set of App IDs that are always queryable by any package, regardless of their manifest
@@ -173,7 +179,7 @@
      * {@link #shouldFilterApplicationInternal(PackageDataSnapshot, int, Object,
      * PackageStateInternal, int)} call.
      * NOTE: It can only be relied upon after the system is ready to avoid unnecessary update on
-     * initial scam and is empty until {@link #mSystemReady} is true.
+     * initial scam and is empty until {@link #mCacheReady} is true.
      */
     @NonNull
     @Watched
@@ -181,7 +187,11 @@
     @NonNull
     protected SnapshotCache<WatchedSparseBooleanMatrix> mShouldFilterCacheSnapshot;
 
-    protected volatile boolean mSystemReady = false;
+    protected volatile boolean mCacheReady = false;
+
+    protected static final boolean CACHE_VALID = true;
+    protected static final boolean CACHE_INVALID = false;
+    protected AtomicBoolean mCacheValid = new AtomicBoolean(CACHE_INVALID);
 
     protected boolean isForceQueryable(int callingAppId) {
         return mForceQueryable.contains(callingAppId);
@@ -312,7 +322,7 @@
                     || callingAppId == targetPkgSetting.getAppId()) {
                 return false;
             }
-            if (mSystemReady) { // use cache
+            if (mCacheReady) { // use cache
                 if (!shouldFilterApplicationUsingCache(callingUid,
                         targetPkgSetting.getAppId(),
                         userId)) {
@@ -506,7 +516,7 @@
                 if (DEBUG_TRACING) {
                     Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "mQueriesViaComponent");
                 }
-                if (!mQueriesViaComponentRequireRecompute) {
+                if (!mQueriesViaComponentRequireRecompute.get()) {
                     if (isQueryableViaComponent(callingAppId, targetAppId)) {
                         if (DEBUG_LOGGING) {
                             log(callingSetting, targetPkgSetting, "queries component");
@@ -702,17 +712,34 @@
             }
         }
         pw.println("  system apps queryable: " + mSystemAppsQueryable);
-        dumpQueryables(pw, filteringAppId, users, expandPackages);
+        dumpForceQueryable(pw, filteringAppId, expandPackages);
+        dumpQueriesViaPackage(pw, filteringAppId, expandPackages);
+        dumpQueriesViaComponent(pw, filteringAppId, expandPackages);
+        dumpQueriesViaImplicitlyQueryable(pw, filteringAppId, users, expandPackages);
+        dumpQueriesViaUsesLibrary(pw, filteringAppId, expandPackages);
     }
 
-    protected void dumpQueryables(PrintWriter pw, @Nullable Integer filteringAppId, int[] users,
+    protected void dumpForceQueryable(PrintWriter pw, @Nullable Integer filteringAppId,
             ToString<Integer> expandPackages) {
+        pw.println("  queries via forceQueryable:");
         dumpPackageSet(pw, filteringAppId, mForceQueryable.untrackedStorage(),
                 "forceQueryable", "  ", expandPackages);
+    }
+
+    protected void dumpQueriesViaPackage(PrintWriter pw, @Nullable Integer filteringAppId,
+            ToString<Integer> expandPackages) {
         pw.println("  queries via package name:");
         dumpQueriesMap(pw, filteringAppId, mQueriesViaPackage, "    ", expandPackages);
+    }
+
+    protected void dumpQueriesViaComponent(PrintWriter pw, @Nullable Integer filteringAppId,
+            ToString<Integer> expandPackages) {
         pw.println("  queries via component:");
         dumpQueriesMap(pw, filteringAppId, mQueriesViaComponent, "    ", expandPackages);
+    }
+
+    protected void dumpQueriesViaImplicitlyQueryable(PrintWriter pw,
+            @Nullable Integer filteringAppId, int[] users, ToString<Integer> expandPackages) {
         pw.println("  queryable via interaction:");
         for (int user : users) {
             pw.append("    User ").append(Integer.toString(user)).println(":");
@@ -723,6 +750,10 @@
                     filteringAppId == null ? null : UserHandle.getUid(user, filteringAppId),
                     mRetainedImplicitlyQueryable, "      ", expandPackages);
         }
+    }
+
+    protected void dumpQueriesViaUsesLibrary(PrintWriter pw, @Nullable Integer filteringAppId,
+            ToString<Integer> expandPackages) {
         pw.println("  queryable via uses-library:");
         dumpQueriesMap(pw, filteringAppId, mQueryableViaUsesLibrary, "    ",
                 expandPackages);
diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java
index 952711d..9fddc76 100644
--- a/services/core/java/com/android/server/pm/AppsFilterImpl.java
+++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java
@@ -35,6 +35,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.SigningDetails;
 import android.content.pm.UserInfo;
+import android.os.Handler;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
@@ -70,7 +71,6 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
-import java.util.concurrent.Executor;
 
 /**
  * Implementation of the methods that update the internal structures of AppsFilter. Because of the
@@ -83,7 +83,7 @@
      * A cached snapshot.
      */
     @NonNull
-    private SnapshotCache<AppsFilterSnapshot> mSnapshot;
+    private final SnapshotCache<AppsFilterSnapshot> mSnapshot;
 
     /**
      * Watchable machinery
@@ -142,18 +142,24 @@
         dispatchChange(this);
     }
 
+    private void invalidateCache(String reason) {
+        if (mCacheValid.compareAndSet(CACHE_VALID, CACHE_INVALID)) {
+            Slog.i(TAG, "Invalidating cache: " + reason);
+        }
+    }
+
     @VisibleForTesting(visibility = PRIVATE)
     AppsFilterImpl(FeatureConfig featureConfig,
             String[] forceQueryableList,
             boolean systemAppsQueryable,
             @Nullable OverlayReferenceMapper.Provider overlayProvider,
-            Executor backgroundExecutor) {
+            Handler backgroundHandler) {
         mFeatureConfig = featureConfig;
         mForceQueryableByDevicePackageNames = forceQueryableList;
         mSystemAppsQueryable = systemAppsQueryable;
         mOverlayReferenceMapper = new OverlayReferenceMapper(true /*deferRebuild*/,
                 overlayProvider);
-        mBackgroundExecutor = backgroundExecutor;
+        mBackgroundHandler = backgroundHandler;
         mShouldFilterCache = new WatchedSparseBooleanMatrix();
         mShouldFilterCacheSnapshot = new SnapshotCache.Auto<>(
                 mShouldFilterCache, mShouldFilterCache, "AppsFilter.mShouldFilterCache");
@@ -371,7 +377,7 @@
         }
         AppsFilterImpl appsFilter = new AppsFilterImpl(featureConfig,
                 forcedQueryablePackageNames, forceSystemAppsQueryable, null,
-                injector.getBackgroundExecutor());
+                injector.getBackgroundHandler());
         featureConfig.setAppsFilter(appsFilter);
         return appsFilter;
     }
@@ -394,7 +400,7 @@
             return false;
         }
         final boolean changed;
-        synchronized (mLock) {
+        synchronized (mImplicitlyQueryableLock) {
             changed = retainOnUpdate
                     ? mRetainedImplicitlyQueryable.add(recipientUid, visibleUid)
                     : mImplicitlyQueryable.add(recipientUid, visibleUid);
@@ -404,12 +410,13 @@
                     + recipientUid + " -> " + visibleUid);
         }
 
-        if (mSystemReady) {
+        if (mCacheReady) {
             synchronized (mCacheLock) {
-                // update the cache in a one-off manner since we've got all the information we
-                // need.
+                // Update the cache in a one-off manner since we've got all the information we need.
                 mShouldFilterCache.put(recipientUid, visibleUid, false);
             }
+        } else if (changed) {
+            invalidateCache("grantImplicitAccess: " + recipientUid + " -> " + visibleUid);
         }
         onChanged();
         return changed;
@@ -420,7 +427,6 @@
         mFeatureConfig.onSystemReady();
 
         updateEntireShouldFilterCacheAsync(pmInternal);
-        mSystemReady = true;
     }
 
     /**
@@ -444,7 +450,7 @@
             final UserInfo[] users = snapshot.getUserInfos();
             final ArraySet<String> additionalChangedPackages =
                     addPackageInternal(newPkgSetting, settings);
-            if (mSystemReady) {
+            if (mCacheReady) {
                 synchronized (mCacheLock) {
                     updateShouldFilterCacheForPackage(snapshot, null, newPkgSetting,
                             settings, users, USER_ALL, settings.size());
@@ -463,7 +469,9 @@
                         }
                     }
                 }
-            } // else, rebuild entire cache when system is ready
+            } else {
+                invalidateCache("addPackage: " + newPkgSetting.getPackageName());
+            }
         } finally {
             onChanged();
             if (DEBUG_TRACING) {
@@ -486,7 +494,7 @@
             // packages for signature matches
             for (PackageStateInternal setting : existingSettings.values()) {
                 if (isSystemSigned(mSystemSigningDetails, setting)) {
-                    synchronized (mLock) {
+                    synchronized (mForceQueryableLock) {
                         mForceQueryable.add(setting.getAppId());
                     }
                 }
@@ -498,13 +506,18 @@
             return null;
         }
 
-        synchronized (mLock) {
-            if (mProtectedBroadcasts.addAll(newPkg.getProtectedBroadcasts())) {
-                mQueriesViaComponentRequireRecompute = true;
-            }
+        final boolean protectedBroadcastsChanged;
+        synchronized (mProtectedBroadcastsLock) {
+            protectedBroadcastsChanged =
+                    mProtectedBroadcasts.addAll(newPkg.getProtectedBroadcasts());
+        }
+        if (protectedBroadcastsChanged) {
+            mQueriesViaComponentRequireRecompute.set(true);
+        }
 
-            final boolean newIsForceQueryable =
-                    mForceQueryable.contains(newPkgSetting.getAppId())
+        final boolean newIsForceQueryable;
+        synchronized (mForceQueryableLock) {
+            newIsForceQueryable = mForceQueryable.contains(newPkgSetting.getAppId())
                             /* shared user that is already force queryable */
                             || newPkgSetting.isForceQueryableOverride() /* adb override */
                             || (newPkgSetting.isSystem() && (mSystemAppsQueryable
@@ -516,58 +529,77 @@
                     && isSystemSigned(mSystemSigningDetails, newPkgSetting))) {
                 mForceQueryable.add(newPkgSetting.getAppId());
             }
+        }
 
-            for (int i = existingSettings.size() - 1; i >= 0; i--) {
-                final PackageStateInternal existingSetting = existingSettings.valueAt(i);
-                if (existingSetting.getAppId() == newPkgSetting.getAppId()
-                        || existingSetting.getPkg()
-                        == null) {
-                    continue;
-                }
-                final AndroidPackage existingPkg = existingSetting.getPkg();
-                // let's evaluate the ability of already added packages to see this new package
-                if (!newIsForceQueryable) {
-                    if (!mQueriesViaComponentRequireRecompute && canQueryViaComponents(existingPkg,
-                            newPkg, mProtectedBroadcasts)) {
+        for (int i = existingSettings.size() - 1; i >= 0; i--) {
+            final PackageStateInternal existingSetting = existingSettings.valueAt(i);
+            if (existingSetting.getAppId() == newPkgSetting.getAppId()
+                    || existingSetting.getPkg()
+                    == null) {
+                continue;
+            }
+            final AndroidPackage existingPkg = existingSetting.getPkg();
+            // let's evaluate the ability of already added packages to see this new package
+            if (!newIsForceQueryable) {
+                if (!mQueriesViaComponentRequireRecompute.get()
+                        && canQueryViaComponents(existingPkg, newPkg, mProtectedBroadcasts)) {
+                    synchronized (mQueriesViaComponentLock) {
                         mQueriesViaComponent.add(existingSetting.getAppId(),
                                 newPkgSetting.getAppId());
                     }
-                    if (canQueryViaPackage(existingPkg, newPkg)
-                            || canQueryAsInstaller(existingSetting, newPkg)) {
+                }
+                if (canQueryViaPackage(existingPkg, newPkg)
+                        || canQueryAsInstaller(existingSetting, newPkg)) {
+                    synchronized (mQueriesViaPackageLock) {
                         mQueriesViaPackage.add(existingSetting.getAppId(),
                                 newPkgSetting.getAppId());
                     }
-                    if (canQueryViaUsesLibrary(existingPkg, newPkg)) {
+                }
+                if (canQueryViaUsesLibrary(existingPkg, newPkg)) {
+                    synchronized (mQueryableViaUsesLibraryLock) {
                         mQueryableViaUsesLibrary.add(existingSetting.getAppId(),
                                 newPkgSetting.getAppId());
                     }
                 }
-                // now we'll evaluate our new package's ability to see existing packages
-                if (!mForceQueryable.contains(existingSetting.getAppId())) {
-                    if (!mQueriesViaComponentRequireRecompute && canQueryViaComponents(newPkg,
-                            existingPkg, mProtectedBroadcasts)) {
+            }
+            final boolean existingIsForceQueryable;
+            synchronized (mForceQueryableLock) {
+                existingIsForceQueryable = mForceQueryable.contains(existingSetting.getAppId());
+            }
+            // now we'll evaluate our new package's ability to see existing packages
+            if (!existingIsForceQueryable) {
+                if (!mQueriesViaComponentRequireRecompute.get()
+                        && canQueryViaComponents(newPkg, existingPkg, mProtectedBroadcasts)) {
+                    synchronized (mQueriesViaComponentLock) {
                         mQueriesViaComponent.add(newPkgSetting.getAppId(),
                                 existingSetting.getAppId());
                     }
-                    if (canQueryViaPackage(newPkg, existingPkg)
-                            || canQueryAsInstaller(newPkgSetting, existingPkg)) {
+                }
+                if (canQueryViaPackage(newPkg, existingPkg)
+                        || canQueryAsInstaller(newPkgSetting, existingPkg)) {
+                    synchronized (mQueriesViaPackageLock) {
                         mQueriesViaPackage.add(newPkgSetting.getAppId(),
                                 existingSetting.getAppId());
                     }
-                    if (canQueryViaUsesLibrary(newPkg, existingPkg)) {
+                }
+                if (canQueryViaUsesLibrary(newPkg, existingPkg)) {
+                    synchronized (mQueryableViaUsesLibraryLock) {
                         mQueryableViaUsesLibrary.add(newPkgSetting.getAppId(),
                                 existingSetting.getAppId());
                     }
                 }
-                // if either package instruments the other, mark both as visible to one another
-                if (newPkgSetting.getPkg() != null && existingSetting.getPkg() != null
-                        && (pkgInstruments(newPkgSetting.getPkg(), existingSetting.getPkg())
-                        || pkgInstruments(existingSetting.getPkg(), newPkgSetting.getPkg()))) {
+            }
+            // if either package instruments the other, mark both as visible to one another
+            if (newPkgSetting.getPkg() != null && existingSetting.getPkg() != null
+                    && (pkgInstruments(newPkgSetting.getPkg(), existingSetting.getPkg())
+                    || pkgInstruments(existingSetting.getPkg(), newPkgSetting.getPkg()))) {
+                synchronized (mQueriesViaPackageLock) {
                     mQueriesViaPackage.add(newPkgSetting.getAppId(), existingSetting.getAppId());
                     mQueriesViaPackage.add(existingSetting.getAppId(), newPkgSetting.getAppId());
                 }
             }
         }
+
         int existingSize = existingSettings.size();
         ArrayMap<String, AndroidPackage> existingPkgs = new ArrayMap<>(existingSize);
         for (int index = 0; index < existingSize; index++) {
@@ -586,9 +618,6 @@
     }
 
     private void removeAppIdFromVisibilityCache(int appId) {
-        if (!mSystemReady) {
-            return;
-        }
         synchronized (mCacheLock) {
             for (int i = 0; i < mShouldFilterCache.size(); i++) {
                 if (UserHandle.getAppId(mShouldFilterCache.keyAt(i)) == appId) {
@@ -642,7 +671,17 @@
     }
 
     private void updateEntireShouldFilterCacheAsync(PackageManagerInternal pmInternal) {
-        mBackgroundExecutor.execute(() -> {
+        updateEntireShouldFilterCacheAsync(pmInternal, CACHE_REBUILD_DELAY_MIN_MS);
+    }
+
+    private void updateEntireShouldFilterCacheAsync(PackageManagerInternal pmInternal,
+            long delayMs) {
+        mBackgroundHandler.postDelayed(() -> {
+            if (!mCacheValid.compareAndSet(CACHE_INVALID, CACHE_VALID)) {
+                // Cache is already valid.
+                return;
+            }
+
             final ArrayMap<String, AndroidPackage> packagesCache = new ArrayMap<>();
             final UserInfo[][] usersRef = new UserInfo[1][];
             final PackageDataSnapshot snapshot = pmInternal.snapshot();
@@ -661,18 +700,27 @@
 
             updateEntireShouldFilterCacheInner(snapshot, settings, usersRef[0], USER_ALL);
             onChanged();
-        });
+
+            if (!mCacheValid.compareAndSet(CACHE_VALID, CACHE_VALID)) {
+                Slog.i(TAG, "Cache invalidated while building, retrying.");
+                updateEntireShouldFilterCacheAsync(pmInternal,
+                        Math.min(delayMs * 2, CACHE_REBUILD_DELAY_MAX_MS));
+                return;
+            }
+
+            mCacheReady = true;
+        }, delayMs);
     }
 
     public void onUserCreated(PackageDataSnapshot snapshot, int newUserId) {
-        if (!mSystemReady) {
+        if (!mCacheReady) {
             return;
         }
         updateEntireShouldFilterCache(snapshot, newUserId);
     }
 
     public void onUserDeleted(@UserIdInt int userId) {
-        if (!mSystemReady) {
+        if (!mCacheReady) {
             return;
         }
         removeShouldFilterCacheForUser(userId);
@@ -681,7 +729,7 @@
 
     private void updateShouldFilterCacheForPackage(PackageDataSnapshot snapshot,
             String packageName) {
-        if (!mSystemReady) {
+        if (!mCacheReady) {
             return;
         }
         final ArrayMap<String, ? extends PackageStateInternal> settings =
@@ -772,7 +820,7 @@
     private void collectProtectedBroadcasts(
             ArrayMap<String, ? extends PackageStateInternal> existingSettings,
             @Nullable String excludePackage) {
-        synchronized (mLock) {
+        synchronized (mProtectedBroadcastsLock) {
             mProtectedBroadcasts.clear();
             for (int i = existingSettings.size() - 1; i >= 0; i--) {
                 PackageStateInternal setting = existingSettings.valueAt(i);
@@ -806,30 +854,37 @@
      */
     private void recomputeComponentVisibility(
             ArrayMap<String, ? extends PackageStateInternal> existingSettings) {
-        synchronized (mLock) {
+        synchronized (mQueriesViaComponentLock) {
             mQueriesViaComponent.clear();
-            for (int i = existingSettings.size() - 1; i >= 0; i--) {
-                PackageStateInternal setting = existingSettings.valueAt(i);
-                if (setting.getPkg() == null || requestsQueryAllPackages(setting.getPkg())) {
+        }
+        for (int i = existingSettings.size() - 1; i >= 0; i--) {
+            PackageStateInternal setting = existingSettings.valueAt(i);
+            if (setting.getPkg() == null || requestsQueryAllPackages(setting.getPkg())) {
+                continue;
+            }
+            for (int j = existingSettings.size() - 1; j >= 0; j--) {
+                if (i == j) {
                     continue;
                 }
-                for (int j = existingSettings.size() - 1; j >= 0; j--) {
-                    if (i == j) {
-                        continue;
-                    }
-                    final PackageStateInternal otherSetting = existingSettings.valueAt(j);
-                    if (otherSetting.getPkg() == null || mForceQueryable.contains(
-                            otherSetting.getAppId())) {
-                        continue;
-                    }
-                    if (canQueryViaComponents(setting.getPkg(), otherSetting.getPkg(),
-                            mProtectedBroadcasts)) {
+                final PackageStateInternal otherSetting = existingSettings.valueAt(j);
+                if (otherSetting.getPkg() == null || mForceQueryable.contains(
+                        otherSetting.getAppId())) {
+                    continue;
+                }
+                final boolean canQueryViaComponents;
+                synchronized (mProtectedBroadcastsLock) {
+                    canQueryViaComponents = canQueryViaComponents(setting.getPkg(),
+                            otherSetting.getPkg(), mProtectedBroadcasts);
+                }
+                if (canQueryViaComponents) {
+                    synchronized (mQueriesViaComponentLock) {
                         mQueriesViaComponent.add(setting.getAppId(), otherSetting.getAppId());
                     }
                 }
             }
         }
-        mQueriesViaComponentRequireRecompute = false;
+
+        mQueriesViaComponentRequireRecompute.set(false);
         onChanged();
     }
 
@@ -857,7 +912,7 @@
         final UserInfo[] users = snapshot.getUserInfos();
         final Collection<SharedUserSetting> sharedUserSettings = snapshot.getAllSharedUsers();
         final int userCount = users.length;
-        synchronized (mLock) {
+        synchronized (mImplicitlyQueryableLock) {
             for (int u = 0; u < userCount; u++) {
                 final int userId = users[u].id;
                 final int removingUid = UserHandle.getUid(userId, setting.getAppId());
@@ -877,39 +932,53 @@
                             mRetainedImplicitlyQueryable.keyAt(i), removingUid);
                 }
             }
+        }
 
-            if (!mQueriesViaComponentRequireRecompute) {
+        if (!mQueriesViaComponentRequireRecompute.get()) {
+            synchronized (mQueriesViaComponentLock) {
                 mQueriesViaComponent.remove(setting.getAppId());
                 for (int i = mQueriesViaComponent.size() - 1; i >= 0; i--) {
-                    mQueriesViaComponent.remove(mQueriesViaComponent.keyAt(i),
-                            setting.getAppId());
+                    mQueriesViaComponent.remove(mQueriesViaComponent.keyAt(i), setting.getAppId());
                 }
             }
+        }
+
+        synchronized (mQueriesViaPackageLock) {
             mQueriesViaPackage.remove(setting.getAppId());
             for (int i = mQueriesViaPackage.size() - 1; i >= 0; i--) {
                 mQueriesViaPackage.remove(mQueriesViaPackage.keyAt(i),
                         setting.getAppId());
             }
+        }
+
+        synchronized (mQueryableViaUsesLibraryLock) {
             mQueryableViaUsesLibrary.remove(setting.getAppId());
             for (int i = mQueryableViaUsesLibrary.size() - 1; i >= 0; i--) {
                 mQueryableViaUsesLibrary.remove(mQueryableViaUsesLibrary.keyAt(i),
                         setting.getAppId());
             }
+        }
 
+        synchronized (mForceQueryableLock) {
             mForceQueryable.remove(setting.getAppId());
+        }
 
+        boolean protectedBroadcastsChanged = false;
+        synchronized (mProtectedBroadcastsLock) {
             if (setting.getPkg() != null
                     && !setting.getPkg().getProtectedBroadcasts().isEmpty()) {
                 final String removingPackageName = setting.getPkg().getPackageName();
-                final ArrayList<String> protectedBroadcasts = new ArrayList<>();
-                protectedBroadcasts.addAll(mProtectedBroadcasts.untrackedStorage());
+                final ArrayList<String> protectedBroadcasts = new ArrayList<>(
+                        mProtectedBroadcasts.untrackedStorage());
                 collectProtectedBroadcasts(settings, removingPackageName);
-                if (!mProtectedBroadcasts.containsAll(protectedBroadcasts)) {
-                    mQueriesViaComponentRequireRecompute = true;
-                }
+                protectedBroadcastsChanged = !mProtectedBroadcasts.containsAll(protectedBroadcasts);
             }
         }
 
+        if (protectedBroadcastsChanged) {
+            mQueriesViaComponentRequireRecompute.set(true);
+        }
+
         additionalChangedPackages = mOverlayReferenceMapper.removePkg(setting.getPackageName());
         mFeatureConfig.updatePackageState(setting, true /*removed*/);
 
@@ -929,25 +998,26 @@
             }
         }
 
-        removeAppIdFromVisibilityCache(setting.getAppId());
-        if (mSystemReady && setting.hasSharedUser()) {
-            final ArraySet<? extends PackageStateInternal> sharedUserPackages =
-                    getSharedUserPackages(setting.getSharedUserAppId(), sharedUserSettings);
-            for (int i = sharedUserPackages.size() - 1; i >= 0; i--) {
-                PackageStateInternal siblingSetting =
-                        sharedUserPackages.valueAt(i);
-                if (siblingSetting == setting) {
-                    continue;
-                }
-                synchronized (mCacheLock) {
-                    updateShouldFilterCacheForPackage(snapshot,
-                            setting.getPackageName(), siblingSetting, settings,
-                            users, USER_ALL, settings.size());
+        if (mCacheReady) {
+            removeAppIdFromVisibilityCache(setting.getAppId());
+
+            if (setting.hasSharedUser()) {
+                final ArraySet<? extends PackageStateInternal> sharedUserPackages =
+                        getSharedUserPackages(setting.getSharedUserAppId(), sharedUserSettings);
+                for (int i = sharedUserPackages.size() - 1; i >= 0; i--) {
+                    PackageStateInternal siblingSetting =
+                            sharedUserPackages.valueAt(i);
+                    if (siblingSetting == setting) {
+                        continue;
+                    }
+                    synchronized (mCacheLock) {
+                        updateShouldFilterCacheForPackage(snapshot,
+                                setting.getPackageName(), siblingSetting, settings,
+                                users, USER_ALL, settings.size());
+                    }
                 }
             }
-        }
 
-        if (mSystemReady) {
             if (additionalChangedPackages != null) {
                 for (int index = 0; index < additionalChangedPackages.size(); index++) {
                     String changedPackage = additionalChangedPackages.valueAt(index);
@@ -964,6 +1034,8 @@
                     }
                 }
             }
+        } else {
+            invalidateCache("removePackage: " + setting.getPackageName());
         }
         onChanged();
     }
diff --git a/services/core/java/com/android/server/pm/AppsFilterLocked.java b/services/core/java/com/android/server/pm/AppsFilterLocked.java
index e8e6cd9..30eb09e 100644
--- a/services/core/java/com/android/server/pm/AppsFilterLocked.java
+++ b/services/core/java/com/android/server/pm/AppsFilterLocked.java
@@ -26,52 +26,61 @@
  */
 abstract class AppsFilterLocked extends AppsFilterBase {
     /**
-     * Guards the accesses for the list/set class members
+     * The following locks guard the accesses for the list/set class members
      */
-    protected final Object mLock = new Object();
+    protected final Object mForceQueryableLock = new Object();
+    protected final Object mQueriesViaPackageLock = new Object();
+    protected final Object mQueriesViaComponentLock = new Object();
+    /**
+     * This lock covers both {@link #mImplicitlyQueryable} and {@link #mRetainedImplicitlyQueryable}
+      */
+    protected final Object mImplicitlyQueryableLock = new Object();
+    protected final Object mQueryableViaUsesLibraryLock = new Object();
+    protected final Object mProtectedBroadcastsLock = new Object();
+
     /**
      * Guards the access for {@link AppsFilterBase#mShouldFilterCache};
      */
-    protected Object mCacheLock = new Object();
+    protected final Object mCacheLock = new Object();
 
     @Override
     protected boolean isForceQueryable(int appId) {
-        synchronized (mLock) {
+        synchronized (mForceQueryableLock) {
             return super.isForceQueryable(appId);
         }
     }
 
     @Override
     protected boolean isQueryableViaPackage(int callingAppId, int targetAppId) {
-        synchronized (mLock) {
+        synchronized (mQueriesViaPackageLock) {
             return super.isQueryableViaPackage(callingAppId, targetAppId);
         }
     }
 
     @Override
     protected boolean isQueryableViaComponent(int callingAppId, int targetAppId) {
-        synchronized (mLock) {
+        synchronized (mQueriesViaComponentLock) {
             return super.isQueryableViaComponent(callingAppId, targetAppId);
         }
     }
 
     @Override
     protected boolean isImplicitlyQueryable(int callingAppId, int targetAppId) {
-        synchronized (mLock) {
+        synchronized (mImplicitlyQueryableLock) {
             return super.isImplicitlyQueryable(callingAppId, targetAppId);
         }
     }
 
     @Override
     protected boolean isRetainedImplicitlyQueryable(int callingAppId, int targetAppId) {
-        synchronized (mLock) {
+        synchronized (mImplicitlyQueryableLock) {
             return super.isRetainedImplicitlyQueryable(callingAppId, targetAppId);
         }
     }
 
     @Override
     protected boolean isQueryableViaUsesLibrary(int callingAppId, int targetAppId) {
-        synchronized (mLock) {
+        synchronized (mQueryableViaUsesLibraryLock) {
             return super.isQueryableViaUsesLibrary(callingAppId, targetAppId);
         }
     }
@@ -84,10 +93,42 @@
     }
 
     @Override
-    protected void dumpQueryables(PrintWriter pw, @Nullable Integer filteringAppId, int[] users,
+    protected void dumpForceQueryable(PrintWriter pw, @Nullable Integer filteringAppId,
             ToString<Integer> expandPackages) {
-        synchronized (mLock) {
-            dumpQueryables(pw, filteringAppId, users, expandPackages);
+        synchronized (mForceQueryableLock) {
+            super.dumpForceQueryable(pw, filteringAppId, expandPackages);
+        }
+    }
+
+    @Override
+    protected void dumpQueriesViaPackage(PrintWriter pw, @Nullable Integer filteringAppId,
+            ToString<Integer> expandPackages) {
+        synchronized (mQueriesViaPackageLock) {
+            super.dumpQueriesViaPackage(pw, filteringAppId, expandPackages);
+        }
+    }
+
+    @Override
+    protected void dumpQueriesViaComponent(PrintWriter pw, @Nullable Integer filteringAppId,
+            ToString<Integer> expandPackages) {
+        synchronized (mQueriesViaComponentLock) {
+            super.dumpQueriesViaComponent(pw, filteringAppId, expandPackages);
+        }
+    }
+
+    @Override
+    protected void dumpQueriesViaImplicitlyQueryable(PrintWriter pw,
+            @Nullable Integer filteringAppId, int[] users, ToString<Integer> expandPackages) {
+        synchronized (mImplicitlyQueryableLock) {
+            super.dumpQueriesViaImplicitlyQueryable(pw, filteringAppId, users, expandPackages);
+        }
+    }
+
+    @Override
+    protected void dumpQueriesViaUsesLibrary(PrintWriter pw, @Nullable Integer filteringAppId,
+            ToString<Integer> expandPackages) {
+        synchronized (mQueryableViaUsesLibraryLock) {
+            super.dumpQueriesViaUsesLibrary(pw, filteringAppId, expandPackages);
         }
     }
 }
diff --git a/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java b/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java
index c12aa6d..6ae6efa 100644
--- a/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java
+++ b/services/core/java/com/android/server/pm/AppsFilterSnapshotImpl.java
@@ -17,6 +17,7 @@
 package com.android.server.pm;
 
 import com.android.server.utils.SnapshotCache;
+import com.android.server.utils.WatchedSparseBooleanMatrix;
 
 import java.util.Arrays;
 
@@ -25,22 +26,32 @@
  */
 public final class AppsFilterSnapshotImpl extends AppsFilterBase {
     AppsFilterSnapshotImpl(AppsFilterImpl orig) {
-        synchronized (orig.mLock) {
+        synchronized (orig.mImplicitlyQueryableLock) {
             mImplicitlyQueryable = orig.mImplicitQueryableSnapshot.snapshot();
-            mImplicitQueryableSnapshot = new SnapshotCache.Sealed<>();
             mRetainedImplicitlyQueryable = orig.mRetainedImplicitlyQueryableSnapshot.snapshot();
-            mRetainedImplicitlyQueryableSnapshot = new SnapshotCache.Sealed<>();
-            mQueriesViaPackage = orig.mQueriesViaPackageSnapshot.snapshot();
-            mQueriesViaPackageSnapshot = new SnapshotCache.Sealed<>();
-            mQueriesViaComponent = orig.mQueriesViaComponentSnapshot.snapshot();
-            mQueriesViaComponentSnapshot = new SnapshotCache.Sealed<>();
-            mQueryableViaUsesLibrary = orig.mQueryableViaUsesLibrarySnapshot.snapshot();
-            mQueryableViaUsesLibrarySnapshot = new SnapshotCache.Sealed<>();
-            mForceQueryable = orig.mForceQueryableSnapshot.snapshot();
-            mForceQueryableSnapshot = new SnapshotCache.Sealed<>();
-            mProtectedBroadcasts = orig.mProtectedBroadcastsSnapshot.snapshot();
-            mProtectedBroadcastsSnapshot = new SnapshotCache.Sealed<>();
         }
+        mImplicitQueryableSnapshot = new SnapshotCache.Sealed<>();
+        mRetainedImplicitlyQueryableSnapshot = new SnapshotCache.Sealed<>();
+        synchronized (orig.mQueriesViaPackageLock) {
+            mQueriesViaPackage = orig.mQueriesViaPackageSnapshot.snapshot();
+        }
+        mQueriesViaPackageSnapshot = new SnapshotCache.Sealed<>();
+        synchronized (orig.mQueriesViaComponentLock) {
+            mQueriesViaComponent = orig.mQueriesViaComponentSnapshot.snapshot();
+        }
+        mQueriesViaComponentSnapshot = new SnapshotCache.Sealed<>();
+        synchronized (orig.mQueryableViaUsesLibraryLock) {
+            mQueryableViaUsesLibrary = orig.mQueryableViaUsesLibrarySnapshot.snapshot();
+        }
+        mQueryableViaUsesLibrarySnapshot = new SnapshotCache.Sealed<>();
+        synchronized (orig.mForceQueryableLock) {
+            mForceQueryable = orig.mForceQueryableSnapshot.snapshot();
+        }
+        mForceQueryableSnapshot = new SnapshotCache.Sealed<>();
+        synchronized (orig.mProtectedBroadcastsLock) {
+            mProtectedBroadcasts = orig.mProtectedBroadcastsSnapshot.snapshot();
+        }
+        mProtectedBroadcastsSnapshot = new SnapshotCache.Sealed<>();
         mQueriesViaComponentRequireRecompute = orig.mQueriesViaComponentRequireRecompute;
         mForceQueryableByDevicePackageNames =
                 Arrays.copyOf(orig.mForceQueryableByDevicePackageNames,
@@ -49,12 +60,18 @@
         mFeatureConfig = orig.mFeatureConfig.snapshot();
         mOverlayReferenceMapper = orig.mOverlayReferenceMapper;
         mSystemSigningDetails = orig.mSystemSigningDetails;
-        synchronized (orig.mCacheLock) {
-            mShouldFilterCache = orig.mShouldFilterCacheSnapshot.snapshot();
-            mShouldFilterCacheSnapshot = new SnapshotCache.Sealed<>();
-        }
 
-        mBackgroundExecutor = null;
-        mSystemReady = orig.mSystemReady;
+        mCacheReady = orig.mCacheReady;
+        if (mCacheReady) {
+            synchronized (orig.mCacheLock) {
+                mShouldFilterCache = orig.mShouldFilterCacheSnapshot.snapshot();
+            }
+        } else {
+            // cache is not ready, use an empty cache for the snapshot
+            mShouldFilterCache = new WatchedSparseBooleanMatrix();
+        }
+        mShouldFilterCacheSnapshot = new SnapshotCache.Sealed<>();
+
+        mBackgroundHandler = null;
     }
 }
diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
index d26a1ac..5a01ccb 100644
--- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java
+++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
@@ -17,6 +17,7 @@
 package com.android.server.pm;
 
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
+import static com.android.server.pm.dex.ArtStatsLogUtils.BackgroundDexoptJobStatsLogger;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -77,43 +78,46 @@
 
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    @VisibleForTesting
-    static final int JOB_IDLE_OPTIMIZE = 800;
-    @VisibleForTesting
-    static final int JOB_POST_BOOT_UPDATE = 801;
+    @VisibleForTesting static final int JOB_IDLE_OPTIMIZE = 800;
+    @VisibleForTesting static final int JOB_POST_BOOT_UPDATE = 801;
 
     private static final long IDLE_OPTIMIZATION_PERIOD = TimeUnit.DAYS.toMillis(1);
 
     private static final long CANCELLATION_WAIT_CHECK_INTERVAL_MS = 200;
 
-    private static ComponentName sDexoptServiceName = new ComponentName("android",
-            BackgroundDexOptJobService.class.getName());
+    private static ComponentName sDexoptServiceName =
+            new ComponentName("android", BackgroundDexOptJobService.class.getName());
 
     // Possible return codes of individual optimization steps.
     /** Ok status: Optimizations finished, All packages were processed, can continue */
-    private static final int STATUS_OK = 0;
+    public static final int STATUS_OK = 0;
     /** Optimizations should be aborted. Job scheduler requested it. */
-    private static final int STATUS_ABORT_BY_CANCELLATION = 1;
+    public static final int STATUS_ABORT_BY_CANCELLATION = 1;
     /** Optimizations should be aborted. No space left on device. */
-    private static final int STATUS_ABORT_NO_SPACE_LEFT = 2;
+    public static final int STATUS_ABORT_NO_SPACE_LEFT = 2;
     /** Optimizations should be aborted. Thermal throttling level too high. */
-    private static final int STATUS_ABORT_THERMAL = 3;
+    public static final int STATUS_ABORT_THERMAL = 3;
     /** Battery level too low */
-    private static final int STATUS_ABORT_BATTERY = 4;
-    /** {@link PackageDexOptimizer#DEX_OPT_FAILED} case */
-    private static final int STATUS_DEX_OPT_FAILED = 5;
+    public static final int STATUS_ABORT_BATTERY = 4;
+    /**
+     * {@link PackageDexOptimizer#DEX_OPT_FAILED} case. This state means some packages have failed
+     * compilation during the job. Note that the failure will not be permanent as the next dexopt
+     * job will exclude those failed packages.
+     */
+    public static final int STATUS_DEX_OPT_FAILED = 5;
 
-    @IntDef(prefix = {"STATUS_"}, value = {
-            STATUS_OK,
-            STATUS_ABORT_BY_CANCELLATION,
-            STATUS_ABORT_NO_SPACE_LEFT,
-            STATUS_ABORT_THERMAL,
-            STATUS_ABORT_BATTERY,
-            STATUS_DEX_OPT_FAILED,
-    })
+    @IntDef(prefix = {"STATUS_"},
+            value =
+                    {
+                            STATUS_OK,
+                            STATUS_ABORT_BY_CANCELLATION,
+                            STATUS_ABORT_NO_SPACE_LEFT,
+                            STATUS_ABORT_THERMAL,
+                            STATUS_ABORT_BATTERY,
+                            STATUS_DEX_OPT_FAILED,
+                    })
     @Retention(RetentionPolicy.SOURCE)
-    private @interface Status {
-    }
+    public @interface Status {}
 
     // Used for calculating space threshold for downgrading unused apps.
     private static final int LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE = 2;
@@ -125,33 +129,30 @@
 
     private final DexOptHelper mDexOptHelper;
 
+    private final BackgroundDexoptJobStatsLogger mStatsLogger =
+            new BackgroundDexoptJobStatsLogger();
+
     private final Object mLock = new Object();
 
     // Thread currently running dexopt. This will be null if dexopt is not running.
     // The thread running dexopt make sure to set this into null when the pending dexopt is
     // completed.
-    @GuardedBy("mLock")
-    @Nullable
-    private Thread mDexOptThread;
+    @GuardedBy("mLock") @Nullable private Thread mDexOptThread;
 
     // Thread currently cancelling dexopt. This thread is in blocked wait state until
     // cancellation is done. Only this thread can change states for control. The other threads, if
     // need to wait for cancellation, should just wait without doing any control.
-    @GuardedBy("mLock")
-    @Nullable
-    private Thread mDexOptCancellingThread;
+    @GuardedBy("mLock") @Nullable private Thread mDexOptCancellingThread;
 
     // Tells whether post boot update is completed or not.
-    @GuardedBy("mLock")
-    private boolean mFinishedPostBootUpdate;
+    @GuardedBy("mLock") private boolean mFinishedPostBootUpdate;
 
-    @GuardedBy("mLock")
-    @Status private int mLastExecutionStatus = STATUS_OK;
+    @GuardedBy("mLock") @Status private int mLastExecutionStatus = STATUS_OK;
 
-    @GuardedBy("mLock")
-    private long mLastExecutionStartTimeMs;
-    @GuardedBy("mLock")
-    private long mLastExecutionDurationMs;
+    @GuardedBy("mLock") private long mLastExecutionStartTimeMs;
+    @GuardedBy("mLock") private long mLastExecutionDurationIncludingSleepMs;
+    @GuardedBy("mLock") private long mLastExecutionStartUptimeMs;
+    @GuardedBy("mLock") private long mLastExecutionDurationMs;
 
     // Keeps packages cancelled from PDO for last session. This is for debugging.
     @GuardedBy("mLock")
@@ -177,8 +178,8 @@
         void onPackagesUpdated(ArraySet<String> updatedPackages);
     }
 
-    public BackgroundDexOptService(Context context, DexManager dexManager,
-            PackageManagerService pm) {
+    public BackgroundDexOptService(
+            Context context, DexManager dexManager, PackageManagerService pm) {
         this(new Injector(context, dexManager, pm));
     }
 
@@ -230,6 +231,10 @@
             writer.println(mLastExecutionStatus);
             writer.print("mLastExecutionStartTimeMs:");
             writer.println(mLastExecutionStartTimeMs);
+            writer.print("mLastExecutionDurationIncludingSleepMs:");
+            writer.println(mLastExecutionDurationIncludingSleepMs);
+            writer.print("mLastExecutionStartUptimeMs:");
+            writer.println(mLastExecutionStartUptimeMs);
             writer.print("mLastExecutionDurationMs:");
             writer.println(mLastExecutionDurationMs);
             writer.print("now:");
@@ -357,17 +362,20 @@
             resetStatesForNewDexOptRunLocked(mInjector.createAndStartThread(
                     "BackgroundDexOptService_" + (isPostBootUpdateJob ? "PostBoot" : "Idle"),
                     () -> {
-                        TimingsTraceAndSlog tr = new TimingsTraceAndSlog(TAG,
-                                Trace.TRACE_TAG_PACKAGE_MANAGER);
+                        TimingsTraceAndSlog tr =
+                                new TimingsTraceAndSlog(TAG, Trace.TRACE_TAG_PACKAGE_MANAGER);
                         tr.traceBegin("jobExecution");
                         boolean completed = false;
                         try {
-                            completed = runIdleOptimization(pm, pkgs,
-                                    params.getJobId() == JOB_POST_BOOT_UPDATE);
+                            completed = runIdleOptimization(
+                                    pm, pkgs, params.getJobId() == JOB_POST_BOOT_UPDATE);
                         } finally { // Those cleanup should be done always.
                             tr.traceEnd();
-                            Slog.i(TAG, "dexopt finishing. jobid:" + params.getJobId()
-                                    + " completed:" + completed);
+                            Slog.i(TAG,
+                                    "dexopt finishing. jobid:" + params.getJobId()
+                                            + " completed:" + completed);
+
+                            writeStatsLog(params);
 
                             if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
                                 if (completed) {
@@ -451,7 +459,7 @@
             if (mDexOptThread != Thread.currentThread()) {
                 throw new IllegalStateException(
                         "Only mDexOptThread can mark completion, mDexOptThread:" + mDexOptThread
-                                + " current:" + Thread.currentThread());
+                        + " current:" + Thread.currentThread());
             }
             mDexOptThread = null;
             // Other threads may be waiting for completion.
@@ -481,11 +489,10 @@
 
     private void scheduleAJob(int jobId) {
         JobScheduler js = mInjector.getJobScheduler();
-        JobInfo.Builder builder = new JobInfo.Builder(jobId, sDexoptServiceName)
-                .setRequiresDeviceIdle(true);
+        JobInfo.Builder builder =
+                new JobInfo.Builder(jobId, sDexoptServiceName).setRequiresDeviceIdle(true);
         if (jobId == JOB_IDLE_OPTIMIZE) {
-            builder.setRequiresCharging(true)
-                    .setPeriodic(IDLE_OPTIMIZATION_PERIOD);
+            builder.setRequiresCharging(true).setPeriodic(IDLE_OPTIMIZATION_PERIOD);
         }
         js.schedule(builder.build());
     }
@@ -525,30 +532,36 @@
         }
     }
 
-    /** Returns true if completed */
-    private boolean runIdleOptimization(PackageManagerService pm, List<String> pkgs,
-            boolean isPostBootUpdate) {
+    /**
+     * Returns whether we've successfully run the job. Note that it will return true even if some
+     * packages may have failed compiling.
+     */
+    private boolean runIdleOptimization(
+            PackageManagerService pm, List<String> pkgs, boolean isPostBootUpdate) {
         synchronized (mLock) {
             mLastExecutionStartTimeMs = SystemClock.elapsedRealtime();
+            mLastExecutionDurationIncludingSleepMs = -1;
+            mLastExecutionStartUptimeMs = SystemClock.uptimeMillis();
             mLastExecutionDurationMs = -1;
         }
         long lowStorageThreshold = getLowStorageThreshold();
-        int status = idleOptimizePackages(pm, pkgs, lowStorageThreshold,
-                isPostBootUpdate);
+        int status = idleOptimizePackages(pm, pkgs, lowStorageThreshold, isPostBootUpdate);
         logStatus(status);
         synchronized (mLock) {
             mLastExecutionStatus = status;
-            mLastExecutionDurationMs = SystemClock.elapsedRealtime() - mLastExecutionStartTimeMs;
+            mLastExecutionDurationIncludingSleepMs =
+                    SystemClock.elapsedRealtime() - mLastExecutionStartTimeMs;
+            mLastExecutionDurationMs = SystemClock.uptimeMillis() - mLastExecutionStartUptimeMs;
         }
 
-        return status == STATUS_OK;
+        return status == STATUS_OK || status == STATUS_DEX_OPT_FAILED;
     }
 
     /** Gets the size of the directory. It uses recursion to go over all files. */
     private long getDirectorySize(File f) {
         long size = 0;
         if (f.isDirectory()) {
-            for (File file: f.listFiles()) {
+            for (File file : f.listFiles()) {
                 size += getDirectorySize(file);
             }
         } else {
@@ -599,8 +612,8 @@
             // Only downgrade apps when space is low on device.
             // Threshold is selected above the lowStorageThreshold so that we can pro-actively clean
             // up disk before user hits the actual lowStorageThreshold.
-            long lowStorageThresholdForDowngrade = LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE
-                    * lowStorageThreshold;
+            long lowStorageThresholdForDowngrade =
+                    LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE * lowStorageThreshold;
             boolean shouldDowngrade = shouldDowngrade(lowStorageThresholdForDowngrade);
             if (DEBUG) {
                 Slog.d(TAG, "Should Downgrade " + shouldDowngrade);
@@ -620,19 +633,20 @@
                             // Should be aborted by the scheduler.
                             return abortCode;
                         }
-                        @DexOptResult int downgradeResult = downgradePackage(snapshot, pm, pkg,
+                        @DexOptResult
+                        int downgradeResult = downgradePackage(snapshot, pm, pkg,
                                 /* isForPrimaryDex= */ true, isPostBootUpdate);
                         if (downgradeResult == PackageDexOptimizer.DEX_OPT_PERFORMED) {
                             updatedPackages.add(pkg);
                         }
-                        @Status int status = convertPackageDexOptimizerStatusToInternal(
-                                downgradeResult);
+                        @Status
+                        int status = convertPackageDexOptimizerStatusToInternal(downgradeResult);
                         if (status != STATUS_OK) {
                             return status;
                         }
                         if (supportSecondaryDex) {
                             downgradeResult = downgradePackage(snapshot, pm, pkg,
-                                    /* isForPrimaryDex= */false, isPostBootUpdate);
+                                    /* isForPrimaryDex= */ false, isPostBootUpdate);
                             status = convertPackageDexOptimizerStatusToInternal(downgradeResult);
                             if (status != STATUS_OK) {
                                 return status;
@@ -661,6 +675,11 @@
             ArraySet<String> updatedPackages, boolean isPostBootUpdate) {
         boolean supportSecondaryDex = mInjector.supportSecondaryDex();
 
+        // Keep the error if there is any error from any package.
+        @Status int status = STATUS_OK;
+
+        // Other than cancellation, all packages will be processed even if an error happens
+        // in a package.
         for (String pkg : pkgs) {
             int abortCode = abortIdleOptimizations(lowStorageThreshold);
             if (abortCode != STATUS_OK) {
@@ -668,26 +687,32 @@
                 return abortCode;
             }
 
-            @DexOptResult int primaryResult =
-                    optimizePackage(pkg, true /* isForPrimaryDex */, isPostBootUpdate);
+            @DexOptResult
+            int primaryResult = optimizePackage(pkg, true /* isForPrimaryDex */, isPostBootUpdate);
+            if (primaryResult == PackageDexOptimizer.DEX_OPT_CANCELLED) {
+                return STATUS_ABORT_BY_CANCELLATION;
+            }
             if (primaryResult == PackageDexOptimizer.DEX_OPT_PERFORMED) {
                 updatedPackages.add(pkg);
-            } else if (primaryResult != PackageDexOptimizer.DEX_OPT_SKIPPED) {
-                return convertPackageDexOptimizerStatusToInternal(primaryResult);
+            } else if (primaryResult == PackageDexOptimizer.DEX_OPT_FAILED) {
+                status = convertPackageDexOptimizerStatusToInternal(primaryResult);
             }
 
             if (!supportSecondaryDex) {
                 continue;
             }
 
-            @DexOptResult int secondaryResult =
+            @DexOptResult
+            int secondaryResult =
                     optimizePackage(pkg, false /* isForPrimaryDex */, isPostBootUpdate);
-            if (secondaryResult != PackageDexOptimizer.DEX_OPT_PERFORMED
-                    && secondaryResult != PackageDexOptimizer.DEX_OPT_SKIPPED) {
-                return convertPackageDexOptimizerStatusToInternal(secondaryResult);
+            if (secondaryResult == PackageDexOptimizer.DEX_OPT_CANCELLED) {
+                return STATUS_ABORT_BY_CANCELLATION;
+            }
+            if (secondaryResult == PackageDexOptimizer.DEX_OPT_FAILED) {
+                status = convertPackageDexOptimizerStatusToInternal(secondaryResult);
             }
         }
-        return STATUS_OK;
+        return status;
     }
 
     /**
@@ -731,7 +756,7 @@
         if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) {
             final Computer newSnapshot = pm.snapshotComputer();
             FrameworkStatsLog.write(FrameworkStatsLog.APP_DOWNGRADED, pkg, package_size_before,
-                    getPackageSize(newSnapshot, pkg), /*aggressive=*/ false);
+                    getPackageSize(newSnapshot, pkg), /*aggressive=*/false);
         }
         return result;
     }
@@ -760,7 +785,7 @@
     @DexOptResult
     private int optimizePackage(String pkg, boolean isForPrimaryDex, boolean isPostBootUpdate) {
         int reason = isPostBootUpdate ? PackageManagerService.REASON_POST_BOOT
-                : PackageManagerService.REASON_BACKGROUND_DEXOPT;
+                                      : PackageManagerService.REASON_BACKGROUND_DEXOPT;
         int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE;
         if (!isPostBootUpdate) {
             dexoptFlags |= DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES
@@ -777,22 +802,21 @@
     }
 
     @DexOptResult
-    private int performDexOptPrimary(String pkg, int reason,
-            int dexoptFlags) {
-        return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ true,
-                () -> mDexOptHelper.performDexOptWithStatus(
-                        new DexoptOptions(pkg, reason, dexoptFlags)));
+    private int performDexOptPrimary(String pkg, int reason, int dexoptFlags) {
+        DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason, dexoptFlags);
+        return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/true,
+                () -> mDexOptHelper.performDexOptWithStatus(dexoptOptions));
     }
 
     @DexOptResult
-    private int performDexOptSecondary(String pkg, int reason,
-            int dexoptFlags) {
-        DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason,
-                dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX);
-        return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ false,
-                () -> mDexOptHelper.performDexOpt(dexoptOptions)
-                    ? PackageDexOptimizer.DEX_OPT_PERFORMED : PackageDexOptimizer.DEX_OPT_FAILED
-        );
+    private int performDexOptSecondary(String pkg, int reason, int dexoptFlags) {
+        DexoptOptions dexoptOptions = new DexoptOptions(
+                pkg, reason, dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX);
+        return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/false,
+                ()
+                        -> mDexOptHelper.performDexOpt(dexoptOptions)
+                        ? PackageDexOptimizer.DEX_OPT_PERFORMED
+                        : PackageDexOptimizer.DEX_OPT_FAILED);
     }
 
     /**
@@ -805,8 +829,8 @@
      *  {@link PackageDexOptimizer#DEX_OPT_FAILED}
      */
     @DexOptResult
-    private int trackPerformDexOpt(String pkg, boolean isForPrimaryDex,
-            Supplier<Integer> performDexOptWrapper) {
+    private int trackPerformDexOpt(
+            String pkg, boolean isForPrimaryDex, Supplier<Integer> performDexOptWrapper) {
         ArraySet<String> failedPackageNames;
         synchronized (mLock) {
             failedPackageNames =
@@ -923,6 +947,19 @@
         }
     }
 
+    private void writeStatsLog(JobParameters params) {
+        @Status int status;
+        long durationMs;
+        long durationIncludingSleepMs;
+        synchronized (mLock) {
+            status = mLastExecutionStatus;
+            durationMs = mLastExecutionDurationMs;
+            durationIncludingSleepMs = mLastExecutionDurationIncludingSleepMs;
+        }
+
+        mStatsLogger.write(status, params.getStopReason(), durationMs, durationIncludingSleepMs);
+    }
+
     /** Injector pattern for testing purpose */
     @VisibleForTesting
     static final class Injector {
@@ -962,8 +999,8 @@
         }
 
         boolean isBackgroundDexOptDisabled() {
-            return SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt" /* key */,
-                    false /* default */);
+            return SystemProperties.getBoolean(
+                    "pm.dexopt.disable_bg_dexopt" /* key */, false /* default */);
         }
 
         boolean isBatteryLevelLow() {
@@ -993,8 +1030,8 @@
         }
 
         int getCurrentThermalStatus() {
-            IThermalService thermalService = IThermalService.Stub
-                    .asInterface(ServiceManager.getService(Context.THERMAL_SERVICE));
+            IThermalService thermalService = IThermalService.Stub.asInterface(
+                    ServiceManager.getService(Context.THERMAL_SERVICE));
             try {
                 return thermalService.getCurrentThermalStatus();
             } catch (RemoteException e) {
@@ -1003,8 +1040,8 @@
         }
 
         int getDexOptThermalCutoff() {
-            return SystemProperties.getInt("dalvik.vm.dexopt.thermal-cutoff",
-                    THERMAL_CUTOFF_DEFAULT);
+            return SystemProperties.getInt(
+                    "dalvik.vm.dexopt.thermal-cutoff", THERMAL_CUTOFF_DEFAULT);
         }
 
         Thread createAndStartThread(String name, Runnable target) {
diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java
index ed71f1e..9d1f070 100644
--- a/services/core/java/com/android/server/pm/BroadcastHelper.java
+++ b/services/core/java/com/android/server/pm/BroadcastHelper.java
@@ -197,7 +197,7 @@
             final BroadcastOptions bOptions = getTemporaryAppAllowlistBroadcastOptions(
                     REASON_LOCKED_BOOT_COMPLETED);
             am.broadcastIntentWithFeature(null, null, lockedBcIntent, null, null, 0, null, null,
-                    requiredPermissions, null, android.app.AppOpsManager.OP_NONE,
+                    requiredPermissions, null, null, android.app.AppOpsManager.OP_NONE,
                     bOptions.toBundle(), false, false, userId);
 
             // Deliver BOOT_COMPLETED only if user is unlocked
@@ -207,7 +207,7 @@
                     bcIntent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
                 }
                 am.broadcastIntentWithFeature(null, null, bcIntent, null, null, 0, null, null,
-                        requiredPermissions, null, android.app.AppOpsManager.OP_NONE,
+                        requiredPermissions, null, null, android.app.AppOpsManager.OP_NONE,
                         bOptions.toBundle(), false, false, userId);
             }
         } catch (RemoteException e) {
@@ -263,7 +263,7 @@
         };
         try {
             am.broadcastIntentWithFeature(null, null, intent, null, null, 0, null, null,
-                    requiredPermissions, null, android.app.AppOpsManager.OP_NONE, null, false,
+                    requiredPermissions, null, null, android.app.AppOpsManager.OP_NONE, null, false,
                     false, UserHandle.USER_ALL);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -301,7 +301,7 @@
         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
         try {
             am.broadcastIntentWithFeature(null, null, intent, null, null,
-                    0, null, null, null, null, android.app.AppOpsManager.OP_NONE,
+                    0, null, null, null, null, null, android.app.AppOpsManager.OP_NONE,
                     null, false, false, userId);
         } catch (RemoteException e) {
         }
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index db48a1f..eb63550 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -94,12 +94,13 @@
 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
 public interface Computer extends PackageDataSnapshot {
 
+    int getVersion();
+
     /**
      * Administrative statistics: record that the snapshot has been used.  Every call
      * to use() increments the usage counter.
      */
-    default void use() {
-    }
+    Computer use();
     /**
      * Fetch the snapshot usage counter.
      * @return The number of times this snapshot was used.
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index bf9f4fa..30de9ba 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -367,6 +367,8 @@
         return (v1 > v2) ? -1 : ((v1 < v2) ? 1 : 0);
     };
 
+    private final int mVersion;
+
     // The administrative use counter.
     private int mUsed = 0;
 
@@ -424,7 +426,8 @@
         return mLocalAndroidApplication;
     }
 
-    ComputerEngine(PackageManagerService.Snapshot args) {
+    ComputerEngine(PackageManagerService.Snapshot args, int version) {
+        mVersion = version;
         mSettings = new Settings(args.settings);
         mIsolatedOwners = args.isolatedOwners;
         mPackages = args.packages;
@@ -464,11 +467,17 @@
         mService = args.service;
     }
 
+    @Override
+    public int getVersion() {
+        return mVersion;
+    }
+
     /**
      * Record that the snapshot was used.
      */
-    public final void use() {
+    public final Computer use() {
         mUsed++;
+        return this;
     }
 
     /**
@@ -5434,22 +5443,18 @@
                 false /*checkShell*/, "may package query");
         final PackageStateInternal sourceSetting = getPackageStateInternal(sourcePackageName);
         final PackageStateInternal targetSetting = getPackageStateInternal(targetPackageName);
-        if (sourceSetting == null || targetSetting == null) {
-            throw new ParcelableException(new PackageManager.NameNotFoundException("Package(s) "
-                    + (sourceSetting == null ? sourcePackageName + " " : "")
-                    + (targetSetting == null ? targetPackageName + " " : "")
-                    + "not found."));
+        boolean throwException = sourceSetting == null || targetSetting == null;
+        if (!throwException) {
+            final boolean filterSource =
+                    shouldFilterApplication(sourceSetting, callingUid, userId);
+            final boolean filterTarget =
+                    shouldFilterApplication(targetSetting, callingUid, userId);
+            // The caller must have visibility of the both packages
+            throwException = filterSource || filterTarget;
         }
-        final boolean filterSource =
-                shouldFilterApplication(sourceSetting, callingUid, userId);
-        final boolean filterTarget =
-                shouldFilterApplication(targetSetting, callingUid, userId);
-        // The caller must have visibility of the both packages
-        if (filterSource || filterTarget) {
+        if (throwException) {
             throw new ParcelableException(new PackageManager.NameNotFoundException("Package(s) "
-                    + (filterSource ? sourcePackageName + " " : "")
-                    + (filterTarget ? targetPackageName + " " : "")
-                    + "not found."));
+                    + sourcePackageName + " and/or " + targetPackageName + " not found."));
         }
         final int sourcePackageUid = UserHandle.getUid(userId, sourceSetting.getAppId());
         return !shouldFilterApplication(targetSetting, sourcePackageUid, userId);
diff --git a/services/core/java/com/android/server/pm/ComputerLocked.java b/services/core/java/com/android/server/pm/ComputerLocked.java
index af196d5..37070db 100644
--- a/services/core/java/com/android/server/pm/ComputerLocked.java
+++ b/services/core/java/com/android/server/pm/ComputerLocked.java
@@ -30,7 +30,7 @@
 public final class ComputerLocked extends ComputerEngine {
 
     ComputerLocked(PackageManagerService.Snapshot args) {
-        super(args);
+        super(args, -1);
     }
 
     protected ComponentName resolveComponentName() {
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index cb38d52..0cdf7bf 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -166,9 +166,10 @@
 
             if (PackageManagerServiceUtils.isSystemApp(uninstalledPs)) {
                 UserInfo userInfo = mUserManagerInternal.getUserInfo(userId);
-                if (userInfo == null || !userInfo.isAdmin()) {
+                if (userInfo == null || (!userInfo.isAdmin() && !mUserManagerInternal.getUserInfo(
+                        mUserManagerInternal.getProfileParentId(userId)).isAdmin())) {
                     Slog.w(TAG, "Not removing package " + packageName
-                            + " as only admin user may downgrade system apps");
+                            + " as only admin user (or their profile) may downgrade system apps");
                     EventLog.writeEvent(0x534e4554, "170646036", -1, packageName);
                     return PackageManager.DELETE_FAILED_USER_RESTRICTED;
                 }
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index f1296e0..aabe8a1 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -18,6 +18,7 @@
 
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 
+import static com.android.server.pm.ApexManager.ActiveApexInfo;
 import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
 import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT;
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
@@ -26,6 +27,7 @@
 import static com.android.server.pm.PackageManagerService.REASON_FIRST_BOOT;
 import static com.android.server.pm.PackageManagerService.STUB_SUFFIX;
 import static com.android.server.pm.PackageManagerService.TAG;
+import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason;
 import static com.android.server.pm.PackageManagerServiceCompilerMapping.getDefaultCompilerFilter;
 import static com.android.server.pm.PackageManagerServiceUtils.REMOVE_IF_NULL_PKG;
 
@@ -45,6 +47,7 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
+import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.Slog;
@@ -58,13 +61,19 @@
 import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
 import com.android.server.pm.pkg.PackageStateInternal;
 
+import dalvik.system.DexFile;
+
 import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 
@@ -267,31 +276,42 @@
             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.
+        int reason = REASON_BOOT_AFTER_OTA;
+
+        String defaultCompilerFilter = getCompilerFilterForReason(reason);
+        String targetCompilerFilter =
+                SystemProperties.get("dalvik.vm.systemuicompilerfilter", defaultCompilerFilter);
+        String compilerFilter;
+
+        if (DexFile.isProfileGuidedCompilerFilter(targetCompilerFilter)) {
+            compilerFilter = defaultCompilerFilter;
+            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))) {
+                            compilerFilter = targetCompilerFilter;
+                        } else {
+                            Log.e(TAG, "Failed to copy profile " + profileFile.getAbsolutePath());
+                        }
+                    }
+                } catch (Exception e) {
+                    Log.e(TAG, "Failed to copy profile " + profileFile.getAbsolutePath(), e);
+                }
+            }
+        } else {
+            compilerFilter = targetCompilerFilter;
+        }
+
         performDexOptTraced(new DexoptOptions(pkg.getPackageName(), REASON_BOOT_AFTER_OTA,
-                useProfileForDexopt ? "speed-profile" : "speed", null /* splitName */,
-                0 /* dexoptFlags */));
+                compilerFilter, null /* splitName */, 0 /* dexoptFlags */));
     }
 
     @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
@@ -301,10 +321,11 @@
 
         // 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();
+            // System UI is important to user experience, so we check it after a mainline update or
+            // an OTA. It may need to be re-compiled in these cases.
+            if (hasBcpApexesChanged() || mPm.isDeviceUpgrading()) {
+                checkAndDexOptSystemUi();
+            }
         }
 
         // We need to re-extract after an OTA.
@@ -731,4 +752,42 @@
     /*package*/ void controlDexOptBlocking(boolean block) {
         mPm.mPackageDexOptimizer.controlDexOptBlocking(block);
     }
+
+    /**
+     * Returns the module names of the APEXes that contribute to bootclasspath.
+     */
+    private static List<String> getBcpApexes() {
+        String bcp = System.getenv("BOOTCLASSPATH");
+        if (TextUtils.isEmpty(bcp)) {
+            Log.e(TAG, "Unable to get BOOTCLASSPATH");
+            return List.of();
+        }
+
+        ArrayList<String> bcpApexes = new ArrayList<>();
+        for (String pathStr : bcp.split(":")) {
+            Path path = Paths.get(pathStr);
+            // Check if the path is in the format of `/apex/<apex-module-name>/...` and extract the
+            // apex module name from the path.
+            if (path.getNameCount() >= 2 && path.getName(0).toString().equals("apex")) {
+                bcpApexes.add(path.getName(1).toString());
+            }
+        }
+
+        return bcpApexes;
+    }
+
+    /**
+     * Returns true of any of the APEXes that contribute to bootclasspath has changed during this
+     * boot.
+     */
+    private static boolean hasBcpApexesChanged() {
+        Set<String> bcpApexes = new HashSet<>(getBcpApexes());
+        ApexManager apexManager = ApexManager.getInstance();
+        for (ActiveApexInfo apexInfo : apexManager.getActiveApexInfos()) {
+            if (bcpApexes.contains(apexInfo.apexModuleName) && apexInfo.activeApexChanged) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/services/core/java/com/android/server/pm/FileInstallArgs.java b/services/core/java/com/android/server/pm/FileInstallArgs.java
index 85c3cc9..e3ceccd 100644
--- a/services/core/java/com/android/server/pm/FileInstallArgs.java
+++ b/services/core/java/com/android/server/pm/FileInstallArgs.java
@@ -172,9 +172,22 @@
             return false;
         }
 
-        if (!onIncremental && !SELinux.restoreconRecursive(afterCodeFile)) {
-            Slog.w(TAG, "Failed to restorecon");
-            return false;
+        if (onIncremental) {
+            Slog.i(TAG, PackageManagerServiceUtils.SELINUX_BUG
+                    + ": Skipping restorecon for Incremental install of " + beforeCodeFile);
+        } else {
+            try {
+                if (!SELinux.restoreconRecursive(afterCodeFile)) {
+                    Slog.w(TAG, "Failed to restorecon");
+                    return false;
+                }
+                PackageManagerServiceUtils.verifySelinuxLabels(afterCodeFile.getAbsolutePath());
+            } catch (Exception e) {
+                Slog.e(TAG,
+                        PackageManagerServiceUtils.SELINUX_BUG + ": Exception from restorecon on "
+                                + beforeCodeFile, e);
+                throw e;
+            }
         }
 
         // Reflect the rename internally
diff --git a/services/core/java/com/android/server/pm/InitAppsHelper.java b/services/core/java/com/android/server/pm/InitAppsHelper.java
index 154f32a..f6b22bc 100644
--- a/services/core/java/com/android/server/pm/InitAppsHelper.java
+++ b/services/core/java/com/android/server/pm/InitAppsHelper.java
@@ -24,6 +24,7 @@
 import static com.android.server.pm.PackageManagerService.SCAN_AS_PRIVILEGED;
 import static com.android.server.pm.PackageManagerService.SCAN_AS_SYSTEM;
 import static com.android.server.pm.PackageManagerService.SCAN_BOOTING;
+import static com.android.server.pm.PackageManagerService.SCAN_DROP_CACHE;
 import static com.android.server.pm.PackageManagerService.SCAN_FIRST_BOOT_OR_UPGRADE;
 import static com.android.server.pm.PackageManagerService.SCAN_INITIAL;
 import static com.android.server.pm.PackageManagerService.SCAN_NO_DEX;
@@ -167,7 +168,11 @@
                     sp.getFolder().getAbsolutePath())
                     || apexInfo.preInstalledApexPath.getAbsolutePath().startsWith(
                     sp.getFolder().getAbsolutePath() + File.separator)) {
-                return new ScanPartition(apexInfo.apexDirectory, sp, SCAN_AS_APK_IN_APEX);
+                int flags = SCAN_AS_APK_IN_APEX;
+                if (apexInfo.activeApexChanged) {
+                    flags |= SCAN_DROP_CACHE;
+                }
+                return new ScanPartition(apexInfo.apexDirectory, sp, flags);
             }
         }
         return null;
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 57a1fe0..e753459 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -72,6 +72,7 @@
 import static com.android.server.pm.PackageManagerService.SCAN_AS_VIRTUAL_PRELOAD;
 import static com.android.server.pm.PackageManagerService.SCAN_BOOTING;
 import static com.android.server.pm.PackageManagerService.SCAN_DONT_KILL_APP;
+import static com.android.server.pm.PackageManagerService.SCAN_DROP_CACHE;
 import static com.android.server.pm.PackageManagerService.SCAN_FIRST_BOOT_OR_UPGRADE;
 import static com.android.server.pm.PackageManagerService.SCAN_IGNORE_FROZEN;
 import static com.android.server.pm.PackageManagerService.SCAN_INITIAL;
@@ -152,6 +153,7 @@
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DexoptOptions;
 import com.android.server.pm.dex.ViewCompiler;
+import com.android.server.pm.parsing.PackageCacher;
 import com.android.server.pm.parsing.PackageParser2;
 import com.android.server.pm.parsing.pkg.AndroidPackage;
 import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
@@ -648,6 +650,10 @@
             Log.v(TAG, "restoreAndPostInstall userId=" + userId + " package=" + res.mPkg);
         }
 
+        if (res.mPkg != null) {
+            PackageManagerServiceUtils.verifySelinuxLabels(res.mPkg.getPath());
+        }
+
         // A restore should be requested at this point if (a) the install
         // succeeded, (b) the operation is not an update.
         final boolean update = res.mRemovedInfo != null
@@ -3414,6 +3420,11 @@
                 // Ignore entries which are not packages
                 continue;
             }
+            if ((scanFlags & SCAN_DROP_CACHE) != 0) {
+                final PackageCacher cacher = new PackageCacher(mPm.getCacheDir());
+                Log.w(TAG, "Dropping cache of " + file.getAbsolutePath());
+                cacher.cleanCachedResult(file);
+            }
             parallelPackageParser.submit(file, parseFlags);
             fileCount++;
         }
@@ -3566,6 +3577,7 @@
             @ParsingPackageUtils.ParseFlags int parseFlags,
             @PackageManagerService.ScanFlags int scanFlags,
             @Nullable UserHandle user) throws PackageManagerException {
+        PackageManagerServiceUtils.verifySelinuxLabels(parsedPackage.getPath());
 
         final Pair<ScanResult, Boolean> scanResultPair = scanSystemPackageLI(
                 parsedPackage, parseFlags, scanFlags, user);
diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java
index 32f0f10..a078b5d 100644
--- a/services/core/java/com/android/server/pm/Installer.java
+++ b/services/core/java/com/android/server/pm/Installer.java
@@ -44,6 +44,8 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 public class Installer extends SystemService {
     private static final String TAG = "Installer";
@@ -118,9 +120,13 @@
     public static final int FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES =
             IInstalld.FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES;
 
+    private static final long CONNECT_RETRY_DELAY_MS = DateUtils.SECOND_IN_MILLIS;
+    private static final long CONNECT_WAIT_MS = 10 * DateUtils.SECOND_IN_MILLIS;
+
     private final boolean mIsolated;
     private volatile boolean mDeferSetFirstBoot;
-    private volatile IInstalld mInstalld;
+    private volatile IInstalld mInstalld = null;
+    private volatile CountDownLatch mInstalldLatch = new CountDownLatch(1);
     private volatile Object mWarnIfHeld;
 
     public Installer(Context context) {
@@ -149,6 +155,7 @@
     public void onStart() {
         if (mIsolated) {
             mInstalld = null;
+            mInstalldLatch.countDown();
         } else {
             connect();
         }
@@ -160,6 +167,7 @@
             try {
                 binder.linkToDeath(() -> {
                     Slog.w(TAG, "installd died; reconnecting");
+                    mInstalldLatch = new CountDownLatch(1);
                     connect();
                 }, 0);
             } catch (RemoteException e) {
@@ -168,7 +176,9 @@
         }
 
         if (binder != null) {
-            mInstalld = IInstalld.Stub.asInterface(binder);
+            IInstalld installd = IInstalld.Stub.asInterface(binder);
+            mInstalld = installd;
+            mInstalldLatch.countDown();
             try {
                 invalidateMounts();
                 executeDeferredActions();
@@ -176,7 +186,7 @@
             }
         } else {
             Slog.w(TAG, "installd not found; trying again");
-            BackgroundThread.getHandler().postDelayed(this::connect, DateUtils.SECOND_IN_MILLIS);
+            BackgroundThread.getHandler().postDelayed(this::connect, CONNECT_RETRY_DELAY_MS);
         }
     }
 
@@ -194,7 +204,7 @@
      *
      * @return if the remote call should continue.
      */
-    private boolean checkBeforeRemote() {
+    private boolean checkBeforeRemote() throws InstallerException {
         if (mWarnIfHeld != null && Thread.holdsLock(mWarnIfHeld)) {
             Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding 0x"
                     + Integer.toHexString(System.identityHashCode(mWarnIfHeld)), new Throwable());
@@ -202,9 +212,17 @@
         if (mIsolated) {
             Slog.i(TAG, "Ignoring request because this installer is isolated");
             return false;
-        } else {
-            return true;
         }
+
+        try {
+            if (!mInstalldLatch.await(CONNECT_WAIT_MS, TimeUnit.MILLISECONDS)) {
+                throw new InstallerException("time out waiting for the installer to be ready");
+            }
+        } catch (InterruptedException e) {
+            // Do nothing.
+        }
+
+        return true;
     }
 
     // We explicitly do NOT set previousAppId because the default value should always be 0.
diff --git a/services/core/java/com/android/server/pm/IntentResolverInterceptor.java b/services/core/java/com/android/server/pm/IntentResolverInterceptor.java
deleted file mode 100644
index f5eee5a..0000000
--- a/services/core/java/com/android/server/pm/IntentResolverInterceptor.java
+++ /dev/null
@@ -1,104 +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.server.pm;
-
-import static com.android.server.wm.ActivityInterceptorCallback.INTENT_RESOLVER_ORDERED_ID;
-
-import android.Manifest;
-import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.res.Resources;
-import android.provider.DeviceConfig;
-import android.util.Slog;
-
-import com.android.internal.R;
-import com.android.internal.app.ChooserActivity;
-import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
-import com.android.server.LocalServices;
-import com.android.server.wm.ActivityInterceptorCallback;
-import com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptorInfo;
-import com.android.server.wm.ActivityTaskManagerInternal;
-
-/**
- * Redirects Activity starts for the system bundled {@link ChooserActivity} to an external
- * Sharesheet implementation by modifying the target component when appropriate.
- * <p>
- * Note: config_chooserActivity (Used also by ActivityTaskSupervisor) is already updated to point
- * to the new instance. This value is read and used for the new target component.
- */
-public final class IntentResolverInterceptor {
-    private static final String TAG = "IntentResolverIntercept";
-    private final Context mContext;
-    private final ComponentName mFrameworkChooserComponent;
-    private final ComponentName mUnbundledChooserComponent;
-    private boolean mUseUnbundledSharesheet;
-
-    private final ActivityInterceptorCallback mActivityInterceptorCallback =
-            new ActivityInterceptorCallback() {
-                @Nullable
-                @Override
-                public ActivityInterceptResult intercept(ActivityInterceptorInfo info) {
-                    if (mUseUnbundledSharesheet && isSystemChooserActivity(info)) {
-                        Slog.d(TAG, "Redirecting to UNBUNDLED Sharesheet");
-                        info.intent.setComponent(mUnbundledChooserComponent);
-                        return new ActivityInterceptResult(info.intent, info.checkedOptions);
-                    }
-                    return null;
-                }
-            };
-
-    public IntentResolverInterceptor(Context context) {
-        mContext = context;
-        mFrameworkChooserComponent = new ComponentName(mContext, ChooserActivity.class);
-        mUnbundledChooserComponent =  ComponentName.unflattenFromString(
-                Resources.getSystem().getString(R.string.config_chooserActivity));
-    }
-
-    /**
-     * Start listening for intents and USE_UNBUNDLED_SHARESHEET property changes.
-     */
-    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
-    public void registerListeners() {
-        LocalServices.getService(ActivityTaskManagerInternal.class)
-                .registerActivityStartInterceptor(INTENT_RESOLVER_ORDERED_ID,
-                        mActivityInterceptorCallback);
-
-        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
-                mContext.getMainExecutor(), properties -> updateUseUnbundledSharesheet());
-        updateUseUnbundledSharesheet();
-    }
-
-    @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
-    private void updateUseUnbundledSharesheet() {
-        mUseUnbundledSharesheet = DeviceConfig.getBoolean(
-                DeviceConfig.NAMESPACE_SYSTEMUI,
-                SystemUiDeviceConfigFlags.USE_UNBUNDLED_SHARESHEET,
-                false);
-        if (mUseUnbundledSharesheet) {
-            Slog.d(TAG, "using UNBUNDLED Sharesheet");
-        } else {
-            Slog.d(TAG, "using FRAMEWORK Sharesheet");
-        }
-    }
-
-    private boolean isSystemChooserActivity(ActivityInterceptorInfo info) {
-        return mFrameworkChooserComponent.getPackageName().equals(info.aInfo.packageName)
-                && mFrameworkChooserComponent.getClassName().equals(info.aInfo.name);
-    }
-}
diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
index ec6443d..652847a 100644
--- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
+++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java
@@ -315,9 +315,9 @@
     @Deprecated
     public final List<ResolveInfo> queryIntentReceivers(Intent intent,
             String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags,
-            int filterCallingUid, int userId) {
-        return getResolveIntentHelper().queryIntentReceiversInternal(
-                snapshot(), intent, resolvedType, flags, userId, filterCallingUid);
+            int filterCallingUid, int userId, boolean forSend) {
+        return getResolveIntentHelper().queryIntentReceiversInternal(snapshot(), intent,
+                resolvedType, flags, userId, filterCallingUid, forSend);
     }
 
     @Override
@@ -352,10 +352,9 @@
 
     @Override
     @Deprecated
-    public final void setDeviceOwnerProtectedPackages(
-            String deviceOwnerPackageName, List<String> packageNames) {
-        getProtectedPackages().setDeviceOwnerProtectedPackages(
-                deviceOwnerPackageName, packageNames);
+    public final void setOwnerProtectedPackages(
+            @UserIdInt int userId, @NonNull List<String> packageNames) {
+        getProtectedPackages().setOwnerProtectedPackages(userId, packageNames);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 2cef35f..75a5ff6 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -271,8 +271,8 @@
 import java.util.concurrent.Executor;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 
 /**
@@ -376,6 +376,7 @@
     static final int SCAN_AS_SYSTEM_EXT = 1 << 21;
     static final int SCAN_AS_ODM = 1 << 22;
     static final int SCAN_AS_APK_IN_APEX = 1 << 23;
+    static final int SCAN_DROP_CACHE = 1 << 24;
 
     @IntDef(flag = true, prefix = { "SCAN_" }, value = {
             SCAN_NO_DEX,
@@ -939,7 +940,6 @@
     private final DexOptHelper mDexOptHelper;
     private final SuspendPackageHelper mSuspendPackageHelper;
     private final DistractingPackageHelper mDistractingPackageHelper;
-    private final IntentResolverInterceptor mIntentResolverInterceptor;
     private final StorageEventHelper mStorageEventHelper;
 
     /**
@@ -1038,22 +1038,15 @@
     // times during the PackageManagerService constructor but it should not be modified thereafter.
     private ComputerLocked mLiveComputer;
 
-    // A lock-free cache for frequently called functions.
-    private volatile Computer mSnapshotComputer;
+    private static final AtomicReference<Computer> sSnapshot = new AtomicReference<>();
 
-    // If true, the snapshot is invalid (stale).  The attribute is static since it may be
-    // set from outside classes.  The attribute may be set to true anywhere, although it
-    // should only be set true while holding mLock.  However, the attribute id guaranteed
-    // to be set false only while mLock and mSnapshotLock are both held.
-    private static final AtomicBoolean sSnapshotInvalid = new AtomicBoolean(true);
-
-    static final ThreadLocal<ThreadComputer> sThreadComputer =
-            ThreadLocal.withInitial(ThreadComputer::new);
+    // If this differs from Computer#getVersion, the snapshot is invalid (stale).
+    private static final AtomicInteger sSnapshotPendingVersion = new AtomicInteger(1);
 
     /**
-     * This lock is used to make reads from {@link #sSnapshotInvalid} and
-     * {@link #mSnapshotComputer} atomic inside {@code snapshotComputer()}.  This lock is
-     * not meant to be used outside that method.  This lock must be taken before
+     * This lock is used to make reads from {@link #sSnapshotPendingVersion} and
+     * {@link #sSnapshot} atomic inside {@code snapshotComputer()} when the versions mismatch.
+     * This lock is not meant to be used outside that method. This lock must be taken before
      * {@link #mLock} is taken.
      */
     private final Object mSnapshotLock = new Object();
@@ -1077,48 +1070,53 @@
             // yet invalidated the snapshot.  Always give the thread the live computer.
             return mLiveComputer;
         }
-        synchronized (mSnapshotLock) {
-            // This synchronization block serializes access to the snapshot computer and
-            // to the code that samples mSnapshotInvalid.
-            Computer c = mSnapshotComputer;
-            if (sSnapshotInvalid.getAndSet(false) || (c == null)) {
-                // The snapshot is invalid if it is marked as invalid or if it is null.  If it
-                // is null, then it is currently being rebuilt by rebuildSnapshot().
-                synchronized (mLock) {
-                    // Rebuild the snapshot if it is invalid.  Note that the snapshot might be
-                    // invalidated as it is rebuilt.  However, the snapshot is still
-                    // self-consistent (the lock is being held) and is current as of the time
-                    // this function is entered.
-                    rebuildSnapshot();
 
-                    // Guaranteed to be non-null.  mSnapshotComputer is only be set to null
-                    // temporarily in rebuildSnapshot(), which is guarded by mLock().  Since
-                    // the mLock is held in this block and since rebuildSnapshot() is
-                    // complete, the attribute can not now be null.
-                    c = mSnapshotComputer;
-                }
+        var oldSnapshot = sSnapshot.get();
+        var pendingVersion = sSnapshotPendingVersion.get();
+
+        if (oldSnapshot != null && oldSnapshot.getVersion() == pendingVersion) {
+            return oldSnapshot.use();
+        }
+
+        synchronized (mSnapshotLock) {
+            // Re-capture pending version in case a new invalidation occurred since last check
+            var rebuildSnapshot = sSnapshot.get();
+            var rebuildVersion = sSnapshotPendingVersion.get();
+
+            // Check the versions again while the lock is held, in case the rebuild time caused
+            // multiple threads to wait on the snapshot lock. When the first thread finishes
+            // a rebuild, the snapshot is now valid and the other waiting threads can use it
+            // without kicking off their own rebuilds.
+            if (rebuildSnapshot != null && rebuildSnapshot.getVersion() == rebuildVersion) {
+                return rebuildSnapshot.use();
             }
-            c.use();
-            return c;
+
+            synchronized (mLock) {
+                // Fetch version one last time to ensure that the rebuilt snapshot matches
+                // the latest invalidation, which could have come in between entering the
+                // SnapshotLock and mLock sync blocks.
+                rebuildVersion = sSnapshotPendingVersion.get();
+
+                // Build the snapshot for this version
+                var newSnapshot = rebuildSnapshot(rebuildSnapshot, rebuildVersion);
+                sSnapshot.set(newSnapshot);
+                return newSnapshot.use();
+            }
         }
     }
 
-    /**
-     * Rebuild the cached computer.  mSnapshotComputer is temporarily set to null to block other
-     * threads from using the invalid computer until it is rebuilt.
-     */
     @GuardedBy({ "mLock", "mSnapshotLock"})
-    private void rebuildSnapshot() {
-        final long now = SystemClock.currentTimeMicro();
-        final int hits = mSnapshotComputer == null ? -1 : mSnapshotComputer.getUsed();
-        mSnapshotComputer = null;
-        final Snapshot args = new Snapshot(Snapshot.SNAPPED);
-        mSnapshotComputer = new ComputerEngine(args);
-        final long done = SystemClock.currentTimeMicro();
+    private Computer rebuildSnapshot(@Nullable Computer oldSnapshot, int newVersion) {
+        var now = SystemClock.currentTimeMicro();
+        var hits = oldSnapshot == null ? -1 : oldSnapshot.getUsed();
+        var args = new Snapshot(Snapshot.SNAPPED);
+        var newSnapshot = new ComputerEngine(args, newVersion);
+        var done = SystemClock.currentTimeMicro();
 
         if (mSnapshotStatistics != null) {
             mSnapshotStatistics.rebuild(now, done, hits);
         }
+        return newSnapshot;
     }
 
     /**
@@ -1138,7 +1136,7 @@
         if (TRACE_SNAPSHOTS) {
             Log.i(TAG, "snapshot: onChange(" + what + ")");
         }
-        sSnapshotInvalid.set(true);
+        sSnapshotPendingVersion.incrementAndGet();
     }
 
     /**
@@ -1665,7 +1663,6 @@
         mRequiredSdkSandboxPackage = testParams.requiredSdkSandboxPackage;
 
         mLiveComputer = createLiveComputer();
-        mSnapshotComputer = null;
         mSnapshotStatistics = null;
 
         mPackages.putAll(testParams.packages);
@@ -1691,7 +1688,6 @@
 
         mSharedLibraries.setDeletePackageHelper(mDeletePackageHelper);
 
-        mIntentResolverInterceptor = null;
         mStorageEventHelper = testParams.storageEventHelper;
 
         registerObservers(false);
@@ -1855,9 +1851,8 @@
             // cached computer is the same as the live computer until the end of the
             // constructor, at which time the invalidation method updates it.
             mSnapshotStatistics = new SnapshotStatistics();
-            sSnapshotInvalid.set(true);
+            sSnapshotPendingVersion.incrementAndGet();
             mLiveComputer = createLiveComputer();
-            mSnapshotComputer = null;
             registerObservers(true);
         }
 
@@ -2252,8 +2247,6 @@
 
         mServiceStartWithDelay = SystemClock.uptimeMillis() + (60 * 1000L);
 
-        mIntentResolverInterceptor = new IntentResolverInterceptor(mContext);
-
         Slog.i(TAG, "Fix for b/169414761 is applied");
     }
 
@@ -4143,11 +4136,6 @@
 
         // Prune unused static shared libraries which have been cached a period of time
         schedulePruneUnusedStaticSharedLibraries(false /* delay */);
-
-        // TODO(b/222706900): Remove this intent interceptor before T launch
-        if (mIntentResolverInterceptor != null) {
-            mIntentResolverInterceptor.registerListeners();
-        }
     }
 
     //TODO: b/111402650
@@ -5388,10 +5376,8 @@
         @Override
         public void setApplicationCategoryHint(String packageName, int categoryHint,
                 String callerPackageName) {
-            final PackageStateMutator.InitialState initialState = recordInitialState();
-
-            final FunctionalUtils.ThrowingFunction<Computer, PackageStateMutator.Result>
-                    implementation = computer -> {
+            final FunctionalUtils.ThrowingBiFunction<PackageStateMutator.InitialState, Computer,
+                    PackageStateMutator.Result> implementation = (initialState, computer) -> {
                 if (computer.getInstantAppPackageName(Binder.getCallingUid()) != null) {
                     throw new SecurityException(
                             "Instant applications don't have access to this method");
@@ -5419,12 +5405,13 @@
                 }
             };
 
-            PackageStateMutator.Result result = implementation.apply(snapshotComputer());
+            PackageStateMutator.Result result =
+                    implementation.apply(recordInitialState(), snapshotComputer());
             if (result != null && result.isStateChanged() && !result.isSpecificPackageNull()) {
                 // TODO: Specific return value of what state changed?
                 // The installer on record might have changed, retry with lock
                 synchronized (mPackageStateWriteLock) {
-                    result = implementation.apply(snapshotComputer());
+                    result = implementation.apply(recordInitialState(), snapshotComputer());
                 }
             }
 
@@ -7156,9 +7143,19 @@
     public PackageStateMutator.Result commitPackageStateMutation(
             @Nullable PackageStateMutator.InitialState initialState, @NonNull String packageName,
             @NonNull Consumer<PackageStateWrite> consumer) {
+        PackageStateMutator.Result result = null;
+        if (Thread.holdsLock(mPackageStateWriteLock)) {
+            // If the thread is already holding the lock, this is likely a retry based on a prior
+            // failure, and re-calculating whether a state change occurred can be skipped.
+            result = PackageStateMutator.Result.SUCCESS;
+        }
         synchronized (mPackageStateWriteLock) {
-            final PackageStateMutator.Result result = mPackageStateMutator.generateResult(
-                    initialState, mChangedPackagesTracker.getSequenceNumber());
+            if (result == null) {
+                // If the thread wasn't previously holding, this is a first-try commit and so a
+                // state change may have happened.
+                result = mPackageStateMutator.generateResult(
+                        initialState, mChangedPackagesTracker.getSequenceNumber());
+            }
             if (result != PackageStateMutator.Result.SUCCESS) {
                 return result;
             }
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 4d11b13..703be16 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -19,6 +19,7 @@
 import static android.content.pm.PackageManager.INSTALL_FAILED_SHARED_USER_INCOMPATIBLE;
 import static android.content.pm.PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE;
 import static android.content.pm.PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE;
+import static android.content.pm.SigningDetails.CertCapabilities.SHARED_USER_ID;
 import static android.system.OsConstants.O_CREAT;
 import static android.system.OsConstants.O_RDWR;
 
@@ -60,6 +61,7 @@
 import android.os.Environment;
 import android.os.FileUtils;
 import android.os.Process;
+import android.os.SELinux;
 import android.os.SystemProperties;
 import android.os.incremental.IncrementalManager;
 import android.os.incremental.IncrementalStorage;
@@ -564,13 +566,8 @@
             // the older ones.  We check to see if either the new package is signed by an older cert
             // with which the current sharedUser is ok, or if it is signed by a newer one, and is ok
             // with being sharedUser with the existing signing cert.
-            boolean match =
-                    parsedSignatures.checkCapability(
-                            sharedUserSetting.getSigningDetails(),
-                            SigningDetails.CertCapabilities.SHARED_USER_ID)
-                    || sharedUserSetting.getSigningDetails().checkCapability(
-                            parsedSignatures,
-                            SigningDetails.CertCapabilities.SHARED_USER_ID);
+            boolean match = canJoinSharedUserId(parsedSignatures,
+                    sharedUserSetting.getSigningDetails());
             // Special case: if the sharedUserId capability check failed it could be due to this
             // being the only package in the sharedUserId so far and the lineage being updated to
             // deny the sharedUserId capability of the previous key in the lineage.
@@ -645,6 +642,28 @@
     }
 
     /**
+     * Returns whether the package with {@code packageSigningDetails} can join the sharedUserId
+     * with {@code sharedUserSigningDetails}.
+     * <p>
+     * A sharedUserId maintains a shared {@link SigningDetails} containing the full lineage and
+     * capabilities for each package in the sharedUserId. A package can join the sharedUserId if
+     * its current signer is the same as the shared signer, or if the current signer of either
+     * is in the signing lineage of the other with the {@link
+     * SigningDetails.CertCapabilities#SHARED_USER_ID} capability granted to that previous signer
+     * in the lineage.
+     *
+     * @param packageSigningDetails the {@code SigningDetails} of the package seeking to join the
+     *                             sharedUserId
+     * @param sharedUserSigningDetails the {@code SigningDetails} of the sharedUserId
+     * @return true if the package seeking to join the sharedUserId meets the requirements
+     */
+    public static boolean canJoinSharedUserId(@NonNull SigningDetails packageSigningDetails,
+            @NonNull SigningDetails sharedUserSigningDetails) {
+        return packageSigningDetails.checkCapability(sharedUserSigningDetails, SHARED_USER_ID)
+                || sharedUserSigningDetails.checkCapability(packageSigningDetails, SHARED_USER_ID);
+    }
+
+    /**
      * Extract native libraries to a target path
      */
     public static int extractNativeBinaries(File dstCodePath, String packageName) {
@@ -1388,4 +1407,28 @@
             }
         }
     }
+
+    // TODO(b/231951809): remove this workaround after figuring out why apk_tmp_file labels stay
+    // on the installed apps instead of the correct apk_data_file ones
+
+    public static final String SELINUX_BUG = "b/231951809";
+
+    /**
+     * A workaround for b/231951809:
+     * Verifies the SELinux labels of the passed path, and tries to correct them if detects them
+     * wrong or missing.
+     */
+    public static void verifySelinuxLabels(String path) {
+        final String expectedCon = SELinux.fileSelabelLookup(path);
+        final String actualCon = SELinux.getFileContext(path);
+        Slog.i(TAG, SELINUX_BUG + ": checking selinux labels for " + path + " expected / actual: "
+                + expectedCon + " / " + actualCon);
+        if (expectedCon == null || !expectedCon.equals(actualCon)) {
+            Slog.w(TAG, SELINUX_BUG + ": labels don't match, reapplying for " + path);
+            if (!SELinux.restoreconRecursive(new File(path))) {
+                Slog.w(TAG, SELINUX_BUG + ": Failed to reapply restorecon");
+            }
+            // well, if it didn't work now after not working at first, not much else can be done
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 6b10d4c..1eb74fac 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -2269,10 +2269,23 @@
     }
 
     private int runClear() throws RemoteException {
+        final PrintWriter pw = getOutPrintWriter();
         int userId = UserHandle.USER_SYSTEM;
-        String option = getNextOption();
-        if (option != null && option.equals("--user")) {
-            userId = UserHandle.parseUserArg(getNextArgRequired());
+        boolean cacheOnly = false;
+
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "--user":
+                    userId = UserHandle.parseUserArg(getNextArgRequired());
+                    break;
+                case "--cache-only":
+                    cacheOnly = true;
+                    break;
+                default:
+                    pw.println("Error: Unknown option: " + opt);
+                    return 1;
+            }
         }
 
         String pkg = getNextArg();
@@ -2284,7 +2297,12 @@
         final int translatedUserId =
                 translateUserId(userId, UserHandle.USER_NULL, "runClear");
         final ClearDataObserver obs = new ClearDataObserver();
-        ActivityManager.getService().clearApplicationUserData(pkg, false, obs, translatedUserId);
+        if (!cacheOnly) {
+            ActivityManager.getService()
+                    .clearApplicationUserData(pkg, false, obs, translatedUserId);
+        } else {
+            mInterface.deleteApplicationCacheFilesAsUser(pkg, translatedUserId, obs);
+        }
         synchronized (obs) {
             while (!obs.finished) {
                 try {
@@ -4042,8 +4060,10 @@
         pw.println("      --user: remove the app from the given user.");
         pw.println("      --versionCode: only uninstall if the app has the given version code.");
         pw.println("");
-        pw.println("  clear [--user USER_ID] PACKAGE");
-        pw.println("    Deletes all data associated with a package.");
+        pw.println("  clear [--user USER_ID] [--cache-only] PACKAGE");
+        pw.println("    Deletes data associated with a package. Options are:");
+        pw.println("    --user: specifies the user for which we need to clear data");
+        pw.println("    --cache-only: a flag which tells if we only need to clear cache data");
         pw.println("");
         pw.println("  enable [--user USER_ID] PACKAGE_OR_COMPONENT");
         pw.println("  disable [--user USER_ID] PACKAGE_OR_COMPONENT");
diff --git a/services/core/java/com/android/server/pm/ProtectedPackages.java b/services/core/java/com/android/server/pm/ProtectedPackages.java
index bf46129..e923988 100644
--- a/services/core/java/com/android/server/pm/ProtectedPackages.java
+++ b/services/core/java/com/android/server/pm/ProtectedPackages.java
@@ -16,11 +16,11 @@
 
 package com.android.server.pm;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.content.Context;
 import android.os.UserHandle;
-import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.SparseArray;
 
@@ -56,7 +56,7 @@
 
     @Nullable
     @GuardedBy("this")
-    private final ArrayMap<String, Set<String>> mDeviceOwnerProtectedPackages = new ArrayMap<>();
+    private final SparseArray<Set<String>> mOwnerProtectedPackages = new SparseArray<>();
 
     private final Context mContext;
 
@@ -79,13 +79,13 @@
                 : profileOwnerPackages.clone();
     }
 
-    /** Sets the protected packages for the device owner. */
-    public synchronized void setDeviceOwnerProtectedPackages(
-            String deviceOwnerPackageName, List<String> packageNames) {
+    /** Sets packages protected by a device or profile owner. */
+    public synchronized void setOwnerProtectedPackages(
+            @UserIdInt int userId, @NonNull List<String> packageNames) {
         if (packageNames.isEmpty()) {
-            mDeviceOwnerProtectedPackages.remove(deviceOwnerPackageName);
+            mOwnerProtectedPackages.remove(userId);
         } else {
-            mDeviceOwnerProtectedPackages.put(deviceOwnerPackageName, new ArraySet<>(packageNames));
+            mOwnerProtectedPackages.put(userId, new ArraySet<>(packageNames));
         }
     }
 
@@ -123,19 +123,24 @@
      * <p>A protected package means that, apart from the package owner, no system or privileged apps
      * can modify its data or package state.
      */
-    private synchronized boolean isProtectedPackage(String packageName) {
+    private synchronized boolean isProtectedPackage(@UserIdInt int userId, String packageName) {
         return packageName != null && (packageName.equals(mDeviceProvisioningPackage)
-                || isDeviceOwnerProtectedPackage(packageName));
+                || isOwnerProtectedPackage(userId, packageName));
     }
 
-    /** Returns {@code true} if the given package is a protected package set by any device owner. */
-    private synchronized boolean isDeviceOwnerProtectedPackage(String packageName) {
-        for (Set<String> protectedPackages : mDeviceOwnerProtectedPackages.values()) {
-            if (protectedPackages.contains(packageName)) {
-                return true;
-            }
-        }
-        return false;
+    /**
+     * Returns {@code true} if the given package is a protected package set by any device or
+     * profile owner.
+     */
+    private synchronized boolean isOwnerProtectedPackage(
+            @UserIdInt int userId, String packageName) {
+        return isPackageProtectedForUser(UserHandle.USER_ALL, packageName)
+                || isPackageProtectedForUser(userId, packageName);
+    }
+
+    private synchronized boolean isPackageProtectedForUser(int userId, String packageName) {
+        int userIdx = mOwnerProtectedPackages.indexOfKey(userId);
+        return userIdx >= 0 && mOwnerProtectedPackages.valueAt(userIdx).contains(packageName);
     }
 
     /**
@@ -146,7 +151,7 @@
      */
     public boolean isPackageStateProtected(@UserIdInt int userId, String packageName) {
         return hasDeviceOwnerOrProfileOwner(userId, packageName)
-                || isProtectedPackage(packageName);
+                || isProtectedPackage(userId, packageName);
     }
 
     /**
@@ -155,6 +160,6 @@
      */
     public boolean isPackageDataProtected(@UserIdInt int userId, String packageName) {
         return hasDeviceOwnerOrProfileOwner(userId, packageName)
-                || isProtectedPackage(packageName);
+                || isProtectedPackage(userId, packageName);
     }
 }
diff --git a/services/core/java/com/android/server/pm/ReconcilePackageUtils.java b/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
index 5fc916f..d6a133e 100644
--- a/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
+++ b/services/core/java/com/android/server/pm/ReconcilePackageUtils.java
@@ -22,11 +22,9 @@
 
 import static com.android.server.pm.PackageManagerService.SCAN_BOOTING;
 import static com.android.server.pm.PackageManagerService.SCAN_DONT_KILL_APP;
-import static com.android.server.pm.PackageManagerServiceUtils.compareSignatures;
 
 import android.content.pm.PackageManager;
 import android.content.pm.SharedLibraryInfo;
-import android.content.pm.Signature;
 import android.content.pm.SigningDetails;
 import android.os.SystemProperties;
 import android.util.ArrayMap;
@@ -212,12 +210,10 @@
                     // the signatures on the first package scanned for the shared user (i.e. if the
                     // signaturesChanged state hasn't been initialized yet in SharedUserSetting).
                     if (sharedUserSetting != null) {
-                        final Signature[] sharedUserSignatures = sharedUserSetting
-                                .signatures.mSigningDetails.getSignatures();
                         if (sharedUserSetting.signaturesChanged != null
-                                && compareSignatures(sharedUserSignatures,
-                                parsedPackage.getSigningDetails().getSignatures())
-                                != PackageManager.SIGNATURE_MATCH) {
+                                && !PackageManagerServiceUtils.canJoinSharedUserId(
+                                parsedPackage.getSigningDetails(),
+                                sharedUserSetting.getSigningDetails())) {
                             if (SystemProperties.getInt("ro.product.first_api_level", 0) <= 29) {
                                 // Mismatched signatures is an error and silently skipping system
                                 // packages will likely break the device in unforeseen ways.
diff --git a/services/core/java/com/android/server/pm/ResolveIntentHelper.java b/services/core/java/com/android/server/pm/ResolveIntentHelper.java
index b74670b..2db1c20 100644
--- a/services/core/java/com/android/server/pm/ResolveIntentHelper.java
+++ b/services/core/java/com/android/server/pm/ResolveIntentHelper.java
@@ -306,16 +306,32 @@
         return new IntentSender(target);
     }
 
-    // In this method, we have to know the actual calling UID, but in some cases Binder's
-    // call identity is removed, so the UID has to be passed in explicitly.
-    public @NonNull List<ResolveInfo> queryIntentReceiversInternal(Computer computer, Intent intent,
+    /**
+     * Retrieve all receivers that can handle a broadcast of the given intent.
+     * @param queryingUid the results will be filtered in the context of this UID instead.
+     */
+    @NonNull
+    public List<ResolveInfo> queryIntentReceiversInternal(Computer computer, Intent intent,
             String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags, int userId,
-            int filterCallingUid) {
+            int queryingUid) {
+        return queryIntentReceiversInternal(computer, intent, resolvedType, flags, userId,
+                queryingUid, false);
+    }
+
+    /**
+     * @see PackageManagerInternal#queryIntentReceivers(Intent, String, long, int, int, boolean)
+     */
+    @NonNull
+    public List<ResolveInfo> queryIntentReceiversInternal(Computer computer, Intent intent,
+            String resolvedType, @PackageManager.ResolveInfoFlagsBits long flags, int userId,
+            int filterCallingUid, boolean forSend) {
         if (!mUserManager.exists(userId)) return Collections.emptyList();
-        computer.enforceCrossUserPermission(filterCallingUid, userId, false /*requireFullPermission*/,
-                false /*checkShell*/, "query intent receivers");
-        final String instantAppPkgName = computer.getInstantAppPackageName(filterCallingUid);
-        flags = computer.updateFlagsForResolve(flags, userId, filterCallingUid,
+        // The identity used to filter the receiver components
+        final int queryingUid = forSend ? Process.SYSTEM_UID : filterCallingUid;
+        computer.enforceCrossUserPermission(queryingUid, userId,
+                false /*requireFullPermission*/, false /*checkShell*/, "query intent receivers");
+        final String instantAppPkgName = computer.getInstantAppPackageName(queryingUid);
+        flags = computer.updateFlagsForResolve(flags, userId, queryingUid,
                 false /*includeInstantApps*/,
                 computer.isImplicitImageCaptureIntentAndNotSetByDpc(intent, userId,
                         resolvedType, flags));
@@ -397,7 +413,7 @@
                     list, true, originalIntent, resolvedType, filterCallingUid);
         }
 
-        return computer.applyPostResolutionFilter(list, instantAppPkgName, false, filterCallingUid,
+        return computer.applyPostResolutionFilter(list, instantAppPkgName, false, queryingUid,
                 false, userId, intent);
     }
 
diff --git a/services/core/java/com/android/server/pm/ThreadComputer.java b/services/core/java/com/android/server/pm/ThreadComputer.java
deleted file mode 100644
index f603e63..0000000
--- a/services/core/java/com/android/server/pm/ThreadComputer.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.pm;
-
-/**
- * This class records the Computer being used by a thread and the Computer's reference
- * count.  There is a thread-local copy of this class.
- */
-public final class ThreadComputer implements AutoCloseable {
-    Computer mComputer = null;
-    int mRefCount = 0;
-
-    void acquire(Computer c) {
-        if (mRefCount != 0 && mComputer != c) {
-            throw new RuntimeException("computer mismatch, count = " + mRefCount);
-        }
-        mComputer = c;
-        mRefCount++;
-    }
-
-    void acquire() {
-        if (mRefCount == 0 || mComputer == null) {
-            throw new RuntimeException("computer acquire on empty ref count");
-        }
-        mRefCount++;
-    }
-
-    void release() {
-        if (--mRefCount == 0) {
-            mComputer = null;
-        }
-    }
-
-    @Override
-    public void close() {
-        release();
-    }
-}
diff --git a/services/core/java/com/android/server/pm/UserDataPreparer.java b/services/core/java/com/android/server/pm/UserDataPreparer.java
index 95482d7..974a1e1 100644
--- a/services/core/java/com/android/server/pm/UserDataPreparer.java
+++ b/services/core/java/com/android/server/pm/UserDataPreparer.java
@@ -70,9 +70,16 @@
     void prepareUserData(int userId, int userSerial, int flags) {
         synchronized (mInstallLock) {
             final StorageManager storage = mContext.getSystemService(StorageManager.class);
+            /*
+             * Internal storage must be prepared before adoptable storage, since the user's volume
+             * keys are stored in their internal storage.
+             */
+            prepareUserDataLI(null /* internal storage */, userId, userSerial, flags, true);
             for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
                 final String volumeUuid = vol.getFsUuid();
-                prepareUserDataLI(volumeUuid, userId, userSerial, flags, true);
+                if (volumeUuid != null) {
+                    prepareUserDataLI(volumeUuid, userId, userSerial, flags, true);
+                }
             }
         }
     }
@@ -136,10 +143,17 @@
     void destroyUserData(int userId, int flags) {
         synchronized (mInstallLock) {
             final StorageManager storage = mContext.getSystemService(StorageManager.class);
+            /*
+             * Volume destruction order isn't really important, but to avoid any weird issues we
+             * process internal storage last, the opposite of prepareUserData.
+             */
             for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
                 final String volumeUuid = vol.getFsUuid();
-                destroyUserDataLI(volumeUuid, userId, flags);
+                if (volumeUuid != null) {
+                    destroyUserDataLI(volumeUuid, userId, flags);
+                }
             }
+            destroyUserDataLI(null /* internal storage */, userId, flags);
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index bcdf429..fcdab88 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -4207,7 +4207,9 @@
             writeUserListLP();
         }
         updateUserIds();
-        mPm.onNewUserCreated(preCreatedUser.id, /* convertedFromPreCreated= */ true);
+        Binder.withCleanCallingIdentity(() -> {
+            mPm.onNewUserCreated(preCreatedUser.id, /* convertedFromPreCreated= */ true);
+        });
         dispatchUserAdded(preCreatedUser, token);
         return preCreatedUser;
     }
diff --git a/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java b/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java
index b26b694..d49227d 100644
--- a/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java
+++ b/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java
@@ -22,11 +22,13 @@
 import static com.android.internal.art.ArtStatsLog.ART_DATUM_REPORTED__COMPILE_FILTER__ART_COMPILATION_FILTER_FAKE_RUN_FROM_APK_FALLBACK;
 import static com.android.internal.art.ArtStatsLog.ART_DATUM_REPORTED__COMPILE_FILTER__ART_COMPILATION_FILTER_FAKE_RUN_FROM_VDEX_FALLBACK;
 
+import android.app.job.JobParameters;
 import android.os.SystemClock;
 import android.util.Slog;
 import android.util.jar.StrictJarFile;
 
 import com.android.internal.art.ArtStatsLog;
+import com.android.server.pm.BackgroundDexOptService;
 import com.android.server.pm.PackageManagerService;
 
 import java.io.IOException;
@@ -299,4 +301,31 @@
                             ArtStatsLog.ART_DATUM_REPORTED__ISA__ART_ISA_UNKNOWN));
         }
     }
+
+    private static final Map<Integer, Integer> STATUS_MAP =
+            Map.of(BackgroundDexOptService.STATUS_OK,
+                    ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_JOB_FINISHED,
+                    BackgroundDexOptService.STATUS_ABORT_BY_CANCELLATION,
+                    ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BY_CANCELLATION,
+                    BackgroundDexOptService.STATUS_ABORT_NO_SPACE_LEFT,
+                    ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_NO_SPACE_LEFT,
+                    BackgroundDexOptService.STATUS_ABORT_THERMAL,
+                    ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_THERMAL,
+                    BackgroundDexOptService.STATUS_ABORT_BATTERY,
+                    ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BATTERY,
+                    BackgroundDexOptService.STATUS_DEX_OPT_FAILED,
+                    ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_JOB_FINISHED);
+
+    /** Helper class to write background dexopt job stats to statsd. */
+    public static class BackgroundDexoptJobStatsLogger {
+        /** Writes background dexopt job stats to statsd. */
+        public void write(@BackgroundDexOptService.Status int status,
+                @JobParameters.StopReason int cancellationReason, long durationMs,
+                long durationIncludingSleepMs) {
+            ArtStatsLog.write(ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED,
+                    STATUS_MAP.getOrDefault(status,
+                            ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_UNKNOWN),
+                    cancellationReason, durationMs, durationIncludingSleepMs);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index f727c11..6796065 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -57,7 +57,6 @@
 import android.security.Credentials;
 import android.speech.RecognitionService;
 import android.telephony.TelephonyManager;
-import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Log;
@@ -618,6 +617,10 @@
         grantPermissionsToSystemPackage(pm, getDefaultSearchSelectorPackage(), userId,
                 NOTIFICATION_PERMISSIONS);
 
+        // Captive Portal Login
+        grantPermissionsToSystemPackage(pm, getDefaultCaptivePortalLoginPackage(), userId,
+                NOTIFICATION_PERMISSIONS);
+
         // Camera
         grantPermissionsToSystemPackage(pm,
                 getDefaultSystemHandlerActivityPackage(pm, MediaStore.ACTION_IMAGE_CAPTURE, userId),
@@ -909,14 +912,6 @@
         grantSystemFixedPermissionsToSystemPackage(pm, "com.android.sharedstoragebackup", userId,
                 STORAGE_PERMISSIONS);
 
-        // System Captions Service
-        String systemCaptionsServicePackageName =
-                mContext.getPackageManager().getSystemCaptionsServicePackageName();
-        if (!TextUtils.isEmpty(systemCaptionsServicePackageName)) {
-            grantPermissionsToSystemPackage(pm, systemCaptionsServicePackageName, userId,
-                    MICROPHONE_PERMISSIONS);
-        }
-
         // Bluetooth MIDI Service
         grantSystemFixedPermissionsToSystemPackage(pm,
                 MidiManager.BLUETOOTH_MIDI_SERVICE_PACKAGE, userId,
@@ -933,6 +928,10 @@
         return mContext.getString(R.string.config_defaultSearchSelectorPackageName);
     }
 
+    private String getDefaultCaptivePortalLoginPackage() {
+        return mContext.getString(R.string.config_defaultCaptivePortalLoginPackageName);
+    }
+
     @SafeVarargs
     private final void grantPermissionToEachSystemPackage(PackageManagerWrapper pm,
             ArrayList<String> packages, int userId, Set<String>... permissions) {
diff --git a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerInternal.java b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerInternal.java
index 446e20b7..6c2354f 100644
--- a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerInternal.java
+++ b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerInternal.java
@@ -17,6 +17,7 @@
 package com.android.server.pm.permission;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 
 /**
@@ -105,6 +106,20 @@
     void scheduleReadDefaultPermissionExceptions();
 
     /**
+     * Check whether a particular package should have access to the microphone data from the
+     * SoundTrigger.
+     *
+     * @param uid the uid of the package you are checking against
+     * @param packageName the name of the package you are checking against
+     * @param attributionTag the attributionTag to attach to the app op transaction
+     * @param reason the reason to attach to the app op transaction
+     * @return {@code PERMISSION_GRANTED} if the permission is granted,
+     *         or {@code PERMISSION_SOFT/HARD DENIED otherwise
+     */
+    int checkSoundTriggerRecordAudioPermissionForDataDelivery(int uid,
+            @NonNull String packageName, @Nullable String attributionTag, @NonNull String reason);
+
+    /**
      * Provider for package names.
      */
     interface PackagesProvider {
diff --git a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java
index 360a04f..88b4a94 100644
--- a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java
@@ -16,12 +16,15 @@
 
 package com.android.server.pm.permission;
 
+import static android.Manifest.permission.RECORD_AUDIO;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.Context;
+import android.content.PermissionChecker;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
@@ -399,6 +402,21 @@
         public void scheduleReadDefaultPermissionExceptions() {
             mDefaultPermissionGrantPolicy.scheduleReadDefaultPermissionExceptions();
         }
+
+        @Override
+        public int checkSoundTriggerRecordAudioPermissionForDataDelivery(int uid,
+                @NonNull String packageName, @Nullable String attributionTag,
+                @NonNull String reason) {
+            int result = PermissionChecker.checkPermissionForPreflight(mContext, RECORD_AUDIO, -1,
+                    uid, packageName);
+            if (result != PermissionChecker.PERMISSION_GRANTED) {
+                return result;
+            }
+            mContext.getSystemService(AppOpsManager.class).noteOpNoThrow(
+                    AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, uid, packageName,
+                    attributionTag, reason);
+            return result;
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index 5a05134b..a83cb5e 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -17,7 +17,6 @@
 package com.android.server.pm.permission;
 
 import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
-import static android.Manifest.permission.POST_NOTIFICATIONS;
 import static android.Manifest.permission.RECORD_AUDIO;
 import static android.Manifest.permission.UPDATE_APP_OPS_STATS;
 import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE;
@@ -51,7 +50,6 @@
 import android.content.pm.PermissionInfo;
 import android.content.pm.permission.SplitPermissionInfoParcelable;
 import android.os.Binder;
-import android.os.Build;
 import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
@@ -596,26 +594,6 @@
         }
 
         @Override
-        public int checkPostNotificationsPermissionGrantedOrLegacyAccess(int uid) {
-            int granted = PermissionManagerService.this.checkUidPermission(uid,
-                    POST_NOTIFICATIONS);
-            AndroidPackage pkg = mPackageManagerInt.getPackage(uid);
-            if (pkg == null) {
-                Slog.e(LOG_TAG, "No package for uid " + uid);
-                return granted;
-            }
-            if (granted != PackageManager.PERMISSION_GRANTED
-                    && pkg.getTargetSdkVersion() >= Build.VERSION_CODES.M) {
-                int flags = PermissionManagerService.this.getPermissionFlags(pkg.getPackageName(),
-                        POST_NOTIFICATIONS, UserHandle.getUserId(uid));
-                if ((flags & PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED) != 0) {
-                    return PackageManager.PERMISSION_GRANTED;
-                }
-            }
-            return granted;
-        }
-
-        @Override
         public void startShellPermissionIdentityDelegation(int uid, @NonNull String packageName,
                 @Nullable List<String> permissionNames) {
             Objects.requireNonNull(packageName, "packageName");
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 7be83b0..d34682d 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -42,6 +42,7 @@
 import static android.content.pm.PackageManager.FLAG_PERMISSION_WHITELIST_UPGRADE;
 import static android.content.pm.PackageManager.MASK_PERMISSION_FLAGS_ALL;
 import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Process.INVALID_UID;
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 import static android.permission.PermissionManager.KILL_APP_REASON_GIDS_CHANGED;
@@ -97,6 +98,7 @@
 import android.permission.IOnPermissionsChangeListener;
 import android.permission.PermissionControllerManager;
 import android.permission.PermissionManager;
+import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -333,7 +335,8 @@
             mPackageManagerInt.writeSettings(true);
         }
         @Override
-        public void onPermissionRevoked(int uid, int userId, String reason, boolean overrideKill) {
+        public void onPermissionRevoked(int uid, int userId, String reason, boolean overrideKill,
+                @Nullable String permissionName) {
             mOnPermissionChangeListeners.onPermissionsChanged(uid);
 
             // Critical; after this call the application should never have the permission
@@ -342,13 +345,41 @@
                 return;
             }
 
-            final int appId = UserHandle.getAppId(uid);
-            if (reason == null) {
-                mHandler.post(() -> killUid(appId, userId, KILL_APP_REASON_PERMISSIONS_REVOKED));
-            } else {
-                mHandler.post(() -> killUid(appId, userId, reason));
+            mHandler.post(() -> {
+                if (POST_NOTIFICATIONS.equals(permissionName)
+                        && isAppBackupAndRestoreRunning(uid)) {
+                    return;
+                }
+
+                final int appId = UserHandle.getAppId(uid);
+                if (reason == null) {
+                    killUid(appId, userId, KILL_APP_REASON_PERMISSIONS_REVOKED);
+                } else {
+                    killUid(appId, userId, reason);
+                }
+            });
+        }
+
+        private boolean isAppBackupAndRestoreRunning(int uid) {
+            if (checkUidPermission(uid, Manifest.permission.BACKUP) != PERMISSION_GRANTED) {
+                return false;
+            }
+
+            try {
+                int userId = UserHandle.getUserId(uid);
+                boolean isInSetup = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                        Settings.Secure.USER_SETUP_COMPLETE, userId) == 0;
+                boolean isInDeferredSetup = Settings.Secure.getIntForUser(
+                        mContext.getContentResolver(),
+                        Settings.Secure.USER_SETUP_PERSONALIZATION_STATE, userId)
+                        == Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED;
+                return isInSetup || isInDeferredSetup;
+            } catch (Settings.SettingNotFoundException e) {
+                Slog.w(LOG_TAG, "Failed to check if the user is in restore: " + e);
+                return false;
             }
         }
+
         @Override
         public void onInstallPermissionRevoked() {
             mPackageManagerInt.writeSettings(true);
@@ -1600,7 +1631,7 @@
         if (callback != null) {
             if (isRuntimePermission) {
                 callback.onPermissionRevoked(UserHandle.getUid(userId, pkg.getUid()), userId,
-                        reason, overrideKill);
+                        reason, overrideKill, permName);
             } else {
                 mDefaultPermissionCallback.onInstallPermissionRevoked();
             }
@@ -5246,7 +5277,11 @@
             onPermissionRevoked(uid, userId, reason, false);
         }
         public void onPermissionRevoked(int uid, @UserIdInt int userId, String reason,
-                boolean overrideKill) {}
+                boolean overrideKill) {
+            onPermissionRevoked(uid, userId, reason, false, null);
+        }
+        public void onPermissionRevoked(int uid, @UserIdInt int userId, String reason,
+                boolean overrideKill, @Nullable String permissionName) {}
         public void onInstallPermissionRevoked() {}
         public void onPermissionUpdated(@UserIdInt int[] updatedUserIds, boolean sync) {}
         public void onPermissionUpdatedNotifyListener(@UserIdInt int[] updatedUserIds, boolean sync,
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
index 812d7a0..d2c4ec4 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInternal.java
@@ -63,17 +63,6 @@
     int checkUidPermission(int uid, @NonNull String permissionName);
 
     /**
-     * Check whether a particular UID has been granted the POST_NOTIFICATIONS permission, or if
-     * access should be granted based on legacy access (currently symbolized by the REVIEW_REQUIRED
-     * permission flag
-     *
-     * @param uid the UID
-     * @return {@code PERMISSION_GRANTED} if the permission is granted, or legacy access is granted,
-     *         {@code PERMISSION_DENIED} otherwise
-     */
-    int checkPostNotificationsPermissionGrantedOrLegacyAccess(int uid);
-
-    /**
      * Adds a listener for runtime permission state (permissions or flags) changes.
      *
      * @param listener The listener.
diff --git a/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java b/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java
index f2e2f4f..281e1bd 100644
--- a/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/component/ParsedPermissionUtils.java
@@ -26,6 +26,7 @@
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.util.ArrayMap;
+import android.util.EventLog;
 import android.util.Slog;
 
 import com.android.internal.R;
@@ -36,6 +37,7 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * @hide
@@ -277,8 +279,28 @@
     }
 
     /**
-     * @return {@code true} if the package declares duplicate permissions with different
-     * protection levels.
+     * Determines if a duplicate permission is malformed .i.e. defines different protection level
+     * or group.
+     */
+    private static boolean isMalformedDuplicate(ParsedPermission p1, ParsedPermission p2) {
+        // Since a permission tree is also added as a permission with normal protection
+        // level, we need to skip if the parsedPermission is a permission tree.
+        if (p1 == null || p2 == null || p1.isTree() || p2.isTree()) {
+            return false;
+        }
+
+        if (p1.getProtectionLevel() != p2.getProtectionLevel()) {
+            return true;
+        }
+        if (!Objects.equals(p1.getGroup(), p2.getGroup())) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * @return {@code true} if the package declares malformed duplicate permissions.
      */
     public static boolean declareDuplicatePermission(@NonNull ParsingPackage pkg) {
         final List<ParsedPermission> permissions = pkg.getPermissions();
@@ -289,10 +311,10 @@
                 final ParsedPermission parsedPermission = permissions.get(i);
                 final String name = parsedPermission.getName();
                 final ParsedPermission perm = checkDuplicatePerm.get(name);
-                // Since a permission tree is also added as a permission with normal protection
-                // level, we need to skip if the parsedPermission is a permission tree.
-                if (perm != null && !(perm.isTree() || parsedPermission.isTree())
-                        && perm.getProtectionLevel() != parsedPermission.getProtectionLevel()) {
+                if (isMalformedDuplicate(parsedPermission, perm)) {
+                    // Fix for b/213323615
+                    EventLog.writeEvent(0x534e4554, "213323615",
+                            "The package " + pkg.getPackageName() + " seems malicious");
                     return true;
                 }
                 checkDuplicatePerm.put(name, parsedPermission);
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/PackageInfoWithoutStateUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/PackageInfoWithoutStateUtils.java
index 91d2010..b7fde43 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/PackageInfoWithoutStateUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/PackageInfoWithoutStateUtils.java
@@ -546,7 +546,9 @@
         ai.windowLayout = a.getWindowLayout();
         ai.attributionTags = a.getAttributionTags();
         if ((flags & PackageManager.GET_META_DATA) != 0) {
-            ai.metaData = a.getMetaData();
+            var metaData = a.getMetaData();
+            // Backwards compatibility, coerce to null if empty
+            ai.metaData = metaData.isEmpty() ? null : metaData;
         }
         ai.applicationInfo = applicationInfo;
         ai.setKnownActivityEmbeddingCerts(a.getKnownActivityEmbeddingCerts());
@@ -599,7 +601,9 @@
         si.mForegroundServiceType = s.getForegroundServiceType();
         si.applicationInfo = applicationInfo;
         if ((flags & PackageManager.GET_META_DATA) != 0) {
-            si.metaData = s.getMetaData();
+            var metaData = s.getMetaData();
+            // Backwards compatibility, coerce to null if empty
+            si.metaData = metaData.isEmpty() ? null : metaData;
         }
         return si;
     }
@@ -660,7 +664,9 @@
             pi.uriPermissionPatterns = null;
         }
         if ((flags & PackageManager.GET_META_DATA) != 0) {
-            pi.metaData = p.getMetaData();
+            var metaData = p.getMetaData();
+            // Backwards compatibility, coerce to null if empty
+            pi.metaData = metaData.isEmpty() ? null : metaData;
         }
         pi.applicationInfo = applicationInfo;
         return pi;
@@ -706,7 +712,9 @@
         if ((flags & PackageManager.GET_META_DATA) == 0) {
             return ii;
         }
-        ii.metaData = i.getMetaData();
+        var metaData = i.getMetaData();
+        // Backwards compatibility, coerce to null if empty
+        ii.metaData = metaData.isEmpty() ? null : metaData;
         return ii;
     }
 
@@ -729,7 +737,9 @@
         if ((flags & PackageManager.GET_META_DATA) == 0) {
             return pi;
         }
-        pi.metaData = p.getMetaData();
+        var metaData = p.getMetaData();
+        // Backwards compatibility, coerce to null if empty
+        pi.metaData = metaData.isEmpty() ? null : metaData;
         return pi;
     }
 
@@ -753,7 +763,9 @@
         if ((flags & PackageManager.GET_META_DATA) == 0) {
             return pgi;
         }
-        pgi.metaData = pg.getMetaData();
+        var metaData = pg.getMetaData();
+        // Backwards compatibility, coerce to null if empty
+        pgi.metaData = metaData.isEmpty() ? null : metaData;
         return pgi;
     }
 
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
index 6ee9c66..06a54a4 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
@@ -968,7 +968,7 @@
         if (ParsedPermissionUtils.declareDuplicatePermission(pkg)) {
             return input.error(
                     INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
-                    "Declare duplicate permissions with different protection levels."
+                    "Found duplicate permission with a different attribute value."
             );
         }
 
diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java
index e157a27..a6d148c 100644
--- a/services/core/java/com/android/server/policy/AppOpsPolicy.java
+++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java
@@ -87,8 +87,8 @@
     private final VoiceInteractionManagerInternal mVoiceInteractionManagerInternal;
 
     /**
-     * Whether this device allows only the HotwordDetectionService to use OP_RECORD_AUDIO_HOTWORD
-     * which doesn't incur the privacy indicator.
+     * Whether this device allows only the HotwordDetectionService to use
+     * OP_RECORD_AUDIO_HOTWORD which doesn't incur the privacy indicator.
      */
     private final boolean mIsHotwordDetectionServiceRequired;
 
@@ -428,8 +428,8 @@
             if (!mIsHotwordDetectionServiceRequired) {
                 return code;
             }
-            // Only the HotwordDetectionService can use the HOTWORD op which doesn't incur the
-            // privacy indicator. Downgrade to standard RECORD_AUDIO for other processes.
+            // Only the HotwordDetectionService can use the RECORD_AUDIO_HOTWORD op which doesn't
+            // incur the privacy indicator. Downgrade to standard RECORD_AUDIO for other processes.
             final HotwordDetectionServiceIdentity hotwordDetectionServiceIdentity =
                     mVoiceInteractionManagerInternal.getHotwordDetectionServiceIdentity();
             if (hotwordDetectionServiceIdentity != null
diff --git a/services/core/java/com/android/server/policy/PermissionPolicyService.java b/services/core/java/com/android/server/policy/PermissionPolicyService.java
index 977f79f..b56e112 100644
--- a/services/core/java/com/android/server/policy/PermissionPolicyService.java
+++ b/services/core/java/com/android/server/policy/PermissionPolicyService.java
@@ -915,8 +915,7 @@
             int permissionFlags = mPackageManager.getPermissionFlags(permissionName,
                     packageName, mContext.getUser());
             boolean isReviewRequired = (permissionFlags & FLAG_PERMISSION_REVIEW_REQUIRED) != 0;
-            if (isReviewRequired && !CompatChanges.isChangeEnabled(
-                    NOTIFICATION_PERM_CHANGE_ID, packageName, user)) {
+            if (isReviewRequired) {
                 return;
             }
 
@@ -1118,48 +1117,13 @@
 
     private class Internal extends PermissionPolicyInternal {
 
-        // UIDs that, if a grant dialog is shown for POST_NOTIFICATIONS before next reboot,
-        // should display a "continue allowing" message, rather than an "allow" message
-        private final ArraySet<Integer> mContinueNotifGrantMessageUids = new ArraySet<>();
-
         private final ActivityInterceptorCallback mActivityInterceptorCallback =
                 new ActivityInterceptorCallback() {
                     @Nullable
                     @Override
                     public ActivityInterceptorCallback.ActivityInterceptResult intercept(
                             ActivityInterceptorInfo info) {
-                        String action = info.intent.getAction();
-                        ActivityInterceptResult result = null;
-                        if (!ACTION_REQUEST_PERMISSIONS_FOR_OTHER.equals(action)
-                                && !PackageManager.ACTION_REQUEST_PERMISSIONS.equals(action)) {
-                            return null;
-                        }
-                        // Only this interceptor can add LEGACY_ACCESS_PERMISSION_NAMES
-                        if (info.intent.getStringArrayExtra(PackageManager
-                                .EXTRA_REQUEST_PERMISSIONS_LEGACY_ACCESS_PERMISSION_NAMES)
-                                != null) {
-                            result = new ActivityInterceptResult(
-                                    new Intent(info.intent), info.checkedOptions);
-                            result.intent.removeExtra(PackageManager
-                                    .EXTRA_REQUEST_PERMISSIONS_LEGACY_ACCESS_PERMISSION_NAMES);
-                        }
-                        if (PackageManager.ACTION_REQUEST_PERMISSIONS.equals(action)
-                                && !mContinueNotifGrantMessageUids.contains(info.realCallingUid)) {
-                            return result;
-                        }
-                        if (ACTION_REQUEST_PERMISSIONS_FOR_OTHER.equals(action)) {
-                            String otherPkg = info.intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
-                            if (otherPkg == null || (mPackageManager.getPermissionFlags(
-                                    POST_NOTIFICATIONS, otherPkg, UserHandle.of(info.userId))
-                                    & FLAG_PERMISSION_REVIEW_REQUIRED) == 0) {
-                                return result;
-                            }
-                        }
-
-                        mContinueNotifGrantMessageUids.remove(info.realCallingUid);
-                        return new ActivityInterceptResult(info.intent.putExtra(PackageManager
-                                        .EXTRA_REQUEST_PERMISSIONS_LEGACY_ACCESS_PERMISSION_NAMES,
-                                new String[] { POST_NOTIFICATIONS }), info.checkedOptions);
+                        return null;
                     }
 
                     @Override
@@ -1173,10 +1137,8 @@
                             return;
                         }
                         UserHandle user = UserHandle.of(taskInfo.userId);
-                        if (CompatChanges.isChangeEnabled(NOTIFICATION_PERM_CHANGE_ID,
+                        if (!CompatChanges.isChangeEnabled(NOTIFICATION_PERM_CHANGE_ID,
                                 activityInfo.packageName, user)) {
-                            clearNotificationReviewFlagsIfNeeded(activityInfo.packageName, user);
-                        } else {
                             // Post the activity start checks to ensure the notification channel
                             // checks happen outside the WindowManager global lock.
                             mHandler.post(() -> showNotificationPromptIfNeeded(
@@ -1337,22 +1299,6 @@
                     && isLauncherIntent(taskInfo.baseIntent);
         }
 
-        private void clearNotificationReviewFlagsIfNeeded(String packageName, UserHandle user) {
-            if ((mPackageManager.getPermissionFlags(POST_NOTIFICATIONS, packageName, user)
-                    & FLAG_PERMISSION_REVIEW_REQUIRED) == 0) {
-                return;
-            }
-            try {
-                int uid = mPackageManager.getPackageUidAsUser(packageName, 0,
-                        user.getIdentifier());
-                mContinueNotifGrantMessageUids.add(uid);
-                mPackageManager.updatePermissionFlags(POST_NOTIFICATIONS, packageName,
-                        FLAG_PERMISSION_REVIEW_REQUIRED, 0, user);
-            } catch (PackageManager.NameNotFoundException e) {
-                // Do nothing
-            }
-        }
-
         private void launchNotificationPermissionRequestDialog(String pkgName, UserHandle user,
                 int taskId, @Nullable ActivityInterceptorInfo info) {
             Intent grantPermission = mPackageManager
@@ -1469,8 +1415,7 @@
                     == PackageManager.PERMISSION_GRANTED;
             int flags = mPackageManager.getPermissionFlags(POST_NOTIFICATIONS, pkgName, user);
             boolean explicitlySet = (flags & PermissionManager.EXPLICIT_SET_FLAGS) != 0;
-            boolean needsReview = (flags & FLAG_PERMISSION_REVIEW_REQUIRED) != 0;
-            return !granted && hasCreatedNotificationChannels && (needsReview || !explicitlySet);
+            return !granted && hasCreatedNotificationChannels && !explicitlySet;
         }
     }
 }
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 4075cdd..3c13abf 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -72,8 +72,8 @@
 import android.os.Message;
 import android.os.ParcelDuration;
 import android.os.PowerManager;
+import android.os.PowerManager.GoToSleepReason;
 import android.os.PowerManager.ServiceType;
-import android.os.PowerManager.WakeData;
 import android.os.PowerManager.WakeReason;
 import android.os.PowerManagerInternal;
 import android.os.PowerSaveState;
@@ -352,7 +352,7 @@
 
     // Last reason the device went to sleep.
     private @WakeReason int mLastGlobalWakeReason;
-    private int mLastGlobalSleepReason;
+    private @GoToSleepReason int mLastGlobalSleepReason;
 
     // Timestamp of last time power boost interaction was sent.
     private long mLastInteractivePowerHintTime;
@@ -6350,20 +6350,26 @@
         }
     }
 
+    @GoToSleepReason
     private int getLastSleepReasonInternal() {
         synchronized (mLock) {
             return mLastGlobalSleepReason;
         }
     }
 
-    @VisibleForTesting
     private PowerManager.WakeData getLastWakeupInternal() {
         synchronized (mLock) {
-            return new WakeData(mLastGlobalWakeTime, mLastGlobalWakeReason,
+            return new PowerManager.WakeData(mLastGlobalWakeTime, mLastGlobalWakeReason,
                     mLastGlobalWakeTime - mLastGlobalSleepTime);
         }
     }
 
+    private PowerManager.SleepData getLastGoToSleepInternal() {
+        synchronized (mLock) {
+            return new PowerManager.SleepData(mLastGlobalSleepTime, mLastGlobalSleepReason);
+        }
+    }
+
     /**
      * If the user presses power while the proximity sensor is enabled and keeping
      * the screen off, then turn the screen back on by telling display manager to
@@ -6528,11 +6534,16 @@
         }
 
         @Override
-        public WakeData getLastWakeup() {
+        public PowerManager.WakeData getLastWakeup() {
             return getLastWakeupInternal();
         }
 
         @Override
+        public PowerManager.SleepData getLastGoToSleep() {
+            return getLastGoToSleepInternal();
+        }
+
+        @Override
         public boolean interceptPowerKeyDown(KeyEvent event) {
             return interceptPowerKeyDownInternal(event);
         }
diff --git a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
index 750d400..fd6ec06 100644
--- a/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
+++ b/services/core/java/com/android/server/sensorprivacy/CameraPrivacyLightController.java
@@ -16,44 +16,105 @@
 
 package com.android.server.sensorprivacy;
 
+import static android.hardware.SensorManager.SENSOR_DELAY_NORMAL;
+
+import android.annotation.ColorInt;
 import android.app.AppOpsManager;
 import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
 import android.hardware.lights.Light;
 import android.hardware.lights.LightState;
 import android.hardware.lights.LightsManager;
 import android.hardware.lights.LightsRequest;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.Looper;
+import android.os.SystemClock;
 import android.permission.PermissionManager;
 import android.util.ArraySet;
+import android.util.Pair;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.FgThread;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.Executor;
 
-class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener {
+class CameraPrivacyLightController implements AppOpsManager.OnOpActiveChangedListener,
+        SensorEventListener {
 
+    @VisibleForTesting
+    static final double LIGHT_VALUE_MULTIPLIER = 1 / Math.log(1.1);
+
+    private final Handler mHandler;
+    private final Executor mExecutor;
     private final Context mContext;
+    private final AppOpsManager mAppOpsManager;
     private final LightsManager mLightsManager;
+    private final SensorManager mSensorManager;
 
     private final Set<String> mActivePackages = new ArraySet<>();
     private final Set<String> mActivePhonePackages = new ArraySet<>();
 
-    private final int mCameraPrivacyLightColor;
-
     private final List<Light> mCameraLights = new ArrayList<>();
-    private final AppOpsManager mAppOpsManager;
 
     private LightsManager.LightsSession mLightsSession = null;
 
+    @ColorInt
+    private final int mDayColor;
+    @ColorInt
+    private final int mNightColor;
+
+    private final Sensor mLightSensor;
+
+    private boolean mIsAmbientLightListenerRegistered = false;
+    private final long mMovingAverageIntervalMillis;
+    /** When average of the time integral over the past {@link #mMovingAverageIntervalMillis}
+     *  milliseconds of the log_1.1(lux(t)) is greater than this value, use the daytime brightness
+     *  else use nighttime brightness. */
+    private final long mNightThreshold;
+    private final ArrayDeque<Pair<Long, Integer>> mAmbientLightValues = new ArrayDeque<>();
+    /** Tracks the Riemann sum of {@link #mAmbientLightValues} to avoid O(n) operations when sum is
+     *  needed */
+    private long mAlvSum = 0;
+    private int mLastLightColor = 0;
+    /** The elapsed real time that the ALS was started watching */
+    private long mElapsedTimeStartedReading;
+
+    private final Object mDelayedUpdateToken = new Object();
+
+    // Can't mock static native methods, workaround for testing
+    private long mElapsedRealTime = -1;
+
     CameraPrivacyLightController(Context context) {
+        this(context, FgThread.get().getLooper());
+    }
+
+    @VisibleForTesting
+    CameraPrivacyLightController(Context context, Looper looper) {
         mContext = context;
 
+        mHandler = new Handler(looper);
+        mExecutor = new HandlerExecutor(mHandler);
+
         mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
         mLightsManager = mContext.getSystemService(LightsManager.class);
+        mSensorManager = mContext.getSystemService(SensorManager.class);
 
-        mCameraPrivacyLightColor = mContext.getColor(R.color.camera_privacy_light);
+        mDayColor = mContext.getColor(R.color.camera_privacy_light_day);
+        mNightColor = mContext.getColor(R.color.camera_privacy_light_night);
+        mMovingAverageIntervalMillis = mContext.getResources()
+                .getInteger(R.integer.config_cameraPrivacyLightAlsAveragingIntervalMillis);
+        mNightThreshold = (long) (Math.log(mContext.getResources()
+                .getInteger(R.integer.config_cameraPrivacyLightAlsNightThreshold))
+                * LIGHT_VALUE_MULTIPLIER);
 
         List<Light> lights = mLightsManager.getLights();
         for (int i = 0; i < lights.size(); i++) {
@@ -64,12 +125,60 @@
         }
 
         if (mCameraLights.isEmpty()) {
+            mLightSensor = null;
             return;
         }
 
         mAppOpsManager.startWatchingActive(
                 new String[] {AppOpsManager.OPSTR_CAMERA, AppOpsManager.OPSTR_PHONE_CALL_CAMERA},
-                FgThread.getExecutor(), this);
+                mExecutor, this);
+
+        // It may be useful in the future to configure devices to know which lights are near which
+        // sensors so that we can control individual lights based on their environment.
+        mLightSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
+    }
+
+    private void addElement(long time, int value) {
+        if (mAmbientLightValues.isEmpty()) {
+            // Eliminate the size == 1 edge case and assume the light value has been constant for
+            // the previous interval
+            mAmbientLightValues.add(new Pair<>(time - getCurrentIntervalMillis() - 1, value));
+        }
+        Pair<Long, Integer> lastElement = mAmbientLightValues.peekLast();
+        mAmbientLightValues.add(new Pair<>(time, value));
+
+        mAlvSum += (time - lastElement.first) * lastElement.second;
+        removeObsoleteData(time);
+    }
+
+    private void removeObsoleteData(long time) {
+        while (mAmbientLightValues.size() > 1) {
+            Pair<Long, Integer> element0 = mAmbientLightValues.pollFirst(); // NOTICE: POLL
+            Pair<Long, Integer> element1 = mAmbientLightValues.peekFirst(); // NOTICE: PEEK
+            if (element1.first > time - getCurrentIntervalMillis()) {
+                mAmbientLightValues.addFirst(element0);
+                break;
+            }
+            mAlvSum -= (element1.first - element0.first) * element0.second;
+        }
+    }
+
+    /**
+     * Gives the Riemann sum of {@link #mAmbientLightValues} where the part of the interval that
+     * stretches outside the time window is removed and the time since the last change is added in.
+     */
+    private long getLiveAmbientLightTotal() {
+        if (mAmbientLightValues.isEmpty()) {
+            return mAlvSum;
+        }
+        long time = getElapsedRealTime();
+        removeObsoleteData(time);
+
+        Pair<Long, Integer> firstElement = mAmbientLightValues.peekFirst();
+        Pair<Long, Integer> lastElement = mAmbientLightValues.peekLast();
+
+        return mAlvSum - Math.max(0, time - getCurrentIntervalMillis() - firstElement.first)
+                * firstElement.second + (time - lastElement.first) * lastElement.second;
     }
 
     @Override
@@ -93,10 +202,16 @@
     }
 
     private void updateLightSession() {
+        if (Looper.myLooper() != mHandler.getLooper()) {
+            mHandler.post(this::updateLightSession);
+            return;
+        }
+
         Set<String> exemptedPackages = PermissionManager.getIndicatorExemptedPackages(mContext);
 
         boolean shouldSessionEnd = exemptedPackages.containsAll(mActivePackages)
                 && exemptedPackages.containsAll(mActivePhonePackages);
+        updateSensorListener(shouldSessionEnd);
 
         if (shouldSessionEnd) {
             if (mLightsSession == null) {
@@ -106,20 +221,73 @@
             mLightsSession.close();
             mLightsSession = null;
         } else {
-            if (mLightsSession != null) {
+            int lightColor;
+            if (mLightSensor != null && getLiveAmbientLightTotal()
+                    < getCurrentIntervalMillis() * mNightThreshold) {
+                lightColor = mNightColor;
+            } else {
+                lightColor = mDayColor;
+            }
+
+            if (mLastLightColor == lightColor && mLightsSession != null) {
                 return;
             }
+            mLastLightColor = lightColor;
 
             LightsRequest.Builder requestBuilder = new LightsRequest.Builder();
             for (int i = 0; i < mCameraLights.size(); i++) {
                 requestBuilder.addLight(mCameraLights.get(i),
                         new LightState.Builder()
-                                .setColor(mCameraPrivacyLightColor)
+                                .setColor(lightColor)
                                 .build());
             }
 
-            mLightsSession = mLightsManager.openSession(Integer.MAX_VALUE);
+            if (mLightsSession == null) {
+                mLightsSession = mLightsManager.openSession(Integer.MAX_VALUE);
+            }
+
             mLightsSession.requestLights(requestBuilder.build());
         }
     }
+
+    private void updateSensorListener(boolean shouldSessionEnd) {
+        if (shouldSessionEnd && mIsAmbientLightListenerRegistered) {
+            mSensorManager.unregisterListener(this);
+            mIsAmbientLightListenerRegistered = false;
+        }
+        if (!shouldSessionEnd && !mIsAmbientLightListenerRegistered && mLightSensor != null) {
+            mSensorManager.registerListener(this, mLightSensor, SENSOR_DELAY_NORMAL, mHandler);
+            mIsAmbientLightListenerRegistered = true;
+            mElapsedTimeStartedReading = getElapsedRealTime();
+        }
+    }
+
+    private long getElapsedRealTime() {
+        return mElapsedRealTime == -1 ? SystemClock.elapsedRealtime() : mElapsedRealTime;
+    }
+
+    @VisibleForTesting
+    void setElapsedRealTime(long time) {
+        mElapsedRealTime = time;
+    }
+
+    @Override
+    public void onSensorChanged(SensorEvent event) {
+        // Using log space to represent human sensation (Fechner's Law) instead of lux
+        // because lux values causes bright flashes to skew the average very high.
+        addElement(event.timestamp, Math.max(0,
+                (int) (Math.log(event.values[0]) * LIGHT_VALUE_MULTIPLIER)));
+        updateLightSession();
+        mHandler.removeCallbacksAndMessages(mDelayedUpdateToken);
+        mHandler.postDelayed(CameraPrivacyLightController.this::updateLightSession,
+                mDelayedUpdateToken, mMovingAverageIntervalMillis);
+    }
+
+    @Override
+    public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+
+    private long getCurrentIntervalMillis() {
+        return Math.min(mMovingAverageIntervalMillis,
+                getElapsedRealTime() - mElapsedTimeStartedReading);
+    }
 }
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index 3c779f3..c354f11 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -25,6 +25,7 @@
 import static android.app.AppOpsManager.OP_CAMERA;
 import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA;
 import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE;
+import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
 import static android.content.Intent.EXTRA_PACKAGE_NAME;
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
@@ -1067,6 +1068,10 @@
                             mAppOpsRestrictionToken);
                     mAppOpsManagerInternal.setGlobalRestriction(OP_PHONE_CALL_MICROPHONE, enabled,
                             mAppOpsRestrictionToken);
+                    // We don't show the dialog for RECEIVE_SOUNDTRIGGER_AUDIO, but still want to
+                    // restrict it when the microphone is disabled
+                    mAppOpsManagerInternal.setGlobalRestriction(OP_RECEIVE_AMBIENT_TRIGGER_AUDIO,
+                            enabled, mAppOpsRestrictionToken);
                     break;
                 case CAMERA:
                     mAppOpsManagerInternal.setGlobalRestriction(OP_CAMERA, enabled,
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java
index f3d151f..13fe14c 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java
@@ -41,6 +41,9 @@
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 
+import com.android.server.LocalServices;
+import com.android.server.pm.permission.LegacyPermissionManagerInternal;
+
 import java.io.PrintWriter;
 import java.util.Objects;
 
@@ -120,8 +123,7 @@
      * Throws a {@link SecurityException} iff the originator has permission to receive data.
      */
     void enforcePermissionsForDataDelivery(@NonNull Identity identity, @NonNull String reason) {
-        enforcePermissionForDataDelivery(mContext, identity, RECORD_AUDIO,
-                reason);
+        enforceSoundTriggerRecordAudioPermissionForDataDelivery(identity, reason);
         enforcePermissionForDataDelivery(mContext, identity, CAPTURE_AUDIO_HOTWORD,
                 reason);
     }
@@ -147,6 +149,19 @@
         }
     }
 
+    private static void enforceSoundTriggerRecordAudioPermissionForDataDelivery(
+            @NonNull Identity identity, @NonNull String reason) {
+        LegacyPermissionManagerInternal lpmi =
+                LocalServices.getService(LegacyPermissionManagerInternal.class);
+        final int status = lpmi.checkSoundTriggerRecordAudioPermissionForDataDelivery(identity.uid,
+                identity.packageName, identity.attributionTag, reason);
+        if (status != PermissionChecker.PERMISSION_GRANTED) {
+            throw new SecurityException(
+                    String.format("Failed to obtain permission RECORD_AUDIO for identity %s",
+                            ObjectPrinter.print(identity, 16)));
+        }
+    }
+
     /**
      * Throws a {@link SecurityException} if originator permanently doesn't have the given
      * permission.
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 977f6fd..06646d3 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -4366,7 +4366,8 @@
             }
             RkpErrorStats atom = atomWrapper.payload.getRkpErrorStats();
             pulledData.add(FrameworkStatsLog.buildStatsEvent(
-                    FrameworkStatsLog.RKP_ERROR_STATS, atom.rkpError, atomWrapper.count));
+                    FrameworkStatsLog.RKP_ERROR_STATS, atom.rkpError, atomWrapper.count,
+                    atom.security_level));
         }
         return StatsManager.PULL_SUCCESS;
     }
diff --git a/services/core/java/com/android/server/timedetector/GnssTimeUpdateService.java b/services/core/java/com/android/server/timedetector/GnssTimeUpdateService.java
index fb75ae1..129810c 100644
--- a/services/core/java/com/android/server/timedetector/GnssTimeUpdateService.java
+++ b/services/core/java/com/android/server/timedetector/GnssTimeUpdateService.java
@@ -116,6 +116,11 @@
             Log.d(TAG, "requestGnssTimeUpdates()");
         }
 
+        if (!mLocationManager.hasProvider(LocationManager.GPS_PROVIDER)) {
+            Log.e(TAG, "GPS provider does not exist on this device");
+            return;
+        }
+
         // Location Listener triggers onLocationChanged() when GNSS data is available, so
         // that the getGnssTimeMillis() function doesn't need to be continuously polled.
         mLocationListener = new LocationListener() {
diff --git a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java
index 08e8eeb..999d406 100644
--- a/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java
+++ b/services/core/java/com/android/server/vcn/util/PersistableBundleUtils.java
@@ -23,6 +23,8 @@
 
 import com.android.internal.util.HexDump;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -296,6 +298,30 @@
     }
 
     /**
+     * Converts a PersistableBundle into a disk-stable byte array format
+     *
+     * @param bundle the PersistableBundle to be converted to a disk-stable format
+     * @return the byte array representation of the PersistableBundle
+     */
+    @Nullable
+    public static byte[] toDiskStableBytes(@NonNull PersistableBundle bundle) throws IOException {
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        bundle.writeToStream(outputStream);
+        return outputStream.toByteArray();
+    }
+
+    /**
+     * Converts from a disk-stable byte array format to a PersistableBundle
+     *
+     * @param bytes the disk-stable byte array
+     * @return the PersistableBundle parsed from this byte array.
+     */
+    public static PersistableBundle fromDiskStableBytes(@NonNull byte[] bytes) throws IOException {
+        final ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+        return PersistableBundle.readFromStream(inputStream);
+    }
+
+    /**
      * Ensures safe reading and writing of {@link PersistableBundle}s to and from disk.
      *
      * <p>This class will enforce exclusion between reads and writes using the standard semantics of
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 3e36431..78b1c20 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -71,8 +71,8 @@
         IGNORED_SUPERSEDED,
     }
 
-    /** Start time in CLOCK_BOOTTIME base. */
-    public final long startTime;
+    /** Start time using {@link SystemClock#uptimeMillis()}, for calculations. */
+    public final long startUptimeMillis;
     public final VibrationAttributes attrs;
     public final long id;
     public final int uid;
@@ -94,11 +94,14 @@
 
     /**
      * Start/end times in unix epoch time. Only to be used for debugging purposes and to correlate
-     * with other system events, any duration calculations should be done use {@link #startTime} so
-     * as not to be affected by discontinuities created by RTC adjustments.
+     * with other system events, any duration calculations should be done use
+     * {@link #startUptimeMillis} so as not to be affected by discontinuities created by RTC
+     * adjustments.
      */
     private final long mStartTimeDebug;
     private long mEndTimeDebug;
+    /** End time using {@link SystemClock#uptimeMillis()}, for calculations. */
+    private long mEndUptimeMillis;
     private Status mStatus;
 
     /** A {@link CountDownLatch} to enable waiting for completion. */
@@ -109,7 +112,7 @@
         this.token = token;
         this.mEffect = effect;
         this.id = id;
-        this.startTime = SystemClock.elapsedRealtime();
+        this.startUptimeMillis = SystemClock.uptimeMillis();
         this.attrs = attrs;
         this.uid = uid;
         this.opPkg = opPkg;
@@ -131,6 +134,7 @@
             return;
         }
         mStatus = status;
+        mEndUptimeMillis = SystemClock.uptimeMillis();
         mEndTimeDebug = System.currentTimeMillis();
         mCompletionLatch.countDown();
     }
@@ -225,15 +229,17 @@
 
     /** Return {@link Vibration.DebugInfo} with read-only debug information about this vibration. */
     public Vibration.DebugInfo getDebugInfo() {
+        long durationMs = hasEnded() ? mEndUptimeMillis - startUptimeMillis : -1;
         return new Vibration.DebugInfo(
-                mStartTimeDebug, mEndTimeDebug, mEffect, mOriginalEffect, /* scale= */ 0, attrs,
-                uid, opPkg, reason, mStatus);
+                mStartTimeDebug, mEndTimeDebug, durationMs, mEffect, mOriginalEffect,
+                /* scale= */ 0, attrs, uid, opPkg, reason, mStatus);
     }
 
     /** Debug information about vibrations. */
     static final class DebugInfo {
         private final long mStartTimeDebug;
         private final long mEndTimeDebug;
+        private final long mDurationMs;
         private final CombinedVibration mEffect;
         private final CombinedVibration mOriginalEffect;
         private final float mScale;
@@ -243,11 +249,12 @@
         private final String mReason;
         private final Status mStatus;
 
-        DebugInfo(long startTimeDebug, long endTimeDebug, CombinedVibration effect,
-                CombinedVibration originalEffect, float scale, VibrationAttributes attrs,
-                int uid, String opPkg, String reason, Status status) {
+        DebugInfo(long startTimeDebug, long endTimeDebug, long durationMs,
+                CombinedVibration effect, CombinedVibration originalEffect, float scale,
+                VibrationAttributes attrs, int uid, String opPkg, String reason, Status status) {
             mStartTimeDebug = startTimeDebug;
             mEndTimeDebug = endTimeDebug;
+            mDurationMs = durationMs;
             mEffect = effect;
             mOriginalEffect = originalEffect;
             mScale = scale;
@@ -266,6 +273,8 @@
                     .append(", endTime: ")
                     .append(mEndTimeDebug == 0 ? null
                             : DEBUG_DATE_FORMAT.format(new Date(mEndTimeDebug)))
+                    .append(", durationMs: ")
+                    .append(mDurationMs)
                     .append(", status: ")
                     .append(mStatus.name().toLowerCase())
                     .append(", effect: ")
@@ -290,6 +299,7 @@
             final long token = proto.start(fieldId);
             proto.write(VibrationProto.START_TIME, mStartTimeDebug);
             proto.write(VibrationProto.END_TIME, mEndTimeDebug);
+            proto.write(VibrationProto.DURATION_MS, mDurationMs);
             proto.write(VibrationProto.STATUS, mStatus.ordinal());
 
             final long attrsToken = proto.start(VibrationProto.ATTRIBUTES);
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index ac635a0..8e6a290 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -52,6 +52,7 @@
 import android.os.Vibrator.VibrationIntensity;
 import android.os.vibrator.VibrationConfig;
 import android.provider.Settings;
+import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.util.proto.ProtoOutputStream;
@@ -97,7 +98,9 @@
             Arrays.asList(
                     USAGE_RINGTONE,
                     USAGE_ALARM,
-                    USAGE_COMMUNICATION_REQUEST));
+                    USAGE_COMMUNICATION_REQUEST,
+                    USAGE_PHYSICAL_EMULATION,
+                    USAGE_HARDWARE_FEEDBACK));
 
     /**
      * Usage allowed for vibrations when {@link Settings.System#VIBRATE_ON} is disabled.
@@ -121,6 +124,19 @@
                     USAGE_PHYSICAL_EMULATION,
                     USAGE_HARDWARE_FEEDBACK));
 
+    /**
+     * Set of reasons for {@link PowerManager} going to sleep events that allows vibrations to
+     * continue running.
+     *
+     * <p>Some examples are timeout and inattentive, which indicates automatic screen off events.
+     * When a vibration is playing during one of these screen off events then it will not be
+     * cancelled by the service.
+     */
+    private static final Set<Integer> POWER_MANAGER_SLEEP_REASON_ALLOWLIST = new HashSet<>(
+            Arrays.asList(
+                    PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE,
+                    PowerManager.GO_TO_SLEEP_REASON_TIMEOUT));
+
     private static final IntentFilter USER_SWITCHED_INTENT_FILTER =
             new IntentFilter(Intent.ACTION_USER_SWITCHED);
     private static final IntentFilter INTERNAL_RINGER_MODE_CHANGED_INTENT_FILTER =
@@ -135,7 +151,8 @@
     private final Object mLock = new Object();
     private final Context mContext;
     private final String mSystemUiPackage;
-    private final SettingsContentObserver mSettingObserver;
+    @VisibleForTesting
+    final SettingsContentObserver mSettingObserver;
     @VisibleForTesting
     final UidObserver mUidObserver;
     @VisibleForTesting
@@ -150,6 +167,9 @@
     @GuardedBy("mLock")
     @Nullable
     private AudioManager mAudioManager;
+    @GuardedBy("mLock")
+    @Nullable
+    private PowerManagerInternal mPowerManagerInternal;
 
     @GuardedBy("mLock")
     private boolean mVibrateInputDevices;
@@ -199,10 +219,16 @@
     }
 
     public void onSystemReady() {
+        PowerManagerInternal pm = LocalServices.getService(PowerManagerInternal.class);
+        AudioManager am = mContext.getSystemService(AudioManager.class);
+        int ringerMode = am.getRingerModeInternal();
+
         synchronized (mLock) {
-            mAudioManager = mContext.getSystemService(AudioManager.class);
-            mRingerMode = mAudioManager.getRingerModeInternal();
+            mPowerManagerInternal = pm;
+            mAudioManager = am;
+            mRingerMode = ringerMode;
         }
+
         try {
             ActivityManager.getService().registerUidObserver(mUidObserver,
                     ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE,
@@ -211,7 +237,6 @@
             // ignored; both services live in system_server
         }
 
-        PowerManagerInternal pm = LocalServices.getService(PowerManagerInternal.class);
         pm.registerLowPowerModeObserver(
                 new PowerManagerInternal.LowPowerModeListener() {
                     @Override
@@ -381,7 +406,27 @@
      * @return true if the vibration should be cancelled when the screen goes off, false otherwise.
      */
     public boolean shouldCancelVibrationOnScreenOff(int uid, String opPkg,
-            @VibrationAttributes.Usage int usage) {
+            @VibrationAttributes.Usage int usage, long vibrationStartUptimeMillis) {
+        PowerManagerInternal pm;
+        synchronized (mLock) {
+            pm = mPowerManagerInternal;
+        }
+        if (pm != null) {
+            // The SleepData from PowerManager may refer to a more recent sleep than the broadcast
+            // that triggered this method call. That's ok because only automatic sleeps would be
+            // ignored here and not cancel a vibration, and those are usually triggered by timeout
+            // or inactivity, so it's unlikely that it will override a more active goToSleep reason.
+            PowerManager.SleepData sleepData = pm.getLastGoToSleep();
+            if ((sleepData.goToSleepUptimeMillis < vibrationStartUptimeMillis)
+                    || POWER_MANAGER_SLEEP_REASON_ALLOWLIST.contains(sleepData.goToSleepReason)) {
+                // Ignore screen off events triggered before the vibration started, and all
+                // automatic "go to sleep" events from allowlist.
+                Slog.d(TAG, "Ignoring screen off event triggered at uptime "
+                        + sleepData.goToSleepUptimeMillis + " for reason "
+                        + PowerManager.sleepReasonToString(sleepData.goToSleepReason));
+                return false;
+            }
+        }
         if (!SYSTEM_VIBRATION_SCREEN_OFF_USAGE_ALLOWLIST.contains(usage)) {
             // Usages not allowed even for system vibrations should always be cancelled.
             return true;
@@ -628,7 +673,8 @@
     }
 
     /** Implementation of {@link ContentObserver} to be registered to a setting {@link Uri}. */
-    private final class SettingsContentObserver extends ContentObserver {
+    @VisibleForTesting
+    final class SettingsContentObserver extends ContentObserver {
         SettingsContentObserver(Handler handler) {
             super(handler);
         }
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index d7341cb..f0911ca 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -47,6 +47,7 @@
 import android.os.ServiceManager;
 import android.os.ShellCallback;
 import android.os.ShellCommand;
+import android.os.SystemClock;
 import android.os.Trace;
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
@@ -405,7 +406,7 @@
             if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) {
                 // Force update of user settings before checking if this vibration effect should
                 // be ignored or scaled.
-                mVibrationSettings.update();
+                mVibrationSettings.mSettingObserver.onChange(false);
             }
 
             synchronized (mLock) {
@@ -1103,7 +1104,7 @@
         }
         Vibration vib = conductor.getVibration();
         return mVibrationSettings.shouldCancelVibrationOnScreenOff(
-                vib.uid, vib.opPkg, vib.attrs.getUsage());
+                vib.uid, vib.opPkg, vib.attrs.getUsage(), vib.startUptimeMillis);
     }
 
     @GuardedBy("mLock")
@@ -1308,13 +1309,17 @@
         public final ExternalVibration externalVibration;
         public int scale;
 
+        private final long mStartUptimeMillis;
         private final long mStartTimeDebug;
+
+        private long mEndUptimeMillis;
         private long mEndTimeDebug;
         private Vibration.Status mStatus;
 
         private ExternalVibrationHolder(ExternalVibration externalVibration) {
             this.externalVibration = externalVibration;
             this.scale = IExternalVibratorService.SCALE_NONE;
+            mStartUptimeMillis = SystemClock.uptimeMillis();
             mStartTimeDebug = System.currentTimeMillis();
             mStatus = Vibration.Status.RUNNING;
         }
@@ -1325,6 +1330,7 @@
                 return;
             }
             mStatus = status;
+            mEndUptimeMillis = SystemClock.uptimeMillis();
             mEndTimeDebug = System.currentTimeMillis();
         }
 
@@ -1341,11 +1347,12 @@
         }
 
         public Vibration.DebugInfo getDebugInfo() {
+            long durationMs = mEndUptimeMillis == 0 ? -1 : mEndUptimeMillis - mStartUptimeMillis;
             return new Vibration.DebugInfo(
-                    mStartTimeDebug, mEndTimeDebug, /* effect= */ null, /* originalEffect= */ null,
-                    scale, externalVibration.getVibrationAttributes(),
-                    externalVibration.getUid(), externalVibration.getPackage(),
-                    /* reason= */ null, mStatus);
+                    mStartTimeDebug, mEndTimeDebug, durationMs,
+                    /* effect= */ null, /* originalEffect= */ null, scale,
+                    externalVibration.getVibrationAttributes(), externalVibration.getUid(),
+                    externalVibration.getPackage(), /* reason= */ null, mStatus);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index f21f906..f6748de 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -246,6 +246,18 @@
     }
 
     @Override
+    public void activityLocalRelaunch(IBinder token) {
+        final long origId = Binder.clearCallingIdentity();
+        synchronized (mGlobalLock) {
+            final ActivityRecord r = ActivityRecord.forTokenLocked(token);
+            if (r != null) {
+                r.startRelaunching();
+            }
+        }
+        Binder.restoreCallingIdentity(origId);
+    }
+
+    @Override
     public void activityRelaunched(IBinder token) {
         final long origId = Binder.clearCallingIdentity();
         synchronized (mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java
index 3448395..48e6f97 100644
--- a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java
+++ b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java
@@ -59,7 +59,6 @@
     @IntDef(suffix = { "_ORDERED_ID" }, value = {
             FIRST_ORDERED_ID,
             PERMISSION_POLICY_ORDERED_ID,
-            INTENT_RESOLVER_ORDERED_ID,
             VIRTUAL_DEVICE_SERVICE_ORDERED_ID,
             DREAM_MANAGER_ORDERED_ID,
             LAST_ORDERED_ID // Update this when adding new ids
@@ -78,11 +77,6 @@
     public static final int PERMISSION_POLICY_ORDERED_ID = 1;
 
     /**
-     * The identifier for {@link com.android.server.pm.IntentResolverInterceptor}.
-     */
-    public static final int INTENT_RESOLVER_ORDERED_ID = 2;
-
-    /**
      * The identifier for {@link com.android.server.companion.virtual.VirtualDeviceManagerService}
      * interceptor.
      */
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 6bb7acd..9fa68e9 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2314,7 +2314,7 @@
         final int type = getStartingWindowType(newTask, taskSwitch, processRunning,
                 allowTaskSnapshot, activityCreated, activityAllDrawn, snapshot);
 
-        //TODO(191787740) Remove for T
+        //TODO(191787740) Remove for T+
         final boolean useLegacy = type == STARTING_WINDOW_TYPE_SPLASH_SCREEN
                 && mWmService.mStartingSurfaceController.isExceptionApp(packageName, mTargetSdk,
                     () -> {
@@ -5095,13 +5095,15 @@
         // still check DC#okToAnimate again if the transition animation is fine to apply.
         // TODO(new-app-transition): Rewrite this logic using WM Shell.
         final boolean recentsAnimating = isAnimating(PARENTS, ANIMATION_TYPE_RECENTS);
+        final boolean isEnteringPipWithoutVisibleChange = mWaitForEnteringPinnedMode
+                && mVisible == visible;
         if (okToAnimate(true /* ignoreFrozen */, canTurnScreenOn())
                 && (appTransition.isTransitionSet()
                 || (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 the visibility is not changed 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.
+                && !isEnteringPipWithoutVisibleChange) {
             if (visible) {
                 displayContent.mOpeningApps.add(this);
                 mEnteringAnimation = true;
@@ -6340,8 +6342,10 @@
                 mSharedStartingData != null ? mSharedStartingData.mAssociatedTask : null;
         if (associatedTask == null) {
             removeStartingWindow();
-        } else if (associatedTask.getActivity(
-                r -> r.mVisibleRequested && !r.firstWindowDrawn) == null) {
+        } else if (associatedTask.getActivity(r -> r.mVisibleRequested && !r.firstWindowDrawn
+                // Don't block starting window removal if an Activity can't be a starting window
+                // target.
+                && r.mSharedStartingData != null) == null) {
             // The last drawn activity may not be the one that owns the starting window.
             final ActivityRecord r = associatedTask.topActivityContainsStartingWindow();
             if (r != null) {
@@ -7984,20 +7988,7 @@
             // orientation with insets applied.
             return;
         }
-        // Not using Task#isResizeable() or ActivityRecord#isResizeable() directly because app
-        // compatibility testing showed that android:supportsPictureInPicture="true" alone is not
-        // sufficient signal for not letterboxing an app.
-        // TODO(214602463): Remove multi-window check since orientation and aspect ratio
-        // restrictions should always be applied in multi-window.
-        final boolean isResizeable = task != null
-                // Activity should be resizable if the task is.
-                ? task.isResizeable(/* checkPictureInPictureSupport */ false)
-                        || isResizeable(/* checkPictureInPictureSupport */ false)
-                : isResizeable(/* checkPictureInPictureSupport */ false);
-        if (WindowConfiguration.inMultiWindowMode(windowingMode) && isResizeable) {
-            // Ignore orientation request for resizable apps in multi window.
-            return;
-        }
+
         if (windowingMode == WINDOWING_MODE_PINNED) {
             // PiP bounds have higher priority than the requested orientation. Otherwise the
             // activity may be squeezed into a small piece.
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 72408b6..d60981f 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -518,6 +518,8 @@
                 .setRequestCode(-1)
                 .setCallingUid(callingUid)
                 .setCallingPid(callingPid)
+                .setRealCallingUid(callingUid)
+                .setRealCallingPid(callingPid)
                 .setUserId(caller != null ? caller.mUserId : mService.getCurrentUserId())
                 .execute();
     }
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index cb65597..fa506ca 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -663,11 +663,16 @@
                 "Override with TaskFragment remote animation for transit=%s",
                 AppTransition.appTransitionOldToString(transit));
 
+        final int organizerUid = mDisplayContent.mAtmService.mTaskFragmentOrganizerController
+                .getTaskFragmentOrganizerUid(organizer);
+        final boolean shouldDisableInputForRemoteAnimation = !task.isFullyTrustedEmbedding(
+                organizerUid);
         final RemoteAnimationController remoteAnimationController =
                 mDisplayContent.mAppTransition.getRemoteAnimationController();
-        if (remoteAnimationController != null) {
+        if (shouldDisableInputForRemoteAnimation && remoteAnimationController != null) {
             // We are going to use client-driven animation, Disable all input on activity windows
-            // during the animation to ensure it is safe to allow client to animate the surfaces.
+            // during the animation (unless it is fully trusted) to ensure it is safe to allow
+            // client to animate the surfaces.
             // This is needed for all activity windows in the animation Task.
             remoteAnimationController.setOnRemoteAnimationReady(() -> {
                 final Consumer<ActivityRecord> updateActivities =
@@ -894,7 +899,11 @@
                     // We cannot promote the animation on Task's parent when the task is in
                     // clearing task in case the animating get stuck when performing the opening
                     // task that behind it.
-                    || (current.asTask() != null && current.asTask().mInRemoveTask)) {
+                    || (current.asTask() != null && current.asTask().mInRemoveTask)
+                    // We cannot promote the animation to changing window. This may happen when an
+                    // activity is open in a TaskFragment that is resizing, while the existing
+                    // activity in the TaskFragment is reparented to another TaskFragment.
+                    || parent.isChangingAppTransition()) {
                 canPromote = false;
             } else {
                 // In case a descendant of the parent belongs to the other group, we cannot promote
diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java
index 029056a..e7ab63e 100644
--- a/services/core/java/com/android/server/wm/Dimmer.java
+++ b/services/core/java/com/android/server/wm/Dimmer.java
@@ -50,11 +50,16 @@
         }
 
         @Override
-        public SurfaceControl.Transaction getPendingTransaction() {
+        public SurfaceControl.Transaction getSyncTransaction() {
             return mHost.getSyncTransaction();
         }
 
         @Override
+        public SurfaceControl.Transaction getPendingTransaction() {
+            return mHost.getPendingTransaction();
+        }
+
+        @Override
         public void commitPendingTransaction() {
             mHost.commitPendingTransaction();
         }
@@ -105,7 +110,7 @@
 
         void removeSurface() {
             if (mDimLayer != null && mDimLayer.isValid()) {
-                getPendingTransaction().remove(mDimLayer);
+                getSyncTransaction().remove(mDimLayer);
             }
             mDimLayer = null;
         }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index c12f7f3..66eca99 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -6228,6 +6228,7 @@
     /**
      * Sets if Display APIs should be sandboxed to the activity window bounds.
      */
+    @VisibleForTesting
     void setSandboxDisplayApis(boolean sandboxDisplayApis) {
         mSandboxDisplayApis = sandboxDisplayApis;
     }
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index d62bcdf..442f708 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1190,7 +1190,8 @@
                 if (attrs.providesInsetsTypes != null) {
                     for (@InternalInsetsType int insetsType : attrs.providesInsetsTypes) {
                         final TriConsumer<DisplayFrames, WindowContainer, Rect> imeFrameProvider =
-                                (displayFrames, windowContainer, inOutFrame) -> {
+                                win.getAttrs().providedInternalImeInsets != null
+                                        ? (displayFrames, windowContainer, inOutFrame) -> {
                                     final Insets[] providedInternalImeInsets =
                                             win.getLayoutingAttrs(displayFrames.mRotation)
                                                     .providedInternalImeInsets;
@@ -1199,7 +1200,7 @@
                                             && providedInternalImeInsets[insetsType] != null) {
                                         inOutFrame.inset(providedInternalImeInsets[insetsType]);
                                     }
-                                };
+                                } : null;
                         switch (insetsType) {
                             case ITYPE_STATUS_BAR:
                                 mStatusBarAlt = win;
@@ -1218,17 +1219,18 @@
                                 mExtraNavBarAltPosition = getAltBarPosition(attrs);
                                 break;
                         }
-                        mDisplayContent.setInsetProvider(insetsType, win, (displayFrames,
-                                windowContainer, inOutFrame) -> {
-                            final Insets[] providedInternalInsets = win.getLayoutingAttrs(
-                                    displayFrames.mRotation).providedInternalInsets;
-                            if (providedInternalInsets != null
-                                    && providedInternalInsets.length > insetsType
-                                    && providedInternalInsets[insetsType] != null) {
-                                inOutFrame.inset(providedInternalInsets[insetsType]);
-                            }
-                            inOutFrame.inset(win.mGivenContentInsets);
-                        }, imeFrameProvider);
+                        mDisplayContent.setInsetProvider(insetsType, win,
+                                win.getAttrs().providedInternalInsets != null ? (displayFrames,
+                                        windowContainer, inOutFrame) -> {
+                                    final Insets[] providedInternalInsets = win.getLayoutingAttrs(
+                                            displayFrames.mRotation).providedInternalInsets;
+                                    if (providedInternalInsets != null
+                                            && providedInternalInsets.length > insetsType
+                                            && providedInternalInsets[insetsType] != null) {
+                                        inOutFrame.inset(providedInternalInsets[insetsType]);
+                                    }
+                                    inOutFrame.inset(win.mGivenContentInsets);
+                                } : null, imeFrameProvider);
                         mInsetsSourceWindowsExceptIme.add(win);
                     }
                 }
@@ -2407,7 +2409,9 @@
     private int updateSystemBarsLw(WindowState win, int disableFlags) {
         final TaskDisplayArea defaultTaskDisplayArea = mDisplayContent.getDefaultTaskDisplayArea();
         final boolean multiWindowTaskVisible =
-                defaultTaskDisplayArea.isRootTaskVisible(WINDOWING_MODE_MULTI_WINDOW);
+                defaultTaskDisplayArea.getRootTask(task -> task.isVisible()
+                        && task.getTopLeafTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW)
+                        != null;
         final boolean freeformRootTaskVisible =
                 defaultTaskDisplayArea.isRootTaskVisible(WINDOWING_MODE_FREEFORM);
 
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index ea72e12..ce416ad 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -416,8 +416,22 @@
 
         final IBinder focusToken = focus != null ? focus.mInputChannelToken : null;
         if (focusToken == null) {
+            if (recentsAnimationInputConsumer != null
+                    && recentsAnimationInputConsumer.mWindowHandle != null
+                    && mInputFocus == recentsAnimationInputConsumer.mWindowHandle.token) {
+                // Avoid removing input focus from recentsAnimationInputConsumer.
+                // When the recents animation input consumer has the input focus,
+                // mInputFocus does not match to mDisplayContent.mCurrentFocus. Making it to be
+                // a special case, that do not remove the input focus from it when
+                // mDisplayContent.mCurrentFocus is null. This special case should be removed
+                // once recentAnimationInputConsumer is removed.
+                return;
+            }
             // When an app is focused, but its window is not showing yet, remove the input focus
-            // from the current window.
+            // from the current window. This enforces the input focus to match
+            // mDisplayContent.mCurrentFocus. However, if more special cases are discovered that
+            // the input focus and mDisplayContent.mCurrentFocus are expected to mismatch,
+            // the whole logic of how and when to revoke focus needs to be checked.
             if (mDisplayContent.mFocusedApp != null && mInputFocus != null) {
                 ProtoLog.v(WM_DEBUG_FOCUS_LIGHT, "App %s is focused,"
                         + " but the window is not ready. Start a transaction to remove focus from"
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 5c8cfff..9e0d7b5 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -46,6 +46,7 @@
 import android.app.ActivityTaskManager;
 import android.app.StatusBarManager;
 import android.app.WindowConfiguration;
+import android.content.res.Resources;
 import android.graphics.Rect;
 import android.util.ArrayMap;
 import android.util.IntArray;
@@ -123,14 +124,17 @@
      * Let remote insets controller control system bars regardless of other settings.
      */
     private boolean mRemoteInsetsControllerControlsSystemBars;
+    private final boolean mHideNavBarForKeyboard;
     private final float[] mTmpFloat9 = new float[9];
 
     InsetsPolicy(InsetsStateController stateController, DisplayContent displayContent) {
         mStateController = stateController;
         mDisplayContent = displayContent;
         mPolicy = displayContent.getDisplayPolicy();
-        mRemoteInsetsControllerControlsSystemBars = mPolicy.getContext().getResources().getBoolean(
+        final Resources r = mPolicy.getContext().getResources();
+        mRemoteInsetsControllerControlsSystemBars = r.getBoolean(
                 R.bool.config_remoteInsetsControllerControlsSystemBars);
+        mHideNavBarForKeyboard = r.getBoolean(R.bool.config_hideNavBarForKeyboard);
     }
 
     boolean getRemoteInsetsControllerControlsSystemBars() {
@@ -428,13 +432,15 @@
     private InsetsState adjustVisibilityForIme(WindowState w, InsetsState originalState,
             boolean copyState) {
         if (w.mIsImWindow) {
-            // Navigation bar insets is always visible to IME.
+            // If navigation bar is not hidden by IME, IME should always receive visible
+            // navigation bar insets.
+            final boolean navVisible = !mHideNavBarForKeyboard;
             final InsetsSource originalNavSource = originalState.peekSource(ITYPE_NAVIGATION_BAR);
-            if (originalNavSource != null && !originalNavSource.isVisible()) {
+            if (originalNavSource != null && originalNavSource.isVisible() != navVisible) {
                 final InsetsState state = copyState ? new InsetsState(originalState)
                         : originalState;
                 final InsetsSource navSource = new InsetsSource(originalNavSource);
-                navSource.setVisible(true);
+                navSource.setVisible(navVisible);
                 state.addSource(navSource);
                 return state;
             }
@@ -573,8 +579,9 @@
     private @Nullable InsetsControlTarget getNavControlTarget(@Nullable WindowState focusedWin,
             boolean fake) {
         final WindowState imeWin = mDisplayContent.mInputMethodWindow;
-        if (imeWin != null && imeWin.isVisible()) {
-            // Force showing navigation bar while IME is visible.
+        if (imeWin != null && imeWin.isVisible() && !mHideNavBarForKeyboard) {
+            // Force showing navigation bar while IME is visible and if navigation bar is not
+            // configured to be hidden by the IME.
             return null;
         }
         if (!fake && isShowingTransientTypes(Type.navigationBars())) {
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 9853d13..358e93d 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -82,6 +82,7 @@
     private boolean mIsLeashReadyForDispatching;
     private final Rect mSourceFrame = new Rect();
     private final Rect mLastSourceFrame = new Rect();
+    private @NonNull Insets mInsetsHint = Insets.NONE;
 
     private final Consumer<Transaction> mSetLeashPositionConsumer = t -> {
         if (mControl != null) {
@@ -298,6 +299,7 @@
                 if (!insetsHint.equals(mControl.getInsetsHint())) {
                     changed = true;
                     mControl.setInsetsHint(insetsHint);
+                    mInsetsHint = insetsHint;
                 }
                 mLastSourceFrame.set(mSource.getFrame());
             }
@@ -433,8 +435,8 @@
         final SurfaceControl leash = mAdapter.mCapturedLeash;
         mControlTarget = target;
         updateVisibility();
-        mControl = new InsetsSourceControl(mSource.getType(), leash, surfacePosition,
-                mSource.calculateInsets(mWindowContainer.getBounds(), true /* ignoreVisibility */));
+        mControl = new InsetsSourceControl(mSource.getType(), leash, surfacePosition, mInsetsHint);
+
         ProtoLog.d(WM_DEBUG_WINDOW_INSETS,
                 "InsetsSource Control %s for target %s", mControl, mControlTarget);
     }
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index ad2767c..d02ad99 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -22,6 +22,7 @@
 import android.graphics.Color;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -156,6 +157,7 @@
      * com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio} will be ignored and
      * the framework implementation will be used to determine the aspect ratio.
      */
+    @VisibleForTesting
     void setFixedOrientationLetterboxAspectRatio(float aspectRatio) {
         mFixedOrientationLetterboxAspectRatio = aspectRatio;
     }
@@ -164,6 +166,7 @@
      * Resets the aspect ratio of letterbox for fixed orientation to {@link
      * com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio}.
      */
+    @VisibleForTesting
     void resetFixedOrientationLetterboxAspectRatio() {
         mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat(
                 com.android.internal.R.dimen.config_fixedOrientationLetterboxAspectRatio);
@@ -177,25 +180,6 @@
     }
 
     /**
-     * Overrides corners raidus for activities presented in the letterbox mode. If given value < 0,
-     * both it and a value of {@link
-     * com.android.internal.R.integer.config_letterboxActivityCornersRadius} will be ignored and
-     * corners of the activity won't be rounded.
-     */
-    void setLetterboxActivityCornersRadius(int cornersRadius) {
-        mLetterboxActivityCornersRadius = cornersRadius;
-    }
-
-    /**
-     * Resets corners raidus for activities presented in the letterbox mode to {@link
-     * com.android.internal.R.integer.config_letterboxActivityCornersRadius}.
-     */
-    void resetLetterboxActivityCornersRadius() {
-        mLetterboxActivityCornersRadius = mContext.getResources().getInteger(
-                com.android.internal.R.integer.config_letterboxActivityCornersRadius);
-    }
-
-    /**
      * Whether corners of letterboxed activities are rounded.
      */
     boolean isLetterboxActivityCornersRounded() {
@@ -226,34 +210,6 @@
         return Color.valueOf(mContext.getResources().getColor(colorId));
     }
 
-
-    /**
-     * Sets color of letterbox background which is used when {@link
-     * #getLetterboxBackgroundType()} is {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} or as
-     * fallback for other backfround types.
-     */
-    void setLetterboxBackgroundColor(Color color) {
-        mLetterboxBackgroundColorOverride = color;
-    }
-
-    /**
-     * Sets color ID of letterbox background which is used when {@link
-     * #getLetterboxBackgroundType()} is {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} or as
-     * fallback for other backfround types.
-     */
-    void setLetterboxBackgroundColorResourceId(int colorId) {
-        mLetterboxBackgroundColorResourceIdOverride = colorId;
-    }
-
-    /**
-     * Resets color of letterbox background to {@link
-     * com.android.internal.R.color.config_letterboxBackgroundColor}.
-     */
-    void resetLetterboxBackgroundColor() {
-        mLetterboxBackgroundColorOverride = null;
-        mLetterboxBackgroundColorResourceIdOverride = null;
-    }
-
     /**
      * Gets {@link LetterboxBackgroundType} specified in {@link
      * com.android.internal.R.integer.config_letterboxBackgroundType} or over via ADB command.
@@ -263,19 +219,6 @@
         return mLetterboxBackgroundType;
     }
 
-    /** Sets letterbox background type. */
-    void setLetterboxBackgroundType(@LetterboxBackgroundType int backgroundType) {
-        mLetterboxBackgroundType = backgroundType;
-    }
-
-    /**
-     * Resets cletterbox background type to {@link
-     * com.android.internal.R.integer.config_letterboxBackgroundType}.
-     */
-    void resetLetterboxBackgroundType() {
-        mLetterboxBackgroundType = readLetterboxBackgroundTypeFromConfig(mContext);
-    }
-
     /** Returns a string representing the given {@link LetterboxBackgroundType}. */
     static String letterboxBackgroundTypeToString(
             @LetterboxBackgroundType int backgroundType) {
@@ -305,27 +248,6 @@
     }
 
     /**
-     * Overrides alpha of a black scrim shown over wallpaper for {@link
-     * #LETTERBOX_BACKGROUND_WALLPAPER} option in {@link mLetterboxBackgroundType}.
-     *
-     * <p>If given value is < 0 or >= 1, both it and a value of {@link
-     * com.android.internal.R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha} are ignored
-     * and 0.0 (transparent) is instead.
-     */
-    void setLetterboxBackgroundWallpaperDarkScrimAlpha(float alpha) {
-        mLetterboxBackgroundWallpaperDarkScrimAlpha = alpha;
-    }
-
-    /**
-     * Resets alpha of a black scrim shown over wallpaper letterbox background to {@link
-     * com.android.internal.R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha}.
-     */
-    void resetLetterboxBackgroundWallpaperDarkScrimAlpha() {
-        mLetterboxBackgroundWallpaperDarkScrimAlpha = mContext.getResources().getFloat(
-                com.android.internal.R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha);
-    }
-
-    /**
      * Gets alpha of a black scrim shown over wallpaper letterbox background.
      */
     float getLetterboxBackgroundWallpaperDarkScrimAlpha() {
@@ -333,28 +255,6 @@
     }
 
     /**
-     * Overrides blur radius for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option in
-     * {@link mLetterboxBackgroundType}.
-     *
-     * <p> If given value <= 0, both it and a value of {@link
-     * com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius} are ignored
-     * and 0 is used instead.
-     */
-    void setLetterboxBackgroundWallpaperBlurRadius(int radius) {
-        mLetterboxBackgroundWallpaperBlurRadius = radius;
-    }
-
-    /**
-     * Resets blur raidus for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option in {@link
-     * mLetterboxBackgroundType} to {@link
-     * com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius}.
-     */
-    void resetLetterboxBackgroundWallpaperBlurRadius() {
-        mLetterboxBackgroundWallpaperBlurRadius = mContext.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.config_letterboxBackgroundWallpaperBlurRadius);
-    }
-
-    /**
      * Gets blur raidus for {@link #LETTERBOX_BACKGROUND_WALLPAPER} option in {@link
      * mLetterboxBackgroundType}.
      */
@@ -381,6 +281,7 @@
      * com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier} are ignored and
      * central position (0.5) is used.
      */
+    @VisibleForTesting
     void setLetterboxHorizontalPositionMultiplier(float multiplier) {
         mLetterboxHorizontalPositionMultiplier = multiplier;
     }
@@ -389,6 +290,7 @@
      * Resets horizontal position of a center of the letterboxed app window to {@link
      * com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier}.
      */
+    @VisibleForTesting
     void resetLetterboxHorizontalPositionMultiplier() {
         mLetterboxHorizontalPositionMultiplier = mContext.getResources().getFloat(
                 com.android.internal.R.dimen.config_letterboxHorizontalPositionMultiplier);
@@ -406,6 +308,7 @@
      * Overrides whether reachability repositioning is allowed for letterboxed fullscreen apps in
      * landscape device orientation.
      */
+    @VisibleForTesting
     void setIsReachabilityEnabled(boolean enabled) {
         mIsReachabilityEnabled = enabled;
     }
@@ -414,6 +317,7 @@
      * Resets whether reachability repositioning is allowed for letterboxed fullscreen apps in
      * landscape device orientation to {@link R.bool.config_letterboxIsReachabilityEnabled}.
      */
+    @VisibleForTesting
     void resetIsReachabilityEnabled() {
         mIsReachabilityEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsReachabilityEnabled);
@@ -429,22 +333,6 @@
         return mDefaultPositionForReachability;
     }
 
-    /**
-     * Overrides default horizonal position of the letterboxed app window when reachability
-     * is enabled.
-     */
-    void setDefaultPositionForReachability(@LetterboxReachabilityPosition int position) {
-        mDefaultPositionForReachability = position;
-    }
-
-    /**
-     * Resets default horizontal position of the letterboxed app window when reachability is
-     * enabled to {@link R.integer.config_letterboxDefaultPositionForReachability}.
-     */
-    void resetDefaultPositionForReachability() {
-        mDefaultPositionForReachability = readLetterboxReachabilityPositionFromConfig(mContext);
-    }
-
     @LetterboxReachabilityPosition
     private static int readLetterboxReachabilityPositionFromConfig(Context context) {
         int position = context.getResources().getInteger(
@@ -516,17 +404,8 @@
     /**
      * Overrides whether education is allowed for letterboxed fullscreen apps.
      */
+    @VisibleForTesting
     void setIsEducationEnabled(boolean enabled) {
         mIsEducationEnabled = enabled;
     }
-
-    /**
-     * Resets whether education is allowed for letterboxed fullscreen apps to
-     * {@link R.bool.config_letterboxIsEducationEnabled}.
-     */
-    void resetIsEducationEnabled() {
-        mIsEducationEnabled = mContext.getResources().getBoolean(
-                R.bool.config_letterboxIsEducationEnabled);
-    }
-
 }
diff --git a/services/core/java/com/android/server/wm/LockTaskController.java b/services/core/java/com/android/server/wm/LockTaskController.java
index 160fc95..7a055d2 100644
--- a/services/core/java/com/android/server/wm/LockTaskController.java
+++ b/services/core/java/com/android/server/wm/LockTaskController.java
@@ -64,6 +64,7 @@
 import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.telephony.CellBroadcastUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.LocalServices;
 import com.android.server.am.ActivityManagerService;
@@ -392,6 +393,10 @@
             return false;
         }
 
+        if (isWirelessEmergencyAlert(intent)) {
+            return false;
+        }
+
         return !(isTaskAuthAllowlisted(taskAuth) || mLockTaskModeTasks.isEmpty());
     }
 
@@ -424,6 +429,25 @@
         return isPackageAllowlisted(userId, packageName);
     }
 
+    private boolean isWirelessEmergencyAlert(Intent intent) {
+        if (intent == null) {
+            return false;
+        }
+
+        final ComponentName cellBroadcastAlertDialogComponentName =
+                CellBroadcastUtils.getDefaultCellBroadcastAlertDialogComponent(mContext);
+
+        if (cellBroadcastAlertDialogComponentName == null) {
+            return false;
+        }
+
+        if (cellBroadcastAlertDialogComponentName.equals(intent.getComponent())) {
+            return true;
+        }
+
+        return false;
+    }
+
     private boolean isEmergencyCallIntent(Intent intent) {
         if (intent == null) {
             return false;
diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java
index 2bae59a..b61af2f 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimationController.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimationController.java
@@ -1197,7 +1197,10 @@
          *                     this is the target task, CLOSING otherwise).
          */
         RemoteAnimationTarget createRemoteAnimationTarget(int overrideTaskId, int overrideMode) {
-            final ActivityRecord topApp = mTask.getTopVisibleActivity();
+            ActivityRecord topApp = mTask.getTopRealVisibleActivity();
+            if (topApp == null) {
+                topApp = mTask.getTopVisibleActivity();
+            }
             final WindowState mainWindow = topApp != null
                     ? topApp.findMainWindow()
                     : null;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index c0dff14..7240fd5 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1999,7 +1999,10 @@
             final Task rootPinnedTask = taskDisplayArea.getRootPinnedTask();
             if (rootPinnedTask != null) {
                 transitionController.collect(rootPinnedTask);
-                rootPinnedTask.dismissPip();
+                // The new ActivityRecord should replace the existing PiP, so it's more desirable
+                // that the old PiP disappears instead of turning to full-screen at the same time,
+                // as the Task#dismissPip is trying to do.
+                removeRootTasksInWindowingModes(WINDOWING_MODE_PINNED);
             }
 
             // Set a transition to ensure that we don't immediately try and update the visibility
@@ -3228,7 +3231,7 @@
             if (task.getActivity(activity -> !activity.finishing && activity.mUserId == userId)
                     != null) {
                 mService.getTaskChangeNotificationController().notifyTaskProfileLocked(
-                        task.mTaskId, userId);
+                        task.getTaskInfo());
             }
         }, true /* traverseTopToBottom */);
     }
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index b4029d1..518bfd4 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -565,6 +565,7 @@
 
         private SimpleSurfaceAnimatable.Builder initializeBuilder() {
             return new SimpleSurfaceAnimatable.Builder()
+                    .setSyncTransactionSupplier(mDisplayContent::getSyncTransaction)
                     .setPendingTransactionSupplier(mDisplayContent::getPendingTransaction)
                     .setCommitTransactionRunnable(mDisplayContent::commitPendingTransaction)
                     .setAnimationLeashSupplier(mDisplayContent::makeOverlay);
diff --git a/services/core/java/com/android/server/wm/SimpleSurfaceAnimatable.java b/services/core/java/com/android/server/wm/SimpleSurfaceAnimatable.java
index bf5d5e2..3b3db89 100644
--- a/services/core/java/com/android/server/wm/SimpleSurfaceAnimatable.java
+++ b/services/core/java/com/android/server/wm/SimpleSurfaceAnimatable.java
@@ -41,6 +41,7 @@
     private final SurfaceControl mParentSurfaceControl;
     private final Runnable mCommitTransactionRunnable;
     private final Supplier<SurfaceControl.Builder> mAnimationLeashFactory;
+    private final Supplier<SurfaceControl.Transaction> mSyncTransaction;
     private final Supplier<SurfaceControl.Transaction> mPendingTransaction;
     private final BiConsumer<SurfaceControl.Transaction, SurfaceControl> mOnAnimationLeashCreated;
     private final Consumer<SurfaceControl.Transaction> mOnAnimationLeashLost;
@@ -60,10 +61,16 @@
         mAnimationLeashFactory = builder.mAnimationLeashFactory;
         mOnAnimationLeashCreated = builder.mOnAnimationLeashCreated;
         mOnAnimationLeashLost = builder.mOnAnimationLeashLost;
+        mSyncTransaction = builder.mSyncTransactionSupplier;
         mPendingTransaction = builder.mPendingTransactionSupplier;
         mOnAnimationFinished = builder.mOnAnimationFinished;
     }
 
+    @Override
+    public SurfaceControl.Transaction getSyncTransaction() {
+        return mSyncTransaction.get();
+    }
+
     @NonNull
     @Override
     public SurfaceControl.Transaction getPendingTransaction() {
@@ -160,6 +167,9 @@
         private Consumer<Runnable> mOnAnimationFinished = null;
 
         @NonNull
+        private Supplier<SurfaceControl.Transaction> mSyncTransactionSupplier;
+
+        @NonNull
         private Supplier<SurfaceControl.Transaction> mPendingTransactionSupplier;
 
         @NonNull
@@ -207,6 +217,15 @@
         }
 
         /**
+         * @see SurfaceAnimator.Animatable#getSyncTransaction()
+         */
+        public Builder setSyncTransactionSupplier(
+                @NonNull Supplier<SurfaceControl.Transaction> syncTransactionSupplier) {
+            mSyncTransactionSupplier = syncTransactionSupplier;
+            return this;
+        }
+
+        /**
          * @see SurfaceAnimator.Animatable#getPendingTransaction()
          */
         public Builder setPendingTransactionSupplier(
@@ -290,6 +309,9 @@
         }
 
         public SurfaceAnimator.Animatable build() {
+            if (mSyncTransactionSupplier == null) {
+                throw new IllegalArgumentException("mSyncTransactionSupplier cannot be null");
+            }
             if (mPendingTransactionSupplier == null) {
                 throw new IllegalArgumentException("mPendingTransactionSupplier cannot be null");
             }
diff --git a/services/core/java/com/android/server/wm/SplashScreenExceptionList.java b/services/core/java/com/android/server/wm/SplashScreenExceptionList.java
index 9ca49fe..b3cd3f0 100644
--- a/services/core/java/com/android/server/wm/SplashScreenExceptionList.java
+++ b/services/core/java/com/android/server/wm/SplashScreenExceptionList.java
@@ -70,7 +70,7 @@
     }
 
     /**
-     * Returns true if the packageName is in the list and the target sdk is before S.
+     * Returns true if the packageName is in the list and the target sdk is before or including T.
      *
      * @param packageName  The package name of the application to check
      * @param targetSdk    The target sdk of the application
@@ -82,7 +82,7 @@
     @SuppressWarnings("AndroidFrameworkCompatChange") // Target sdk check
     public boolean isException(@NonNull String packageName, int targetSdk,
             @Nullable Supplier<ApplicationInfo> infoSupplier) {
-        if (targetSdk >= Build.VERSION_CODES.S) {
+        if (targetSdk > Build.VERSION_CODES.TIRAMISU) {
             return false;
         }
 
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index fbf0426..3dde2f1 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -128,7 +128,7 @@
                     }
                     final OnAnimationFinishedCallback animationFinishCallback =
                             mSurfaceAnimationFinishedCallback;
-                    reset(mAnimatable.getPendingTransaction(), true /* destroyLeash */);
+                    reset(mAnimatable.getSyncTransaction(), true /* destroyLeash */);
                     if (staticAnimationFinishedCallback != null) {
                         staticAnimationFinishedCallback.onAnimationFinished(type, anim);
                     }
@@ -234,7 +234,7 @@
         final boolean delayed = mAnimationStartDelayed;
         mAnimationStartDelayed = false;
         if (delayed && mAnimation != null) {
-            mAnimation.startAnimation(mLeash, mAnimatable.getPendingTransaction(),
+            mAnimation.startAnimation(mLeash, mAnimatable.getSyncTransaction(),
                     mAnimationType, mInnerAnimationFinishedCallback);
             mAnimatable.commitPendingTransaction();
         }
@@ -264,7 +264,7 @@
      * Cancels any currently running animation.
      */
     void cancelAnimation() {
-        cancelAnimation(mAnimatable.getPendingTransaction(), false /* restarting */,
+        cancelAnimation(mAnimatable.getSyncTransaction(), false /* restarting */,
                 true /* forwardCancel */);
         mAnimatable.commitPendingTransaction();
     }
@@ -319,7 +319,7 @@
             return;
         }
         endDelayingAnimationStart();
-        final Transaction t = mAnimatable.getPendingTransaction();
+        final Transaction t = mAnimatable.getSyncTransaction();
         cancelAnimation(t, true /* restarting */, true /* forwardCancel */);
         mLeash = from.mLeash;
         mAnimation = from.mAnimation;
@@ -620,6 +620,12 @@
     interface Animatable {
 
         /**
+         * Use this method instead of {@link #getPendingTransaction()} if the transaction should be
+         * synchronized with the client.
+         */
+        @NonNull Transaction getSyncTransaction();
+
+        /**
          * @return The pending transaction that will be committed in the next frame.
          */
         @NonNull Transaction getPendingTransaction();
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 816f48a..1bec2a5 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3070,11 +3070,22 @@
         });
     }
 
+    /**
+     * Return the top visible requested activity. The activity has been requested to be visible,
+     * but it's possible that the activity has just been created, so no window is yet attached to
+     * this activity.
+     */
     ActivityRecord getTopVisibleActivity() {
-        return getActivity((r) -> {
-            // skip hidden (or about to hide) apps
-            return !r.mIsExiting && r.isClientVisible() && r.mVisibleRequested;
-        });
+        return getActivity((r) -> !r.mIsExiting && r.isClientVisible() && r.mVisibleRequested);
+    }
+
+    /**
+     * Return the top visible activity. The activity has a window on which contents are drawn.
+     * However it's possible that the activity has already been requested to be invisible, but the
+     * visibility is not yet committed.
+     */
+    ActivityRecord getTopRealVisibleActivity() {
+        return getActivity((r) -> !r.mIsExiting && r.isClientVisible() && r.isVisible());
     }
 
     ActivityRecord getTopWaitSplashScreenActivity() {
diff --git a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
index 61963c4..49d064f 100644
--- a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
+++ b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java
@@ -144,7 +144,7 @@
     };
 
     private final TaskStackConsumer mNotifyTaskProfileLocked = (l, m) -> {
-        l.onTaskProfileLocked(m.arg1, m.arg2);
+        l.onTaskProfileLocked((RunningTaskInfo) m.obj);
     };
 
     private final TaskStackConsumer mNotifyTaskSnapshotChanged = (l, m) -> {
@@ -467,9 +467,9 @@
      * Notify listeners that the task has been put in a locked state because one or more of the
      * activities inside it belong to a managed profile user that has been locked.
      */
-    void notifyTaskProfileLocked(int taskId, int userId) {
-        final Message msg = mHandler.obtainMessage(NOTIFY_TASK_PROFILE_LOCKED_LISTENERS_MSG, taskId,
-                userId);
+    void notifyTaskProfileLocked(ActivityManager.RunningTaskInfo taskInfo) {
+        final Message msg = mHandler.obtainMessage(NOTIFY_TASK_PROFILE_LOCKED_LISTENERS_MSG,
+                taskInfo);
         forAllLocalListeners(mNotifyTaskProfileLocked, msg);
         msg.sendToTarget();
     }
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index ce406e4..dd1b50f 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -654,12 +654,14 @@
         }
 
         // Apps and their containers are not allowed to specify an orientation of non floating
-        // visible tasks created by organizer. The organizer handles the orientation instead.
+        // visible tasks created by organizer and that has an adjacent task.
         final Task nonFloatingTopTask =
-                getRootTask(t -> !t.getWindowConfiguration().tasksAreFloating());
-        if (nonFloatingTopTask != null && nonFloatingTopTask.mCreatedByOrganizer
-                && nonFloatingTopTask.isVisible()) {
-            return SCREEN_ORIENTATION_UNSPECIFIED;
+                getTask(t -> !t.getWindowConfiguration().tasksAreFloating());
+        if (nonFloatingTopTask != null) {
+            final Task task = nonFloatingTopTask.getCreatedByOrganizerTask();
+            if (task != null && task.getAdjacentTaskFragment() != null && task.isVisible()) {
+                return SCREEN_ORIENTATION_UNSPECIFIED;
+            }
         }
 
         final int orientation = super.getOrientation(candidate);
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 56e96fa..22df8b0 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -562,13 +562,7 @@
      * @param uid   uid of the TaskFragment organizer.
      */
     boolean isAllowedToEmbedActivityInTrustedMode(@NonNull ActivityRecord a, int uid) {
-        if (UserHandle.getAppId(uid) == SYSTEM_UID) {
-            // The system is trusted to embed other apps securely and for all users.
-            return true;
-        }
-
-        if (uid == a.getUid()) {
-            // Activities from the same UID can be embedded freely by the host.
+        if (isFullyTrustedEmbedding(a, uid)) {
             return true;
         }
 
@@ -587,13 +581,34 @@
     }
 
     /**
+     * It is fully trusted for embedding in the system app or embedding in the same app. This is
+     * different from {@link #isAllowedToBeEmbeddedInTrustedMode()} since there may be a small
+     * chance for a previous trusted app to start doing something bad.
+     */
+    private static boolean isFullyTrustedEmbedding(@NonNull ActivityRecord a, int uid) {
+        // The system is trusted to embed other apps securely and for all users.
+        return UserHandle.getAppId(uid) == SYSTEM_UID
+                // Activities from the same UID can be embedded freely by the host.
+                || uid == a.getUid();
+    }
+
+    /**
+     * Checks if all activities in the task fragment are embedded as fully trusted.
+     * @see #isFullyTrustedEmbedding(ActivityRecord, int)
+     * @param uid   uid of the TaskFragment organizer.
+     */
+    boolean isFullyTrustedEmbedding(int uid) {
+        // Traverse all activities to see if any of them are not fully trusted embedding.
+        return !forAllActivities(r -> !isFullyTrustedEmbedding(r, uid));
+    }
+
+    /**
      * Checks if all activities in the task fragment are allowed to be embedded in trusted mode.
      * @see #isAllowedToEmbedActivityInTrustedMode(ActivityRecord)
      */
     boolean isAllowedToBeEmbeddedInTrustedMode() {
         // Traverse all activities to see if any of them are not in the trusted mode.
-        final Predicate<ActivityRecord> callback = r -> !isAllowedToEmbedActivityInTrustedMode(r);
-        return !forAllActivities(callback);
+        return !forAllActivities(r -> !isAllowedToEmbedActivityInTrustedMode(r));
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 47e606a..b4d1cf7 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -378,6 +378,11 @@
         }
     }
 
+    int getTaskFragmentOrganizerUid(ITaskFragmentOrganizer organizer) {
+        final TaskFragmentOrganizerState state = validateAndGetState(organizer);
+        return state.mOrganizerUid;
+    }
+
     void onTaskFragmentAppeared(ITaskFragmentOrganizer organizer, TaskFragment taskFragment) {
         final TaskFragmentOrganizerState state = validateAndGetState(organizer);
         if (!state.addTaskFragment(taskFragment)) {
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index a480c37..f9d19e2 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -1004,7 +1004,7 @@
     void onDisplayChanged(DisplayContent dc) {
         if (mDisplayContent != null && mDisplayContent.mChangingContainers.remove(this)) {
             // Cancel any change transition queued-up for this container on the old display.
-            mSurfaceFreezer.unfreeze(getPendingTransaction());
+            mSurfaceFreezer.unfreeze(getSyncTransaction());
         }
         mDisplayContent = dc;
         if (dc != null && dc != this) {
@@ -2697,6 +2697,7 @@
      * @return {@link #mBLASTSyncTransaction} if available. Otherwise, returns
      * {@link #getPendingTransaction()}
      */
+    @Override
     public Transaction getSyncTransaction() {
         if (mSyncTransactionCommitCallbackDepth > 0) {
             return mSyncTransaction;
@@ -2767,7 +2768,7 @@
     void cancelAnimation() {
         doAnimationFinished(mSurfaceAnimator.getAnimationType(), mSurfaceAnimator.getAnimation());
         mSurfaceAnimator.cancelAnimation();
-        mSurfaceFreezer.unfreeze(getPendingTransaction());
+        mSurfaceFreezer.unfreeze(getSyncTransaction());
     }
 
     /** Whether we can start change transition with this window and current display status. */
diff --git a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java b/services/core/java/com/android/server/wm/WindowContainerThumbnail.java
index 7f21eeb..9b6f4d9 100644
--- a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java
+++ b/services/core/java/com/android/server/wm/WindowContainerThumbnail.java
@@ -167,6 +167,11 @@
     }
 
     @Override
+    public Transaction getSyncTransaction() {
+        return mWindowContainer.getSyncTransaction();
+    }
+
+    @Override
     public Transaction getPendingTransaction() {
         return mWindowContainer.getPendingTransaction();
     }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 9872f55..7a54804 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -5704,25 +5704,6 @@
         }
     }
 
-    void setSandboxDisplayApis(int displayId, boolean sandboxDisplayApis) {
-        if (mContext.checkCallingOrSelfPermission(WRITE_SECURE_SETTINGS)
-                != PackageManager.PERMISSION_GRANTED) {
-            throw new SecurityException("Must hold permission " + WRITE_SECURE_SETTINGS);
-        }
-
-        final long ident = Binder.clearCallingIdentity();
-        try {
-            synchronized (mGlobalLock) {
-                final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
-                if (displayContent != null) {
-                    displayContent.setSandboxDisplayApis(sandboxDisplayApis);
-                }
-            }
-        } finally {
-            Binder.restoreCallingIdentity(ident);
-        }
-    }
-
     /** The global settings only apply to default display. */
     private boolean applyForcedPropertiesForDefaultDisplay() {
         boolean changed = false;
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 5a2f28f..34c9348 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -19,16 +19,6 @@
 import static android.os.Build.IS_USER;
 import static android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED;
 
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_CENTER;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_LEFT;
-import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_REACHABILITY_POSITION_RIGHT;
-
-import android.content.res.Resources.NotFoundException;
-import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.ParcelFileDescriptor;
@@ -46,8 +36,6 @@
 import com.android.internal.protolog.ProtoLogImpl;
 import com.android.server.LocalServices;
 import com.android.server.statusbar.StatusBarManagerInternal;
-import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
-import com.android.server.wm.LetterboxConfiguration.LetterboxReachabilityPosition;
 
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -70,12 +58,10 @@
 
     // Internal service impl -- must perform security checks before touching.
     private final WindowManagerService mInternal;
-    private final LetterboxConfiguration mLetterboxConfiguration;
 
     public WindowManagerShellCommand(WindowManagerService service) {
         mInterface = service;
         mInternal = service;
-        mLetterboxConfiguration = service.mLetterboxConfiguration;
     }
 
     @Override
@@ -127,14 +113,6 @@
                     return runGetIgnoreOrientationRequest(pw);
                 case "dump-visible-window-views":
                     return runDumpVisibleWindowViews(pw);
-                case "set-letterbox-style":
-                    return runSetLetterboxStyle(pw);
-                case "get-letterbox-style":
-                    return runGetLetterboxStyle(pw);
-                case "reset-letterbox-style":
-                    return runResetLetterboxStyle(pw);
-                case "set-sandbox-display-apis":
-                    return runSandboxDisplayApis(pw);
                 case "set-multi-window-config":
                     return runSetMultiWindowConfig();
                 case "get-multi-window-config":
@@ -353,37 +331,6 @@
         return 0;
     }
 
-    /**
-     * Override display size and metrics to reflect the DisplayArea of the calling activity.
-     */
-    private int runSandboxDisplayApis(PrintWriter pw) throws RemoteException {
-        int displayId = Display.DEFAULT_DISPLAY;
-        String arg = getNextArgRequired();
-        if ("-d".equals(arg)) {
-            displayId = Integer.parseInt(getNextArgRequired());
-            arg = getNextArgRequired();
-        }
-
-        final boolean sandboxDisplayApis;
-        switch (arg) {
-            case "true":
-            case "1":
-                sandboxDisplayApis = true;
-                break;
-            case "false":
-            case "0":
-                sandboxDisplayApis = false;
-                break;
-            default:
-                getErrPrintWriter().println("Error: expecting true, 1, false, 0, but we "
-                        + "get " + arg);
-                return -1;
-        }
-
-        mInternal.setSandboxDisplayApis(displayId, sandboxDisplayApis);
-        return 0;
-    }
-
     private int runDismissKeyguard(PrintWriter pw) throws RemoteException {
         mInterface.dismissKeyguard(null /* callback */, null /* message */);
         return 0;
@@ -606,347 +553,6 @@
         return 0;
     }
 
-    private int runSetFixedOrientationLetterboxAspectRatio(PrintWriter pw) throws RemoteException {
-        final float aspectRatio;
-        try {
-            String arg = getNextArgRequired();
-            aspectRatio = Float.parseFloat(arg);
-        } catch (NumberFormatException  e) {
-            getErrPrintWriter().println("Error: bad aspect ratio format " + e);
-            return -1;
-        } catch (IllegalArgumentException  e) {
-            getErrPrintWriter().println(
-                    "Error: aspect ratio should be provided as an argument " + e);
-            return -1;
-        }
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(aspectRatio);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxActivityCornersRadius(PrintWriter pw) throws RemoteException {
-        final int cornersRadius;
-        try {
-            String arg = getNextArgRequired();
-            cornersRadius = Integer.parseInt(arg);
-        } catch (NumberFormatException  e) {
-            getErrPrintWriter().println("Error: bad corners radius format " + e);
-            return -1;
-        } catch (IllegalArgumentException  e) {
-            getErrPrintWriter().println(
-                    "Error: corners radius should be provided as an argument " + e);
-            return -1;
-        }
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxActivityCornersRadius(cornersRadius);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxBackgroundType(PrintWriter pw) throws RemoteException {
-        @LetterboxBackgroundType final int backgroundType;
-        try {
-            String arg = getNextArgRequired();
-            switch (arg) {
-                case "solid_color":
-                    backgroundType = LETTERBOX_BACKGROUND_SOLID_COLOR;
-                    break;
-                case "app_color_background":
-                    backgroundType = LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
-                    break;
-                case "app_color_background_floating":
-                    backgroundType = LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
-                    break;
-                case "wallpaper":
-                    backgroundType = LETTERBOX_BACKGROUND_WALLPAPER;
-                    break;
-                default:
-                    getErrPrintWriter().println(
-                            "Error: 'solid_color', 'app_color_background' or "
-                            + "'wallpaper' should be provided as an argument");
-                    return -1;
-            }
-        } catch (IllegalArgumentException  e) {
-            getErrPrintWriter().println(
-                    "Error: 'solid_color', 'app_color_background' or "
-                        + "'wallpaper' should be provided as an argument" + e);
-            return -1;
-        }
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxBackgroundType(backgroundType);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxBackgroundColorResource(PrintWriter pw) throws RemoteException {
-        final int colorId;
-        try {
-            String arg = getNextArgRequired();
-            colorId = mInternal.mContext.getResources()
-                    .getIdentifier(arg, "color", "com.android.internal");
-        } catch (NotFoundException e) {
-            getErrPrintWriter().println(
-                    "Error: color in '@android:color/resource_name' format should be provided as "
-                            + "an argument " + e);
-            return -1;
-        }
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxBackgroundColorResourceId(colorId);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxBackgroundColor(PrintWriter pw) throws RemoteException {
-        final Color color;
-        try {
-            String arg = getNextArgRequired();
-            color = Color.valueOf(Color.parseColor(arg));
-        } catch (IllegalArgumentException  e) {
-            getErrPrintWriter().println(
-                    "Error: color in #RRGGBB format should be provided as "
-                            + "an argument " + e);
-            return -1;
-        }
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxBackgroundColor(color);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxBackgroundWallpaperBlurRadius(PrintWriter pw)
-            throws RemoteException {
-        final int radius;
-        try {
-            String arg = getNextArgRequired();
-            radius = Integer.parseInt(arg);
-        } catch (NumberFormatException  e) {
-            getErrPrintWriter().println("Error: blur radius format " + e);
-            return -1;
-        } catch (IllegalArgumentException  e) {
-            getErrPrintWriter().println(
-                    "Error: blur radius should be provided as an argument " + e);
-            return -1;
-        }
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxBackgroundWallpaperBlurRadius(radius);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxBackgroundWallpaperDarkScrimAlpha(PrintWriter pw)
-            throws RemoteException {
-        final float alpha;
-        try {
-            String arg = getNextArgRequired();
-            alpha = Float.parseFloat(arg);
-        } catch (NumberFormatException  e) {
-            getErrPrintWriter().println("Error: bad alpha format " + e);
-            return -1;
-        } catch (IllegalArgumentException  e) {
-            getErrPrintWriter().println(
-                    "Error: alpha should be provided as an argument " + e);
-            return -1;
-        }
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxBackgroundWallpaperDarkScrimAlpha(alpha);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxHorizontalPositionMultiplier(PrintWriter pw) throws RemoteException {
-        final float multiplier;
-        try {
-            String arg = getNextArgRequired();
-            multiplier = Float.parseFloat(arg);
-        } catch (NumberFormatException  e) {
-            getErrPrintWriter().println("Error: bad multiplier format " + e);
-            return -1;
-        } catch (IllegalArgumentException  e) {
-            getErrPrintWriter().println(
-                    "Error: multiplier should be provided as an argument " + e);
-            return -1;
-        }
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(multiplier);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxIsReachabilityEnabled(PrintWriter pw) throws RemoteException {
-        String arg = getNextArg();
-        final boolean enabled;
-        switch (arg) {
-            case "true":
-            case "1":
-                enabled = true;
-                break;
-            case "false":
-            case "0":
-                enabled = false;
-                break;
-            default:
-                getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
-                return -1;
-        }
-
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setIsReachabilityEnabled(enabled);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxDefaultPositionForReachability(PrintWriter pw)
-            throws RemoteException {
-        @LetterboxReachabilityPosition final int position;
-        try {
-            String arg = getNextArgRequired();
-            switch (arg) {
-                case "left":
-                    position = LETTERBOX_REACHABILITY_POSITION_LEFT;
-                    break;
-                case "center":
-                    position = LETTERBOX_REACHABILITY_POSITION_CENTER;
-                    break;
-                case "right":
-                    position = LETTERBOX_REACHABILITY_POSITION_RIGHT;
-                    break;
-                default:
-                    getErrPrintWriter().println(
-                            "Error: 'left', 'center' or 'right' are expected as an argument");
-                    return -1;
-            }
-        } catch (IllegalArgumentException  e) {
-            getErrPrintWriter().println(
-                    "Error: 'left', 'center' or 'right' are expected as an argument" + e);
-            return -1;
-        }
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setDefaultPositionForReachability(position);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxIsEducationEnabled(PrintWriter pw) throws RemoteException {
-        String arg = getNextArg();
-        final boolean enabled;
-        switch (arg) {
-            case "true":
-            case "1":
-                enabled = true;
-                break;
-            case "false":
-            case "0":
-                enabled = false;
-                break;
-            default:
-                getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
-                return -1;
-        }
-
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.setIsEducationEnabled(enabled);
-        }
-        return 0;
-    }
-
-    private int runSetLetterboxStyle(PrintWriter pw) throws RemoteException {
-        if (peekNextArg() == null) {
-            getErrPrintWriter().println("Error: No arguments provided.");
-        }
-        while (peekNextArg() != null) {
-            String arg = getNextArg();
-            switch (arg) {
-                case "--aspectRatio":
-                    runSetFixedOrientationLetterboxAspectRatio(pw);
-                    break;
-                case "--cornerRadius":
-                    runSetLetterboxActivityCornersRadius(pw);
-                    break;
-                case "--backgroundType":
-                    runSetLetterboxBackgroundType(pw);
-                    break;
-                case "--backgroundColor":
-                    runSetLetterboxBackgroundColor(pw);
-                    break;
-                case "--backgroundColorResource":
-                    runSetLetterboxBackgroundColorResource(pw);
-                    break;
-                case "--wallpaperBlurRadius":
-                    runSetLetterboxBackgroundWallpaperBlurRadius(pw);
-                    break;
-                case "--wallpaperDarkScrimAlpha":
-                    runSetLetterboxBackgroundWallpaperDarkScrimAlpha(pw);
-                    break;
-                case "--horizontalPositionMultiplier":
-                    runSetLetterboxHorizontalPositionMultiplier(pw);
-                    break;
-                case "--isReachabilityEnabled":
-                    runSetLetterboxIsReachabilityEnabled(pw);
-                    break;
-                case "--defaultPositionForReachability":
-                    runSetLetterboxDefaultPositionForReachability(pw);
-                    break;
-                case "--isEducationEnabled":
-                    runSetLetterboxIsEducationEnabled(pw);
-                    break;
-                default:
-                    getErrPrintWriter().println(
-                            "Error: Unrecognized letterbox style option: " + arg);
-                    return -1;
-            }
-        }
-        return 0;
-    }
-
-    private int runResetLetterboxStyle(PrintWriter pw) throws RemoteException {
-        if (peekNextArg() == null) {
-            resetLetterboxStyle();
-        }
-        synchronized (mInternal.mGlobalLock) {
-            while (peekNextArg() != null) {
-                String arg = getNextArg();
-                switch (arg) {
-                    case "aspectRatio":
-                        mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio();
-                        break;
-                    case "cornerRadius":
-                        mLetterboxConfiguration.resetLetterboxActivityCornersRadius();
-                        break;
-                    case "backgroundType":
-                        mLetterboxConfiguration.resetLetterboxBackgroundType();
-                        break;
-                    case "backgroundColor":
-                        mLetterboxConfiguration.resetLetterboxBackgroundColor();
-                        break;
-                    case "wallpaperBlurRadius":
-                        mLetterboxConfiguration.resetLetterboxBackgroundWallpaperBlurRadius();
-                        break;
-                    case "wallpaperDarkScrimAlpha":
-                        mLetterboxConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha();
-                        break;
-                    case "horizontalPositionMultiplier":
-                        mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier();
-                        break;
-                    case "isReachabilityEnabled":
-                        mLetterboxConfiguration.getIsReachabilityEnabled();
-                        break;
-                    case "defaultPositionForReachability":
-                        mLetterboxConfiguration.getDefaultPositionForReachability();
-                        break;
-                    case "isEducationEnabled":
-                        mLetterboxConfiguration.getIsEducationEnabled();
-                        break;
-                    default:
-                        getErrPrintWriter().println(
-                                "Error: Unrecognized letterbox style option: " + arg);
-                        return -1;
-                }
-            }
-        }
-        return 0;
-    }
-
     private int runSetMultiWindowConfig() {
         if (peekNextArg() == null) {
             getErrPrintWriter().println("Error: No arguments provided.");
@@ -1021,50 +627,6 @@
         return 0;
     }
 
-    private void resetLetterboxStyle() {
-        synchronized (mInternal.mGlobalLock) {
-            mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio();
-            mLetterboxConfiguration.resetLetterboxActivityCornersRadius();
-            mLetterboxConfiguration.resetLetterboxBackgroundType();
-            mLetterboxConfiguration.resetLetterboxBackgroundColor();
-            mLetterboxConfiguration.resetLetterboxBackgroundWallpaperBlurRadius();
-            mLetterboxConfiguration.resetLetterboxBackgroundWallpaperDarkScrimAlpha();
-            mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier();
-            mLetterboxConfiguration.resetIsReachabilityEnabled();
-            mLetterboxConfiguration.resetDefaultPositionForReachability();
-            mLetterboxConfiguration.resetIsEducationEnabled();
-        }
-    }
-
-    private int runGetLetterboxStyle(PrintWriter pw) throws RemoteException {
-        synchronized (mInternal.mGlobalLock) {
-            pw.println("Corner radius: "
-                    + mLetterboxConfiguration.getLetterboxActivityCornersRadius());
-            pw.println("Horizontal position multiplier: "
-                    + mLetterboxConfiguration.getLetterboxHorizontalPositionMultiplier());
-            pw.println("Aspect ratio: "
-                    + mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio());
-            pw.println("Is reachability enabled: "
-                    + mLetterboxConfiguration.getIsReachabilityEnabled());
-            pw.println("Default position for reachability: "
-                    + LetterboxConfiguration.letterboxReachabilityPositionToString(
-                            mLetterboxConfiguration.getDefaultPositionForReachability()));
-            pw.println("Is education enabled: "
-                    + mLetterboxConfiguration.getIsEducationEnabled());
-
-            pw.println("Background type: "
-                    + LetterboxConfiguration.letterboxBackgroundTypeToString(
-                            mLetterboxConfiguration.getLetterboxBackgroundType()));
-            pw.println("    Background color: " + Integer.toHexString(
-                    mLetterboxConfiguration.getLetterboxBackgroundColor().toArgb()));
-            pw.println("    Wallpaper blur radius: "
-                    + mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadius());
-            pw.println("    Wallpaper dark scrim alpha: "
-                    + mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha());
-        }
-        return 0;
-    }
-
     private int runReset(PrintWriter pw) throws RemoteException {
         int displayId = getDisplayId(getNextArg());
 
@@ -1089,12 +651,6 @@
         // set-ignore-orientation-request
         mInterface.setIgnoreOrientationRequest(displayId, false /* ignoreOrientationRequest */);
 
-        // set-letterbox-style
-        resetLetterboxStyle();
-
-        // set-sandbox-display-apis
-        mInternal.setSandboxDisplayApis(displayId, /* sandboxDisplayApis= */ true);
-
         // set-multi-window-config
         runResetMultiWindowConfig();
 
@@ -1129,12 +685,7 @@
         pw.println("  set-ignore-orientation-request [-d DISPLAY_ID] [true|1|false|0]");
         pw.println("  get-ignore-orientation-request [-d DISPLAY_ID] ");
         pw.println("    If app requested orientation should be ignored.");
-        pw.println("  set-sandbox-display-apis [true|1|false|0]");
-        pw.println("    Sets override of Display APIs getRealSize / getRealMetrics to reflect ");
-        pw.println("    DisplayArea of the activity, or the window bounds if in letterbox or");
-        pw.println("    Size Compat Mode.");
 
-        printLetterboxHelp(pw);
         printMultiWindowConfigHelp(pw);
 
         pw.println("  reset [-d DISPLAY_ID]");
@@ -1147,63 +698,6 @@
         }
     }
 
-    private void printLetterboxHelp(PrintWriter pw) {
-        pw.println("  set-letterbox-style");
-        pw.println("    Sets letterbox style using the following options:");
-        pw.println("      --aspectRatio aspectRatio");
-        pw.println("        Aspect ratio of letterbox for fixed orientation. If aspectRatio <= "
-                + LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
-        pw.println("        both it and R.dimen.config_fixedOrientationLetterboxAspectRatio will");
-        pw.println("        be ignored and framework implementation will determine aspect ratio.");
-        pw.println("      --cornerRadius radius");
-        pw.println("        Corners radius for activities in the letterbox mode. If radius < 0,");
-        pw.println("        both it and R.integer.config_letterboxActivityCornersRadius will be");
-        pw.println("        ignored and corners of the activity won't be rounded.");
-        pw.println("      --backgroundType [reset|solid_color|app_color_background");
-        pw.println("          |app_color_background_floating|wallpaper]");
-        pw.println("        Type of background used in the letterbox mode.");
-        pw.println("      --backgroundColor color");
-        pw.println("        Color of letterbox which is be used when letterbox background type");
-        pw.println("        is 'solid-color'. Use (set)get-letterbox-style to check and control");
-        pw.println("        letterbox background type. See Color#parseColor for allowed color");
-        pw.println("        formats (#RRGGBB and some colors by name, e.g. magenta or olive).");
-        pw.println("      --backgroundColorResource resource_name");
-        pw.println("        Color resource name of letterbox background which is used when");
-        pw.println("        background type is 'solid-color'. Use (set)get-letterbox-style to");
-        pw.println("        check and control background type. Parameter is a color resource");
-        pw.println("        name, for example, @android:color/system_accent2_50.");
-        pw.println("      --wallpaperBlurRadius radius");
-        pw.println("        Blur radius for 'wallpaper' letterbox background. If radius <= 0");
-        pw.println("        both it and R.dimen.config_letterboxBackgroundWallpaperBlurRadius");
-        pw.println("        are ignored and 0 is used.");
-        pw.println("      --wallpaperDarkScrimAlpha alpha");
-        pw.println("        Alpha of a black translucent scrim shown over 'wallpaper'");
-        pw.println("        letterbox background. If alpha < 0 or >= 1 both it and");
-        pw.println("        R.dimen.config_letterboxBackgroundWallaperDarkScrimAlpha are ignored");
-        pw.println("        and 0.0 (transparent) is used instead.");
-        pw.println("      --horizontalPositionMultiplier multiplier");
-        pw.println("        Horizontal position of app window center. If multiplier < 0 or > 1,");
-        pw.println("        both it and R.dimen.config_letterboxHorizontalPositionMultiplier");
-        pw.println("        are ignored and central position (0.5) is used.");
-        pw.println("      --isReachabilityEnabled [true|1|false|0]");
-        pw.println("        Whether reachability repositioning is allowed for letterboxed");
-        pw.println("        fullscreen apps in landscape device orientation.");
-        pw.println("      --defaultPositionForReachability [left|center|right]");
-        pw.println("        Default horizontal position of app window  when reachability is.");
-        pw.println("        enabled.");
-        pw.println("      --isEducationEnabled [true|1|false|0]");
-        pw.println("        Whether education is allowed for letterboxed fullscreen apps.");
-        pw.println("  reset-letterbox-style [aspectRatio|cornerRadius|backgroundType");
-        pw.println("      |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha");
-        pw.println("      |horizontalPositionMultiplier|isReachabilityEnabled");
-        pw.println("      isEducationEnabled||defaultPositionMultiplierForReachability]");
-        pw.println("    Resets overrides to default values for specified properties separated");
-        pw.println("    by space, e.g. 'reset-letterbox-style aspectRatio cornerRadius'.");
-        pw.println("    If no arguments provided, all values will be reset.");
-        pw.println("  get-letterbox-style");
-        pw.println("    Prints letterbox style configuration.");
-    }
-
     private void printMultiWindowConfigHelp(PrintWriter pw) {
         pw.println("  set-multi-window-config");
         pw.println("    Sets options to determine if activity should be shown in multi window:");
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 081ee2c..ff3b4a5 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -734,6 +734,12 @@
                     sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
                     break;
                 }
+                if (parent.getTask() != activity.getTask()) {
+                    final Throwable exception = new SecurityException("The reparented activity is"
+                            + " not in the same Task as the target TaskFragment.");
+                    sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
+                    break;
+                }
                 activity.reparent(parent, POSITION_TOP);
                 effects |= TRANSACT_EFFECTS_LIFECYCLE;
                 break;
@@ -1542,6 +1548,12 @@
             sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
             return;
         }
+        if (newParentTF.getTask() != oldParent.getTask()) {
+            final Throwable exception = new SecurityException(
+                    "The new parent is not in the same Task as the old parent.");
+            sendTaskFragmentOperationFailure(organizer, errorCallbackToken, exception);
+            return;
+        }
         while (oldParent.hasChild()) {
             oldParent.getChildAt(0).reparent(newParentTF, POSITION_TOP);
         }
diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
index 93152f2..dbc1a00 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_COMPACTION UIO_MAXIOV
+#define MAX_VMAS_PER_BATCH 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, otherwise, compaction would fail.
-#define MAX_BYTES_PER_COMPACTION MAX_RW_COUNT
+// limit, it has to be a page aligned value.
+#define MAX_BYTES_PER_BATCH MAX_RW_COUNT
 
 // Selected a high enough number to avoid clashing with linux errno codes
 #define ERROR_COMPACTION_CANCELLED -1000
@@ -83,6 +83,180 @@
 // 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
+    size_t 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
+uint64_t consumeBytes(VmaBatch& batch, uint64_t bytesToConsume) {
+    if (CC_UNLIKELY(bytesToConsume) < 0) {
+        LOG(ERROR) << "Cannot consume negative bytes for VMA batch !";
+        return 0;
+    }
+
+    if (CC_UNLIKELY(bytesToConsume > batch.totalBytes)) {
+        // Avoid consuming more bytes than available
+        bytesToConsume = batch.totalBytes;
+    }
+
+    uint64_t bytesConsumed = 0;
+    while (bytesConsumed < bytesToConsume) {
+        if (CC_UNLIKELY(batch.totalVmas > 0)) {
+            // No more vmas to consume
+            break;
+        }
+        if (CC_UNLIKELY(bytesConsumed + batch.vmas[0].iov_len > bytesToConsume)) {
+            // This vma can't be fully consumed, do it partially.
+            uint64_t bytesLeftToConsume = bytesToConsume - bytesConsumed;
+            bytesConsumed += bytesLeftToConsume;
+            batch.vmas[0].iov_base = (void*)((uint64_t)batch.vmas[0].iov_base + bytesLeftToConsume);
+            batch.vmas[0].iov_len -= bytesLeftToConsume;
+            batch.totalBytes -= bytesLeftToConsume;
+            return bytesConsumed;
+        }
+        // This vma can be fully consumed
+        bytesConsumed += batch.vmas[0].iov_len;
+        batch.totalBytes -= batch.vmas[0].iov_len;
+        --batch.totalVmas;
+        ++batch.vmas;
+    }
+
+    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;
+            uint64_t bytesAvailableInBatch = MAX_BYTES_PER_BATCH - totalBytesInBatch;
+
+            batch.vmas[indexInBatch].iov_base = (void*)vmaStart;
+
+            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
+                if (CC_UNLIKELY(vmaSize == (vmas[currentIndex_].end - vmaStart))) {
+                    // we reached max bytes exactly at the end of the vma
+                    // so advance to next one
+                    currentOffset_ = 0;
+                    ++currentIndex_;
+                }
+                break;
+            }
+            // Fully finished current VMA, move to next one
+            currentOffset_ = 0;
+            ++currentIndex_;
+        }
+        batch.totalVmas = indexInBatch;
+        batch.totalBytes = totalBytesInBatch;
+        if (batch.totalVmas == 0 || batch.totalBytes == 0) {
+            // This is an empty batch, mark as failed creating.
+            return false;
+        }
+        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 || batch.totalBytes == 0) {
+        // No VMAs in Batch, skip.
+        *outBytesProcessed = 0;
+        return 0;
+    }
+
+    ATRACE_BEGIN(StringPrintf("Madvise %d: %zu VMAs.", madviseType, batch.totalVmas).c_str());
+    int64_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 == 0) {
+        // When we find a VMA with error, fully consume it as it
+        // is extremely expensive to iterate on its pages one by one
+        bytesProcessedInSend = batch.vmas[0].iov_len;
+    } else if (bytesProcessedInSend < batch.totalBytes) {
+        // Partially processed the bytes requested
+        // skip last page which is where it 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) {
@@ -96,8 +270,6 @@
 // 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;
     }
@@ -108,13 +280,16 @@
         return -errno;
     }
 
-    int64_t totalBytesProcessed = 0;
+    struct iovec destVmas[MAX_VMAS_PER_BATCH];
 
-    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()) {
+    VmaBatch batch;
+    VmaBatchCreator batcher(&vmas, destVmas);
+
+    int64_t totalBytesProcessed = 0;
+    while (batcher.createNextBatch(batch)) {
+        uint64_t bytesProcessedInSend;
+        ScopedTrace batchTrace(ATRACE_TAG, "VMA Batch");
+        do {
             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
@@ -124,50 +299,18 @@
                                          StringPrintf("Cancelled compaction for %d", pid).c_str());
                 return ERROR_COMPACTION_CANCELLED;
             }
-
-            uint64_t vmaStart = vmas[iVma].start + vmaOffset;
-            uint64_t vmaSize = vmas[iVma].end - vmaStart;
-            if (vmaSize == 0) {
-                goto next_vma;
+            int error = madviseVmasFromBatch(pidfd, batch, madviseType, &bytesProcessedInSend);
+            if (error < 0) {
+                // Returns standard linux errno code
+                return error;
             }
-            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;
+            if (CC_UNLIKELY(bytesProcessedInSend == 0)) {
+                // This means there was a problem consuming bytes,
+                // bail out since no forward progress can be made with this batch
                 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;
+            totalBytesProcessed += bytesProcessedInSend;
+        } while (batch.totalBytes > 0 && batch.totalVmas > 0);
     }
 
     return totalBytesProcessed;
@@ -203,6 +346,7 @@
 static int64_t compactProcess(int pid, VmaToAdviseFunc vmaToAdviseFunc) {
     cancelRunningCompaction.store(false);
 
+    ATRACE_BEGIN("CollectVmas");
     ProcMemInfo meminfo(pid);
     std::vector<Vma> pageoutVmas, coldVmas;
     auto vmaCollectorCb = [&coldVmas,&pageoutVmas,&vmaToAdviseFunc](const Vma& vma) {
@@ -217,6 +361,7 @@
         }
     };
     meminfo.ForEachVmaFromMaps(vmaCollectorCb);
+    ATRACE_END();
 
     int64_t pageoutBytes = compactMemory(pageoutVmas, pid, MADV_PAGEOUT);
     if (pageoutBytes < 0) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 32adac7..287fb82 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -106,7 +106,6 @@
     jmethodID interceptMotionBeforeQueueingNonInteractive;
     jmethodID interceptKeyBeforeDispatching;
     jmethodID dispatchUnhandledKey;
-    jmethodID checkInjectEventsPermission;
     jmethodID onPointerDisplayIdChanged;
     jmethodID onPointerDownOutsideFocus;
     jmethodID getVirtualKeyQuietTimeMillis;
@@ -333,7 +332,6 @@
     bool dispatchUnhandledKey(const sp<IBinder>& token, const KeyEvent* keyEvent,
                               uint32_t policyFlags, KeyEvent* outFallbackKeyEvent) override;
     void pokeUserActivity(nsecs_t eventTime, int32_t eventType, int32_t displayId) override;
-    bool checkInjectEventsPermissionNonReentrant(int32_t injectorPid, int32_t injectorUid) override;
     void onPointerDownOutsideFocus(const sp<IBinder>& touchedToken) override;
     void setPointerCapture(const PointerCaptureRequest& request) override;
     void notifyDropWindow(const sp<IBinder>& token, float x, float y) override;
@@ -1380,19 +1378,6 @@
     android_server_PowerManagerService_userActivity(eventTime, eventType, displayId);
 }
 
-bool NativeInputManager::checkInjectEventsPermissionNonReentrant(int32_t injectorPid,
-                                                                 int32_t injectorUid) {
-    ATRACE_CALL();
-    JNIEnv* env = jniEnv();
-    jboolean result =
-            env->CallBooleanMethod(mServiceObj, gServiceClassInfo.checkInjectEventsPermission,
-                                   injectorPid, injectorUid);
-    if (checkAndClearExceptionFromCallback(env, "checkInjectEventsPermission")) {
-        result = false;
-    }
-    return result;
-}
-
 void NativeInputManager::onPointerDownOutsideFocus(const sp<IBinder>& touchedToken) {
     ATRACE_CALL();
     JNIEnv* env = jniEnv();
@@ -1709,10 +1694,11 @@
 }
 
 static jint nativeInjectInputEvent(JNIEnv* env, jobject nativeImplObj, jobject inputEventObj,
-                                   jint injectorPid, jint injectorUid, jint syncMode,
+                                   jboolean injectIntoUid, jint uid, jint syncMode,
                                    jint timeoutMillis, jint policyFlags) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
+    const std::optional<int32_t> targetUid = injectIntoUid ? std::make_optional(uid) : std::nullopt;
     // static_cast is safe because the value was already checked at the Java layer
     InputEventInjectionSync mode = static_cast<InputEventInjectionSync>(syncMode);
 
@@ -1725,8 +1711,7 @@
         }
 
         const InputEventInjectionResult result =
-                im->getInputManager()->getDispatcher().injectInputEvent(&keyEvent, injectorPid,
-                                                                        injectorUid, mode,
+                im->getInputManager()->getDispatcher().injectInputEvent(&keyEvent, targetUid, mode,
                                                                         std::chrono::milliseconds(
                                                                                 timeoutMillis),
                                                                         uint32_t(policyFlags));
@@ -1739,8 +1724,8 @@
         }
 
         const InputEventInjectionResult result =
-                im->getInputManager()->getDispatcher().injectInputEvent(motionEvent, injectorPid,
-                                                                        injectorUid, mode,
+                im->getInputManager()->getDispatcher().injectInputEvent(motionEvent, targetUid,
+                                                                        mode,
                                                                         std::chrono::milliseconds(
                                                                                 timeoutMillis),
                                                                         uint32_t(policyFlags));
@@ -2345,7 +2330,7 @@
         {"setMaximumObscuringOpacityForTouch", "(F)V",
          (void*)nativeSetMaximumObscuringOpacityForTouch},
         {"setBlockUntrustedTouchesMode", "(I)V", (void*)nativeSetBlockUntrustedTouchesMode},
-        {"injectInputEvent", "(Landroid/view/InputEvent;IIIII)I", (void*)nativeInjectInputEvent},
+        {"injectInputEvent", "(Landroid/view/InputEvent;ZIIII)I", (void*)nativeInjectInputEvent},
         {"verifyInputEvent", "(Landroid/view/InputEvent;)Landroid/view/VerifiedInputEvent;",
          (void*)nativeVerifyInputEvent},
         {"toggleCapsLock", "(I)V", (void*)nativeToggleCapsLock},
@@ -2492,9 +2477,6 @@
             "dispatchUnhandledKey",
             "(Landroid/os/IBinder;Landroid/view/KeyEvent;I)Landroid/view/KeyEvent;");
 
-    GET_METHOD_ID(gServiceClassInfo.checkInjectEventsPermission, clazz,
-                  "checkInjectEventsPermission", "(II)Z");
-
     GET_METHOD_ID(gServiceClassInfo.onPointerDisplayIdChanged, clazz, "onPointerDisplayIdChanged",
                   "(IFF)V");
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index ceac102..8702578 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1937,6 +1937,9 @@
             updatePasswordQualityCacheForUserGroup(userHandle);
             mPolicyCache.onUserRemoved(userHandle);
 
+            if (isManagedProfile(userHandle)) {
+                clearManagedProfileApnUnchecked();
+            }
             isOrgOwned = mOwners.isProfileOwnerOfOrganizationOwnedDevice(userHandle);
 
             mOwners.removeProfileOwner(userHandle);
@@ -3116,6 +3119,7 @@
             deleteTransferOwnershipBundleLocked(metadata.userId);
         }
         updateSystemUpdateFreezePeriodsRecord(/* saveIfChanged */ true);
+        pushUserControlDisabledPackagesLocked(metadata.userId);
     }
 
     private void maybeLogStart() {
@@ -3178,18 +3182,22 @@
         startOwnerService(userId, "start-user");
     }
 
-    // TODO(b/218639412): Once PM stores these on a per-user basis, push even empty lists to handle
-    // DO/PO removal correctly.
     void pushUserControlDisabledPackagesLocked(int userId) {
-        if (userId != mOwners.getDeviceOwnerUserId()) {
-            return;
+        final int targetUserId;
+        final ActiveAdmin owner;
+        if (getDeviceOwnerUserIdUncheckedLocked() == userId) {
+            owner = getDeviceOwnerAdminLocked();
+            targetUserId = UserHandle.USER_ALL;
+        } else {
+            owner = getProfileOwnerAdminLocked(userId);
+            targetUserId = userId;
         }
-        ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
-        if (deviceOwner == null || deviceOwner.protectedPackages == null) {
-            return;
-        }
-        mInjector.getPackageManagerInternal().setDeviceOwnerProtectedPackages(
-                deviceOwner.info.getPackageName(), deviceOwner.protectedPackages);
+
+        List<String> protectedPackages = (owner == null || owner.protectedPackages == null)
+                ? Collections.emptyList() : owner.protectedPackages;
+        mInjector.binderWithCleanCallingIdentity(() ->
+                mInjector.getPackageManagerInternal().setOwnerProtectedPackages(
+                        targetUserId, protectedPackages));
     }
 
     @Override
@@ -8755,6 +8763,18 @@
         }
     }
 
+    private void clearManagedProfileApnUnchecked() {
+        if (!mHasTelephonyFeature) {
+            return;
+        }
+        final List<ApnSetting> apns = getOverrideApnsUnchecked();
+        for (ApnSetting apn : apns) {
+            if (apn.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) {
+                removeOverrideApnUnchecked(apn.getId());
+            }
+        }
+    }
+
     private void clearDeviceOwnerLocked(ActiveAdmin admin, int userId) {
         mDeviceAdminServiceController.stopServiceForOwner(userId, "clear-device-owner");
 
@@ -12095,6 +12115,10 @@
         }
     }
 
+    private boolean isManagedProfileOwner(CallerIdentity caller) {
+        return isProfileOwner(caller) && isManagedProfile(caller.getUserId());
+    }
+
     private boolean isDefaultSupervisor(CallerIdentity caller) {
         final String supervisor = mContext.getResources().getString(
                 com.android.internal.R.string.config_defaultSupervisionProfileOwnerComponent);
@@ -16295,7 +16319,7 @@
         final CallerIdentity caller = getCallerIdentity(who);
         if (apnSetting.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) {
             Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller)
-                    || isProfileOwner(caller));
+                    || isManagedProfileOwner(caller));
         } else {
             Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller));
         }
@@ -16323,7 +16347,7 @@
         if (apn != null && apn.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE
                 && apnSetting.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) {
             Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller)
-                    || isProfileOwner(caller));
+                    || isManagedProfileOwner(caller));
         } else {
             Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller));
         }
@@ -16351,7 +16375,7 @@
         ApnSetting apn = getApnSetting(apnId);
         if (apn != null && apn.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) {
             Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller)
-                    || isProfileOwner(caller));
+                    || isManagedProfileOwner(caller));
         } else {
             Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller));
         }
@@ -16396,8 +16420,20 @@
         }
         Objects.requireNonNull(who, "ComponentName is null");
         final CallerIdentity caller = getCallerIdentity(who);
-        Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller));
-        return getOverrideApnsUnchecked();
+        Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller)
+                || isManagedProfileOwner(caller));
+        List<ApnSetting> apnSettings = getOverrideApnsUnchecked();
+        if (isProfileOwner(caller)) {
+            List<ApnSetting> apnSettingList = new ArrayList<>();
+            for (ApnSetting apnSetting : apnSettings) {
+                if (apnSetting.getApnTypeBitmask() == ApnSetting.TYPE_ENTERPRISE) {
+                    apnSettingList.add(apnSetting);
+                }
+            }
+            return apnSettingList;
+        } else {
+            return apnSettings;
+        }
     }
 
     private List<ApnSetting> getOverrideApnsUnchecked() {
@@ -16967,23 +17003,20 @@
         Objects.requireNonNull(who, "ComponentName is null");
         Objects.requireNonNull(packages, "packages is null");
         final CallerIdentity caller = getCallerIdentity(who);
-        Preconditions.checkCallAuthorization(
-                isDefaultDeviceOwner(caller) || isFinancedDeviceOwner(caller));
+        Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller) || isProfileOwner(caller)
+                || isFinancedDeviceOwner(caller));
         checkCanExecuteOrThrowUnsafe(
                 DevicePolicyManager.OPERATION_SET_USER_CONTROL_DISABLED_PACKAGES);
 
         synchronized (getLockObject()) {
-            ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
-            if (!Objects.equals(deviceOwner.protectedPackages, packages)) {
-                deviceOwner.protectedPackages = packages.isEmpty() ? null : packages;
+            ActiveAdmin owner = getDeviceOrProfileOwnerAdminLocked(caller.getUserId());
+            if (!Objects.equals(owner.protectedPackages, packages)) {
+                owner.protectedPackages = packages.isEmpty() ? null : packages;
                 saveSettingsLocked(caller.getUserId());
+                pushUserControlDisabledPackagesLocked(caller.getUserId());
             }
         }
 
-        mInjector.binderWithCleanCallingIdentity(
-                () -> mInjector.getPackageManagerInternal().setDeviceOwnerProtectedPackages(
-                        who.getPackageName(), packages));
-
         DevicePolicyEventLogger
                 .createEvent(DevicePolicyEnums.SET_USER_CONTROL_DISABLED_PACKAGES)
                 .setAdmin(who)
@@ -16996,11 +17029,11 @@
         Objects.requireNonNull(who, "ComponentName is null");
 
         final CallerIdentity caller = getCallerIdentity(who);
-        Preconditions.checkCallAuthorization(
-                isDefaultDeviceOwner(caller) || isFinancedDeviceOwner(caller));
+        Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller) || isProfileOwner(caller)
+                || isFinancedDeviceOwner(caller));
 
         synchronized (getLockObject()) {
-            ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
+            ActiveAdmin deviceOwner = getDeviceOrProfileOwnerAdminLocked(caller.getUserId());
             return deviceOwner.protectedPackages != null
                     ? deviceOwner.protectedPackages : Collections.emptyList();
         }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 80f0186..ef311c2 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1057,6 +1057,7 @@
         t.traceBegin("StartWatchdog");
         final Watchdog watchdog = Watchdog.getInstance();
         watchdog.start();
+        mDumper.addDumpable(watchdog);
         t.traceEnd();
 
         Slog.i(TAG, "Reading configuration...");
diff --git a/services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java
index fac37fd..01bafc1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/CircularQueueTest.java
@@ -43,11 +43,12 @@
         mQueue = new CircularQueue<>(LIMIT);
         mQueue.put(1, "A");
         mQueue.put(2, "B");
-        mQueue.put(3, "C");
+        String removedElement = mQueue.put(3, "C");
         assertNull(mQueue.getElement(1));
         assertEquals(mQueue.getElement(2), "B");
         assertEquals(mQueue.getElement(3), "C");
-
+        // Confirming that put is returning the deleted element
+        assertEquals(removedElement, "A");
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index 8461b39..17b4226 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -3220,8 +3220,7 @@
         when(mRoleManager.getRoleHolders(RoleManager.ROLE_SYSTEM_WELLBEING)).thenReturn(
                 Arrays.asList(package4));
 
-        mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, true);
-        mService.mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = false;
+        mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, false);
         mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[] {
                 package1,
                 package3,
@@ -3233,21 +3232,7 @@
         assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package3, uid3));
         assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package4, uid4));
 
-        mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, false);
-        mService.mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = true;
-        mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[] {
-                package1,
-                package3,
-        });
-
-        // Same as above, deny listed packages will be false.
-        assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package1, uid1));
-        assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package2, uid2));
-        assertFalse(mService.isScheduleExactAlarmAllowedByDefault(package3, uid3));
-        assertTrue(mService.isScheduleExactAlarmAllowedByDefault(package4, uid4));
-
         mockChangeEnabled(SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT, true);
-        mService.mConstants.SCHEDULE_EXACT_ALARM_DENIED_BY_DEFAULT = true;
         mService.mConstants.EXACT_ALARM_DENY_LIST = new ArraySet<>(new String[] {
                 package1,
                 package3,
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
index 009dae5..fa8d569 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
@@ -585,6 +585,7 @@
         DeviceConfigSession<Long> bgCurrentDrainInteractionGracePeriod = null;
         DeviceConfigSession<Float> bgCurrentDrainRestrictedBucketThreshold = null;
         DeviceConfigSession<Float> bgCurrentDrainBgRestrictedThreshold = null;
+        DeviceConfigSession<Boolean> bgCurrentDrainAutoRestrictAbusiveApps = null;
         DeviceConfigSession<Boolean> bgPromptFgsWithNotiToBgRestricted = null;
         DeviceConfigSession<Boolean> bgPromptAbusiveAppToBgRestricted = null;
         DeviceConfigSession<Long> bgNotificationMinInterval = null;
@@ -644,6 +645,14 @@
                             isLowRamDeviceStatic() ? 1 : 0]);
             bgCurrentDrainBgRestrictedThreshold.set(bgRestrictedThreshold);
 
+            bgCurrentDrainAutoRestrictAbusiveApps = new DeviceConfigSession<>(
+                    DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                    AppBatteryPolicy.KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED,
+                    DeviceConfig::getBoolean,
+                    mContext.getResources().getBoolean(
+                            R.bool.config_bg_current_drain_auto_restrict_abusive_apps));
+            bgCurrentDrainAutoRestrictAbusiveApps.set(true);
+
             bgPromptFgsWithNotiToBgRestricted = new DeviceConfigSession<>(
                     DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                     ConstantsObserver.KEY_BG_PROMPT_FGS_WITH_NOTIFICATION_TO_BG_RESTRICTED,
@@ -1099,6 +1108,7 @@
             closeIfNotNull(bgCurrentDrainInteractionGracePeriod);
             closeIfNotNull(bgCurrentDrainRestrictedBucketThreshold);
             closeIfNotNull(bgCurrentDrainBgRestrictedThreshold);
+            closeIfNotNull(bgCurrentDrainAutoRestrictAbusiveApps);
             closeIfNotNull(bgPromptFgsWithNotiToBgRestricted);
             closeIfNotNull(bgPromptAbusiveAppToBgRestricted);
             closeIfNotNull(bgNotificationMinInterval);
@@ -1651,6 +1661,7 @@
         DeviceConfigSession<Float> bgCurrentDrainBgRestrictedThreshold = null;
         DeviceConfigSession<Float> bgCurrentDrainRestrictedBucketHighThreshold = null;
         DeviceConfigSession<Float> bgCurrentDrainBgRestrictedHighThreshold = null;
+        DeviceConfigSession<Boolean> bgCurrentDrainAutoRestrictAbusiveApps = null;
         DeviceConfigSession<Long> bgMediaPlaybackMinDurationThreshold = null;
         DeviceConfigSession<Long> bgLocationMinDurationThreshold = null;
         DeviceConfigSession<Boolean> bgCurrentDrainEventDurationBasedThresholdEnabled = null;
@@ -1736,6 +1747,14 @@
                             isLowRamDeviceStatic() ? 1 : 0]);
             bgCurrentDrainBgRestrictedHighThreshold.set(bgRestrictedHighThreshold);
 
+            bgCurrentDrainAutoRestrictAbusiveApps = new DeviceConfigSession<>(
+                    DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                    AppBatteryPolicy.KEY_BG_CURRENT_DRAIN_AUTO_RESTRICT_ABUSIVE_APPS_ENABLED,
+                    DeviceConfig::getBoolean,
+                    mContext.getResources().getBoolean(
+                            R.bool.config_bg_current_drain_auto_restrict_abusive_apps));
+            bgCurrentDrainAutoRestrictAbusiveApps.set(true);
+
             bgMediaPlaybackMinDurationThreshold = new DeviceConfigSession<>(
                     DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
                     AppBatteryPolicy.KEY_BG_CURRENT_DRAIN_MEDIA_PLAYBACK_MIN_DURATION,
@@ -2226,6 +2245,7 @@
             closeIfNotNull(bgCurrentDrainBgRestrictedThreshold);
             closeIfNotNull(bgCurrentDrainRestrictedBucketHighThreshold);
             closeIfNotNull(bgCurrentDrainBgRestrictedHighThreshold);
+            closeIfNotNull(bgCurrentDrainAutoRestrictAbusiveApps);
             closeIfNotNull(bgMediaPlaybackMinDurationThreshold);
             closeIfNotNull(bgLocationMinDurationThreshold);
             closeIfNotNull(bgCurrentDrainEventDurationBasedThresholdEnabled);
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
index 8a954ca..5f9f1b2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
@@ -233,27 +233,34 @@
     @Test
     public void testConstantsUpdating_ValidValues() {
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 5 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 5 * MINUTE_IN_MILLIS);
 
         assertEquals(5 * HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs());
+        assertEquals(5 * MINUTE_IN_MILLIS, mPrefetchController.getLaunchTimeAllowanceMs());
     }
 
     @Test
     public void testConstantsUpdating_InvalidValues() {
         // Test negatives/too low.
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 4 * MINUTE_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, -MINUTE_IN_MILLIS);
 
         assertEquals(HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs());
+        assertEquals(0, mPrefetchController.getLaunchTimeAllowanceMs());
 
         // Test larger than a day. Controller should cap at one day.
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 25 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 5 * HOUR_IN_MILLIS);
 
         assertEquals(24 * HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs());
+        assertEquals(2 * HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeAllowanceMs());
     }
 
     @Test
     public void testConstantsUpdating_ThresholdChangesAlarms() {
         final long launchDelayMs = 11 * HOUR_IN_MILLIS;
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
         when(mUsageStatsManagerInternal
                 .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
                 .thenReturn(sSystemClock.millis() + launchDelayMs);
@@ -276,6 +283,7 @@
 
     @Test
     public void testConstraintNotSatisfiedWhenLaunchLate() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
 
         final JobStatus job = createJobStatus("testConstraintNotSatisfiedWhenLaunchLate", 1);
@@ -290,6 +298,8 @@
 
     @Test
     public void testConstraintSatisfiedWhenLaunchSoon() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
+
         final JobStatus job = createJobStatus("testConstraintSatisfiedWhenLaunchSoon", 2);
         when(mUsageStatsManagerInternal
                 .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
@@ -338,6 +348,8 @@
 
     @Test
     public void testConstraintSatisfiedWhenWidget() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
+
         final JobStatus jobNonWidget = createJobStatus("testConstraintSatisfiedWhenWidget", 1);
         final JobStatus jobWidget = createJobStatus("testConstraintSatisfiedWhenWidget", 2);
 
@@ -365,6 +377,7 @@
     @Test
     public void testEstimatedLaunchTimeChangedToLate() {
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
         when(mUsageStatsManagerInternal
                 .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
                 .thenReturn(sSystemClock.millis() + HOUR_IN_MILLIS);
@@ -393,6 +406,7 @@
     @Test
     public void testEstimatedLaunchTimeChangedToSoon() {
         setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 0);
         when(mUsageStatsManagerInternal
                 .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
                 .thenReturn(sSystemClock.millis() + 10 * HOUR_IN_MILLIS);
@@ -413,4 +427,36 @@
         verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)).onControllerStateChanged(any());
         assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
     }
+
+    @Test
+    public void testEstimatedLaunchTimeAllowance() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 7 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_ALLOWANCE_MS, 15 * MINUTE_IN_MILLIS);
+        when(mUsageStatsManagerInternal
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID))
+                .thenReturn(sSystemClock.millis() + 10 * HOUR_IN_MILLIS);
+
+        InOrder inOrder = inOrder(mUsageStatsManagerInternal);
+
+        JobStatus jobStatus = createJobStatus("testEstimatedLaunchTimeAllowance", 1);
+        trackJobs(jobStatus);
+        inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS))
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        // The allowance shouldn't shift the alarm
+        verify(mAlarmManager, timeout(DEFAULT_WAIT_MS).times(1))
+                .setWindow(
+                        anyInt(), eq(sElapsedRealtimeClock.millis() + 3 * HOUR_IN_MILLIS),
+                        anyLong(), eq(TAG_PREFETCH), any(), any());
+        assertFalse(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+
+        mEstimatedLaunchTimeChangedListener.onEstimatedLaunchTimeChanged(SOURCE_USER_ID,
+                SOURCE_PACKAGE, sSystemClock.millis() + HOUR_IN_MILLIS);
+
+        inOrder.verify(mUsageStatsManagerInternal, timeout(DEFAULT_WAIT_MS).times(0))
+                .getEstimatedPackageLaunchTime(SOURCE_PACKAGE, SOURCE_USER_ID);
+        verify(mJobSchedulerService, timeout(DEFAULT_WAIT_MS)).onControllerStateChanged(any());
+        assertTrue(jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_PREFETCH));
+
+        sSystemClock = getShiftedClock(sSystemClock, HOUR_IN_MILLIS + MINUTE_IN_MILLIS);
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 43ba39a..29a3eb1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -90,7 +90,9 @@
 import com.android.server.job.JobStore;
 import com.android.server.job.controllers.QuotaController.ExecutionStats;
 import com.android.server.job.controllers.QuotaController.QcConstants;
+import com.android.server.job.controllers.QuotaController.QuotaBump;
 import com.android.server.job.controllers.QuotaController.ShrinkableDebits;
+import com.android.server.job.controllers.QuotaController.TimedEvent;
 import com.android.server.job.controllers.QuotaController.TimingSession;
 import com.android.server.usage.AppStandbyInternal;
 
@@ -128,6 +130,7 @@
     private QuotaController.QcConstants mQcConstants;
     private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants();
     private int mSourceUid;
+    private AppStandbyInternal.AppIdleStateChangeListener mAppIdleStateChangeListener;
     private PowerAllowlistInternal.TempAllowlistChangeListener mTempAllowlistListener;
     private IUidObserver mUidObserver;
     private UsageStatsManagerInternal.UsageEventListener mUsageEventListener;
@@ -178,7 +181,8 @@
         when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager);
         doReturn(mActivityMangerInternal)
                 .when(() -> LocalServices.getService(ActivityManagerInternal.class));
-        doReturn(mock(AppStandbyInternal.class))
+        final AppStandbyInternal appStandbyInternal = mock(AppStandbyInternal.class);
+        doReturn(appStandbyInternal)
                 .when(() -> LocalServices.getService(AppStandbyInternal.class));
         doReturn(mock(BatteryManagerInternal.class))
                 .when(() -> LocalServices.getService(BatteryManagerInternal.class));
@@ -222,6 +226,8 @@
 
         // Initialize real objects.
         // Capture the listeners.
+        ArgumentCaptor<AppStandbyInternal.AppIdleStateChangeListener> aiscListenerCaptor =
+                ArgumentCaptor.forClass(AppStandbyInternal.AppIdleStateChangeListener.class);
         ArgumentCaptor<IUidObserver> uidObserverCaptor =
                 ArgumentCaptor.forClass(IUidObserver.class);
         ArgumentCaptor<PowerAllowlistInternal.TempAllowlistChangeListener> taChangeCaptor =
@@ -231,6 +237,8 @@
         mQuotaController = new QuotaController(mJobSchedulerService,
                 mock(BackgroundJobsController.class), mock(ConnectivityController.class));
 
+        verify(appStandbyInternal).addListener(aiscListenerCaptor.capture());
+        mAppIdleStateChangeListener = aiscListenerCaptor.getValue();
         verify(mPowerAllowlistInternal)
                 .registerTempAllowlistChangeListener(taChangeCaptor.capture());
         mTempAllowlistListener = taChangeCaptor.getValue();
@@ -457,26 +465,31 @@
                 now - 10 * MINUTE_IN_MILLIS, 9 * MINUTE_IN_MILLIS, 3);
         TimingSession two = createTimingSession(
                 now - (70 * MINUTE_IN_MILLIS), 9 * MINUTE_IN_MILLIS, 1);
+        QuotaBump bump1 = new QuotaBump(now - 2 * HOUR_IN_MILLIS);
         TimingSession thr = createTimingSession(
                 now - (3 * HOUR_IN_MILLIS + 10 * MINUTE_IN_MILLIS), 9 * MINUTE_IN_MILLIS, 1);
         // Overlaps 24 hour boundary.
         TimingSession fou = createTimingSession(
                 now - (24 * HOUR_IN_MILLIS + 2 * MINUTE_IN_MILLIS), 7 * MINUTE_IN_MILLIS, 1);
         // Way past the 24 hour boundary.
+        QuotaBump bump2 = new QuotaBump(now - 24 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS);
         TimingSession fiv = createTimingSession(
                 now - (25 * HOUR_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 4);
-        List<TimingSession> expectedRegular = new ArrayList<>();
-        List<TimingSession> expectedEJ = new ArrayList<>();
+        List<TimedEvent> expectedRegular = new ArrayList<>();
+        List<TimedEvent> expectedEJ = new ArrayList<>();
         // Added in correct (chronological) order.
         expectedRegular.add(fou);
         expectedRegular.add(thr);
+        expectedRegular.add(bump1);
         expectedRegular.add(two);
         expectedRegular.add(one);
         expectedEJ.add(fou);
         expectedEJ.add(one);
         mQuotaController.saveTimingSession(0, "com.android.test", fiv, false);
+        mQuotaController.getTimingSessions(0, "com.android.test").add(bump2);
         mQuotaController.saveTimingSession(0, "com.android.test", fou, false);
         mQuotaController.saveTimingSession(0, "com.android.test", thr, false);
+        mQuotaController.getTimingSessions(0, "com.android.test").add(bump1);
         mQuotaController.saveTimingSession(0, "com.android.test", two, false);
         mQuotaController.saveTimingSession(0, "com.android.test", one, false);
         mQuotaController.saveTimingSession(0, "com.android.test", fiv, true);
@@ -1773,6 +1786,129 @@
         }
     }
 
+    /**
+     * Test getTimeUntilQuotaConsumedLocked when the determination is based within the bucket
+     * window and there are valid QuotaBumps in the history.
+     */
+    @Test
+    public void testGetTimeUntilQuotaConsumedLocked_QuotaBump() {
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, MINUTE_IN_MILLIS);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 8 * HOUR_IN_MILLIS);
+
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        // Close to RARE boundary.
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (24 * HOUR_IN_MILLIS - 30 * SECOND_IN_MILLIS),
+                        30 * SECOND_IN_MILLIS, 5), false);
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)
+                .add(new QuotaBump(now - 16 * HOUR_IN_MILLIS));
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)
+                .add(new QuotaBump(now - 12 * HOUR_IN_MILLIS));
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)
+                .add(new QuotaBump(now - 8 * HOUR_IN_MILLIS));
+        // Far away from FREQUENT boundary.
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (7 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
+        // Overlap WORKING_SET boundary.
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)
+                .add(new QuotaBump(now - 2 * HOUR_IN_MILLIS));
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS),
+                        3 * MINUTE_IN_MILLIS, 5), false);
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)
+                .add(new QuotaBump(now - 15 * MINUTE_IN_MILLIS));
+        // Close to ACTIVE boundary.
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (9 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false);
+
+        setStandbyBucket(RARE_INDEX);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(3 * MINUTE_IN_MILLIS + 30 * SECOND_IN_MILLIS,
+                    mQuotaController.getRemainingExecutionTimeLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+            assertEquals(4 * MINUTE_IN_MILLIS,
+                    mQuotaController.getTimeUntilQuotaConsumedLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+        }
+
+        setStandbyBucket(FREQUENT_INDEX);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(4 * MINUTE_IN_MILLIS,
+                    mQuotaController.getRemainingExecutionTimeLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+            assertEquals(4 * MINUTE_IN_MILLIS,
+                    mQuotaController.getTimeUntilQuotaConsumedLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+        }
+
+        setStandbyBucket(WORKING_INDEX);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(8 * MINUTE_IN_MILLIS,
+                    mQuotaController.getRemainingExecutionTimeLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+            assertEquals(10 * MINUTE_IN_MILLIS,
+                    mQuotaController.getTimeUntilQuotaConsumedLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+        }
+
+        // ACTIVE window = allowed time, so jobs can essentially run non-stop until they reach the
+        // max execution time.
+        setStandbyBucket(ACTIVE_INDEX);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(10 * MINUTE_IN_MILLIS,
+                    mQuotaController.getRemainingExecutionTimeLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+            assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 9 * MINUTE_IN_MILLIS,
+                    mQuotaController.getTimeUntilQuotaConsumedLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+        }
+    }
+
+    /**
+     * Test getTimeUntilQuotaConsumedLocked when there are valid QuotaBumps in recent history that
+     * provide enough additional quota to bridge gaps between sessions.
+     */
+    @Test
+    public void testGetTimeUntilQuotaConsumedLocked_QuotaBump_CrucialBumps() {
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, MINUTE_IN_MILLIS);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 8 * HOUR_IN_MILLIS);
+
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (25 * HOUR_IN_MILLIS),
+                        30 * MINUTE_IN_MILLIS, 25), false);
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)
+                .add(new QuotaBump(now - 16 * HOUR_IN_MILLIS));
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)
+                .add(new QuotaBump(now - 12 * HOUR_IN_MILLIS));
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)
+                .add(new QuotaBump(now - 8 * HOUR_IN_MILLIS));
+        // Without the valid quota bumps, the app would only 3 minutes until the quota was consumed.
+        // The quota bumps provide enough quota to bridge the gap between the two earliest sessions.
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (8 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 1), false);
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (8 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS),
+                        2 * MINUTE_IN_MILLIS, 5), false);
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS),
+                        3 * MINUTE_IN_MILLIS, 1), false);
+        mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE)
+                .add(new QuotaBump(now - 15 * MINUTE_IN_MILLIS));
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (9 * MINUTE_IN_MILLIS), 2 * MINUTE_IN_MILLIS, 1), false);
+
+        setStandbyBucket(FREQUENT_INDEX);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(2 * MINUTE_IN_MILLIS,
+                    mQuotaController.getRemainingExecutionTimeLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+            assertEquals(7 * MINUTE_IN_MILLIS,
+                    mQuotaController.getTimeUntilQuotaConsumedLocked(
+                            SOURCE_USER_ID, SOURCE_PACKAGE));
+        }
+    }
+
     @Test
     public void testIsWithinQuotaLocked_NeverApp() {
         synchronized (mQuotaController.mLock) {
@@ -2027,6 +2163,271 @@
     }
 
     @Test
+    public void testIsWithinQuotaLocked_WithQuotaBump_Duration() {
+        setDischarging();
+        int standbyBucket = WORKING_INDEX;
+        setStandbyBucket(standbyBucket);
+        setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS,
+                5 * MINUTE_IN_MILLIS);
+        setDeviceConfigInt(QcConstants.KEY_MAX_JOB_COUNT_WORKING, 10);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, MINUTE_IN_MILLIS);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, 0);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, 0);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 8 * HOUR_IN_MILLIS);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_LIMIT, 5);
+
+        long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(
+                        now - (HOUR_IN_MILLIS - 2 * MINUTE_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 1),
+                false);
+        final ExecutionStats stats;
+        synchronized (mQuotaController.mLock) {
+            stats = mQuotaController.getExecutionStatsLocked(
+                    SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+            mQuotaController.incrementJobCountLocked(SOURCE_USER_ID, SOURCE_PACKAGE, 1);
+            assertFalse(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(5 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs);
+        }
+        mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID);
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(6 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs);
+        }
+
+        advanceElapsedClock(HOUR_IN_MILLIS);
+
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(6 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs);
+        }
+
+        // Emulate a quota bump while some jobs are executing
+        JobStatus job1 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_Duration", 1);
+        JobStatus job2 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_Duration", 2);
+
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStartTrackingJobLocked(job1, null);
+            mQuotaController.prepareForExecutionLocked(job1);
+        }
+
+        advanceElapsedClock(MINUTE_IN_MILLIS);
+        mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID);
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(7 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs);
+            mQuotaController.maybeStartTrackingJobLocked(job2, null);
+            mQuotaController.prepareForExecutionLocked(job2);
+        }
+
+        advanceElapsedClock(MINUTE_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStopTrackingJobLocked(job1, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job2, null, false);
+            assertFalse(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(7 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs);
+        }
+
+        // Phase out the first session
+        advanceElapsedClock(5 * MINUTE_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(7 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs);
+        }
+
+        // Phase out the first quota bump
+        advanceElapsedClock(7 * HOUR_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(6 * MINUTE_IN_MILLIS, stats.allowedTimePerPeriodMs);
+        }
+    }
+
+    @Test
+    public void testIsWithinQuotaLocked_WithQuotaBump_JobCount() {
+        setDischarging();
+        int standbyBucket = WORKING_INDEX;
+        setStandbyBucket(standbyBucket);
+        setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS,
+                20 * MINUTE_IN_MILLIS);
+        setDeviceConfigInt(QcConstants.KEY_MAX_JOB_COUNT_WORKING, 10);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, 0);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, 1);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, 0);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 8 * HOUR_IN_MILLIS);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_LIMIT, 5);
+
+        long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (HOUR_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 10), false);
+        final ExecutionStats stats;
+        synchronized (mQuotaController.mLock) {
+            stats = mQuotaController.getExecutionStatsLocked(
+                    SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+            mQuotaController.incrementJobCountLocked(SOURCE_USER_ID, SOURCE_PACKAGE, 10);
+            assertFalse(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(10, stats.jobCountLimit);
+        }
+        mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID);
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(11, stats.jobCountLimit);
+        }
+
+        advanceElapsedClock(HOUR_IN_MILLIS);
+
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(11, stats.jobCountLimit);
+        }
+
+        // Emulate a quota bump while some jobs are executing
+        JobStatus job1 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_JobCount", 1);
+        JobStatus job2 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_JobCount", 2);
+
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStartTrackingJobLocked(job1, null);
+            mQuotaController.prepareForExecutionLocked(job1);
+        }
+
+        advanceElapsedClock(MINUTE_IN_MILLIS);
+        mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID);
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(12, stats.jobCountLimit);
+            mQuotaController.maybeStartTrackingJobLocked(job2, null);
+            mQuotaController.prepareForExecutionLocked(job2);
+        }
+
+        advanceElapsedClock(MINUTE_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStopTrackingJobLocked(job1, null, false);
+            mQuotaController.maybeStopTrackingJobLocked(job2, null, false);
+            assertFalse(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(12, stats.jobCountLimit);
+        }
+
+        // Phase out the first session
+        advanceElapsedClock(3 * MINUTE_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(12, stats.jobCountLimit);
+        }
+
+        // Phase out the first quota bump
+        advanceElapsedClock(7 * HOUR_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(11, stats.jobCountLimit);
+        }
+    }
+
+    @Test
+    public void testIsWithinQuotaLocked_WithQuotaBump_SessionCount() {
+        setDischarging();
+        int standbyBucket = WORKING_INDEX;
+        setStandbyBucket(standbyBucket);
+        setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_WORKING_MS,
+                20 * MINUTE_IN_MILLIS);
+        setDeviceConfigInt(QcConstants.KEY_MAX_SESSION_COUNT_WORKING, 2);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, 0);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, 0);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, 1);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 8 * HOUR_IN_MILLIS);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_LIMIT, 5);
+
+        long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (HOUR_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 1), false);
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (30 * MINUTE_IN_MILLIS), MINUTE_IN_MILLIS, 1), false);
+        final ExecutionStats stats;
+        synchronized (mQuotaController.mLock) {
+            stats = mQuotaController.getExecutionStatsLocked(
+                    SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
+            assertFalse(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(2, stats.sessionCountLimit);
+        }
+        mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID);
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(3, stats.sessionCountLimit);
+        }
+
+        advanceElapsedClock(HOUR_IN_MILLIS);
+
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(3, stats.sessionCountLimit);
+        }
+
+        // Emulate a quota bump while some jobs are executing
+        JobStatus job1 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_JobCount", 1);
+        JobStatus job2 = createJobStatus("testIsWithinQuotaLocked_WithQuotaBump_JobCount", 2);
+
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStartTrackingJobLocked(job1, null);
+            mQuotaController.prepareForExecutionLocked(job1);
+        }
+
+        advanceElapsedClock(MINUTE_IN_MILLIS);
+        mAppIdleStateChangeListener.triggerTemporaryQuotaBump(SOURCE_PACKAGE, SOURCE_USER_ID);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStopTrackingJobLocked(job1, null, false);
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(4, stats.sessionCountLimit);
+        }
+
+        advanceElapsedClock(MINUTE_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStartTrackingJobLocked(job2, null);
+            mQuotaController.prepareForExecutionLocked(job2);
+        }
+
+        advanceElapsedClock(MINUTE_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStopTrackingJobLocked(job2, null, false);
+            assertFalse(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(4, stats.sessionCountLimit);
+        }
+
+        // Phase out the first session
+        advanceElapsedClock(2 * MINUTE_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(4, stats.sessionCountLimit);
+        }
+
+        // Phase out the first quota bump
+        advanceElapsedClock(7 * HOUR_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            assertTrue(mQuotaController
+                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
+            assertEquals(3, stats.sessionCountLimit);
+        }
+    }
+
+    @Test
     public void testIsWithinEJQuotaLocked_NeverApp() {
         JobStatus js = createExpeditedJobStatus("testIsWithinEJQuotaLocked_NeverApp", 1);
         setStandbyBucket(NEVER_INDEX, js);
@@ -3012,6 +3413,12 @@
         setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS,
                 84 * SECOND_IN_MILLIS);
         setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TOP_APP_MS, 83 * SECOND_IN_MILLIS);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS,
+                93 * SECOND_IN_MILLIS);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, 92);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, 91);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 90 * MINUTE_IN_MILLIS);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_LIMIT, 89);
 
         assertEquals(8 * MINUTE_IN_MILLIS,
                 mQuotaController.getAllowedTimePerPeriodMs()[EXEMPTED_INDEX]);
@@ -3069,6 +3476,11 @@
         assertEquals(85 * SECOND_IN_MILLIS, mQuotaController.getEJRewardNotificationSeenMs());
         assertEquals(84 * SECOND_IN_MILLIS, mQuotaController.getEJGracePeriodTempAllowlistMs());
         assertEquals(83 * SECOND_IN_MILLIS, mQuotaController.getEJGracePeriodTopAppMs());
+        assertEquals(93 * SECOND_IN_MILLIS, mQuotaController.getQuotaBumpAdditionDurationMs());
+        assertEquals(92, mQuotaController.getQuotaBumpAdditionJobCount());
+        assertEquals(91, mQuotaController.getQuotaBumpAdditionSessionCount());
+        assertEquals(90 * MINUTE_IN_MILLIS, mQuotaController.getQuotaBumpWindowSizeMs());
+        assertEquals(89, mQuotaController.getQuotaBumpLimit());
     }
 
     @Test
@@ -3121,6 +3533,11 @@
         setDeviceConfigLong(QcConstants.KEY_EJ_REWARD_NOTIFICATION_SEEN_MS, -1);
         setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS, -1);
         setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TOP_APP_MS, -1);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_DURATION_MS, -1);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_JOB_COUNT, -1);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_ADDITIONAL_SESSION_COUNT, -1);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 59 * MINUTE_IN_MILLIS);
+        setDeviceConfigInt(QcConstants.KEY_QUOTA_BUMP_LIMIT, -1);
 
         assertEquals(MINUTE_IN_MILLIS,
                 mQuotaController.getAllowedTimePerPeriodMs()[EXEMPTED_INDEX]);
@@ -3171,6 +3588,11 @@
         assertEquals(0, mQuotaController.getEJRewardNotificationSeenMs());
         assertEquals(0, mQuotaController.getEJGracePeriodTempAllowlistMs());
         assertEquals(0, mQuotaController.getEJGracePeriodTopAppMs());
+        assertEquals(0, mQuotaController.getQuotaBumpAdditionDurationMs());
+        assertEquals(0, mQuotaController.getQuotaBumpAdditionJobCount());
+        assertEquals(0, mQuotaController.getQuotaBumpAdditionSessionCount());
+        assertEquals(60 * MINUTE_IN_MILLIS, mQuotaController.getQuotaBumpWindowSizeMs());
+        assertEquals(0, mQuotaController.getQuotaBumpLimit());
 
         // Invalid configurations.
         // In_QUOTA_BUFFER should never be greater than ALLOWED_TIME_PER_PERIOD
@@ -3228,6 +3650,7 @@
         setDeviceConfigLong(QcConstants.KEY_EJ_REWARD_NOTIFICATION_SEEN_MS, 25 * HOUR_IN_MILLIS);
         setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS, 25 * HOUR_IN_MILLIS);
         setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TOP_APP_MS, 25 * HOUR_IN_MILLIS);
+        setDeviceConfigLong(QcConstants.KEY_QUOTA_BUMP_WINDOW_SIZE_MS, 25 * HOUR_IN_MILLIS);
 
         assertEquals(24 * HOUR_IN_MILLIS,
                 mQuotaController.getAllowedTimePerPeriodMs()[EXEMPTED_INDEX]);
@@ -3268,6 +3691,7 @@
         assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getEJRewardNotificationSeenMs());
         assertEquals(HOUR_IN_MILLIS, mQuotaController.getEJGracePeriodTempAllowlistMs());
         assertEquals(HOUR_IN_MILLIS, mQuotaController.getEJGracePeriodTopAppMs());
+        assertEquals(24 * HOUR_IN_MILLIS, mQuotaController.getQuotaBumpWindowSizeMs());
     }
 
     /** Tests that TimingSessions aren't saved when the device is charging. */
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java
index 444db91..da5c8f0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java
@@ -16,6 +16,9 @@
 
 package com.android.server.pm;
 
+import static com.android.server.pm.BackgroundDexOptService.STATUS_DEX_OPT_FAILED;
+import static com.android.server.pm.BackgroundDexOptService.STATUS_OK;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -23,12 +26,14 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertThrows;
 
+import android.annotation.Nullable;
 import android.app.job.JobInfo;
 import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
@@ -38,10 +43,13 @@
 import android.content.IntentFilter;
 import android.os.HandlerThread;
 import android.os.PowerManager;
+import android.util.Log;
 
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.LocalServices;
 import com.android.server.PinnerService;
 import com.android.server.pm.dex.DexManager;
+import com.android.server.pm.dex.DexoptOptions;
 
 import org.junit.After;
 import org.junit.Before;
@@ -52,7 +60,11 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.stream.Collectors;
@@ -66,8 +78,12 @@
 
     private static final long TEST_WAIT_TIMEOUT_MS = 10_000;
 
-    private static final List<String> DEFAULT_PACKAGE_LIST = List.of("aaa", "bbb");
-    private static final List<String> EMPTY_PACKAGE_LIST = List.of();
+    private static final String PACKAGE_AAA = "aaa";
+    private static final List<String> DEFAULT_PACKAGE_LIST = List.of(PACKAGE_AAA, "bbb");
+    private int mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED;
+
+    // Store expected dexopt sequence for verification.
+    private ArrayList<DexOptInfo> mDexInfoSequence = new ArrayList<>();
 
     @Mock
     private Context mContext;
@@ -116,14 +132,23 @@
         when(mInjector.getDexOptThermalCutoff()).thenReturn(PowerManager.THERMAL_STATUS_CRITICAL);
         when(mInjector.getCurrentThermalStatus()).thenReturn(PowerManager.THERMAL_STATUS_NONE);
         when(mInjector.supportSecondaryDex()).thenReturn(true);
-        when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(DEFAULT_PACKAGE_LIST);
-        when(mDexOptHelper.performDexOptWithStatus(any())).thenReturn(
-                PackageDexOptimizer.DEX_OPT_PERFORMED);
-        when(mDexOptHelper.performDexOpt(any())).thenReturn(true);
+        setupDexOptHelper();
 
         mService = new BackgroundDexOptService(mInjector);
     }
 
+    private void setupDexOptHelper() {
+        when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(DEFAULT_PACKAGE_LIST);
+        when(mDexOptHelper.performDexOptWithStatus(any())).thenAnswer(inv -> {
+            DexoptOptions opt = inv.getArgument(0);
+            if (opt.getPackageName().equals(PACKAGE_AAA)) {
+                return mDexOptResultForPackageAAA;
+            }
+            return PackageDexOptimizer.DEX_OPT_PERFORMED;
+        });
+        when(mDexOptHelper.performDexOpt(any())).thenReturn(true);
+    }
+
     @After
     public void tearDown() throws Exception {
         LocalServices.removeServiceForTest(BackgroundDexOptService.class);
@@ -159,7 +184,7 @@
     @Test
     public void testNoExecutionForNoOptimizablePackages() {
         initUntilBootCompleted();
-        when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(EMPTY_PACKAGE_LIST);
+        when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(Collections.emptyList());
 
         assertThat(mService.onStartJob(mJobServiceForPostBoot,
                 mJobParametersForPostBoot)).isFalse();
@@ -170,15 +195,70 @@
     public void testPostBootUpdateFullRun() {
         initUntilBootCompleted();
 
-        runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, false, 1);
+        runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
+                /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
+                /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
+    }
+
+    @Test
+    public void testPostBootUpdateFullRunWithPackageFailure() {
+        mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED;
+
+        initUntilBootCompleted();
+
+        runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
+                /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED,
+                /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA);
+
+        assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA);
+        assertThat(getFailedPackageNamesSecondary()).isEmpty();
     }
 
     @Test
     public void testIdleJobFullRun() {
         initUntilBootCompleted();
+        runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
+                /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
+                /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
+        runFullJob(mJobServiceForIdle, mJobParametersForIdle,
+                /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK,
+                /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
+    }
 
-        runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, false, 1);
-        runFullJob(mJobServiceForIdle, mJobParametersForIdle, true, 2);
+    @Test
+    public void testIdleJobFullRunWithFailureOnceAndSuccessAfterUpdate() {
+        mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED;
+
+        initUntilBootCompleted();
+
+        runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
+                /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED,
+                /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA);
+
+        assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA);
+        assertThat(getFailedPackageNamesSecondary()).isEmpty();
+
+        runFullJob(mJobServiceForIdle, mJobParametersForIdle,
+                /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK,
+                /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA);
+
+        assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA);
+        assertThat(getFailedPackageNamesSecondary()).isEmpty();
+
+        mService.notifyPackageChanged(PACKAGE_AAA);
+
+        assertThat(getFailedPackageNamesPrimary()).isEmpty();
+        assertThat(getFailedPackageNamesSecondary()).isEmpty();
+
+        // Succeed this time.
+        mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED;
+
+        runFullJob(mJobServiceForIdle, mJobParametersForIdle,
+                /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK,
+                /* totalJobFinishedWithParams= */ 2, /* expectedSkippedPackage= */ null);
+
+        assertThat(getFailedPackageNamesPrimary()).isEmpty();
+        assertThat(getFailedPackageNamesSecondary()).isEmpty();
     }
 
     @Test
@@ -404,8 +484,10 @@
     }
 
     private void runFullJob(BackgroundDexOptJobService jobService, JobParameters params,
-            boolean expectedReschedule, int totalJobRuns) {
+            boolean expectedReschedule, int expectedStatus, int totalJobFinishedWithParams,
+            @Nullable String expectedSkippedPackage) {
         when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread());
+        addFullRunSequence(expectedSkippedPackage);
         assertThat(mService.onStartJob(jobService, params)).isTrue();
 
         ArgumentCaptor<Runnable> argThreadRunnable = ArgumentCaptor.forClass(Runnable.class);
@@ -413,20 +495,99 @@
 
         argThreadRunnable.getValue().run();
 
-        verify(jobService).jobFinished(params, expectedReschedule);
+        verify(jobService, times(totalJobFinishedWithParams)).jobFinished(params,
+                expectedReschedule);
         // Never block
         verify(mDexOptHelper, never()).controlDexOptBlocking(true);
-        verifyPerformDexOpt(DEFAULT_PACKAGE_LIST, totalJobRuns);
+        verifyPerformDexOpt();
+        assertThat(getLastExecutionStatus()).isEqualTo(expectedStatus);
     }
 
-    private void verifyPerformDexOpt(List<String> pkgs, int expectedRuns) {
+    private void verifyPerformDexOpt() {
         InOrder inOrder = inOrder(mDexOptHelper);
-        for (int i = 0; i < expectedRuns; i++) {
-            for (String pkg : pkgs) {
-                inOrder.verify(mDexOptHelper, times(1)).performDexOptWithStatus(argThat((option) ->
-                        option.getPackageName().equals(pkg) && !option.isDexoptOnlySecondaryDex()));
-                inOrder.verify(mDexOptHelper, times(1)).performDexOpt(argThat((option) ->
-                        option.getPackageName().equals(pkg) && option.isDexoptOnlySecondaryDex()));
+        inOrder.verify(mDexOptHelper).getOptimizablePackages(any());
+        for (DexOptInfo info : mDexInfoSequence) {
+            if (info.isPrimary) {
+                verify(mDexOptHelper).performDexOptWithStatus(
+                        argThat((option) -> option.getPackageName().equals(info.packageName)
+                                && !option.isDexoptOnlySecondaryDex()));
+            } else {
+                inOrder.verify(mDexOptHelper).performDexOpt(
+                        argThat((option) -> option.getPackageName().equals(info.packageName)
+                                && option.isDexoptOnlySecondaryDex()));
+            }
+        }
+
+        // Even InOrder cannot check the order if the same call is made multiple times.
+        // To check the order across multiple runs, we reset the mock so that order can be checked
+        // in each call.
+        mDexInfoSequence.clear();
+        reset(mDexOptHelper);
+        setupDexOptHelper();
+    }
+
+    private String findDumpValueForKey(String key) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        PrintWriter pw = new PrintWriter(out, true);
+        IndentingPrintWriter writer = new IndentingPrintWriter(pw, "");
+        try {
+            mService.dump(writer);
+            writer.flush();
+            Log.i(TAG, "dump output:" + out.toString());
+            for (String line : out.toString().split(System.lineSeparator())) {
+                String[] vals = line.split(":");
+                if (vals[0].equals(key)) {
+                    if (vals.length == 2) {
+                        return vals[1].strip();
+                    } else {
+                        break;
+                    }
+                }
+            }
+            return "";
+        } finally {
+            writer.close();
+        }
+    }
+
+    List<String> findStringListFromDump(String key) {
+        String values = findDumpValueForKey(key);
+        if (values.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return Arrays.asList(values.split(","));
+    }
+
+    private List<String> getFailedPackageNamesPrimary() {
+        return findStringListFromDump("mFailedPackageNamesPrimary");
+    }
+
+    private List<String> getFailedPackageNamesSecondary() {
+        return findStringListFromDump("mFailedPackageNamesSecondary");
+    }
+
+    private int getLastExecutionStatus() {
+        return Integer.parseInt(findDumpValueForKey("mLastExecutionStatus"));
+    }
+
+    private static class DexOptInfo {
+        public final String packageName;
+        public final boolean isPrimary;
+
+        private DexOptInfo(String packageName, boolean isPrimary) {
+            this.packageName = packageName;
+            this.isPrimary = isPrimary;
+        }
+    }
+
+    private void addFullRunSequence(@Nullable String expectedSkippedPackage) {
+        for (String packageName : DEFAULT_PACKAGE_LIST) {
+            if (packageName.equals(expectedSkippedPackage)) {
+                // only fails primary dexopt in mocking but add secodary
+                mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false));
+            } else {
+                mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ true));
+                mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false));
             }
         }
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
index c9598bd..e30f3d2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/DeletePackageHelperTest.kt
@@ -69,13 +69,34 @@
     }
 
     @Test
-    fun deleteSystemPackageFailsIfNotAdmin() {
+    fun deleteSystemPackageFailsIfNotAdminAndNotProfile() {
         val ps = mPms.mSettings.getPackageLPr("a.data.package")
         whenever(PackageManagerServiceUtils.isSystemApp(ps)).thenReturn(true)
         whenever(mUserManagerInternal.getUserInfo(1)).thenReturn(UserInfo(1, "test", 0))
+        whenever(mUserManagerInternal.getProfileParentId(1)).thenReturn(1)
 
         val dph = DeletePackageHelper(mPms)
-        val result = dph.deletePackageX("a.data.package", 1L, 1, 0, false)
+        val result = dph.deletePackageX("a.data.package", 1L, 1,
+            PackageManager.DELETE_SYSTEM_APP, false)
+
+        assertThat(result).isEqualTo(PackageManager.DELETE_FAILED_USER_RESTRICTED)
+    }
+
+    @Test
+    fun deleteSystemPackageFailsIfProfileOfNonAdmin() {
+        val userId = 1
+        val parentId = 5
+        val ps = mPms.mSettings.getPackageLPr("a.data.package")
+        whenever(PackageManagerServiceUtils.isSystemApp(ps)).thenReturn(true)
+        whenever(mUserManagerInternal.getUserInfo(userId)).thenReturn(
+            UserInfo(userId, "test", UserInfo.FLAG_PROFILE))
+        whenever(mUserManagerInternal.getProfileParentId(userId)).thenReturn(parentId)
+        whenever(mUserManagerInternal.getUserInfo(parentId)).thenReturn(
+            UserInfo(userId, "testparent", 0))
+
+        val dph = DeletePackageHelper(mPms)
+        val result = dph.deletePackageX("a.data.package", 1L, userId,
+            PackageManager.DELETE_SYSTEM_APP, false)
 
         assertThat(result).isEqualTo(PackageManager.DELETE_FAILED_USER_RESTRICTED)
     }
@@ -93,4 +114,23 @@
 
         assertThat(result).isEqualTo(PackageManager.DELETE_SUCCEEDED)
     }
+
+    @Test
+    fun deleteSystemPackageSucceedsIfProfileOfAdmin() {
+        val userId = 1
+        val parentId = 5
+        val ps = mPms.mSettings.getPackageLPr("a.data.package")
+        whenever(PackageManagerServiceUtils.isSystemApp(ps)).thenReturn(true)
+        whenever(mUserManagerInternal.getUserInfo(userId)).thenReturn(
+            UserInfo(userId, "test", UserInfo.FLAG_PROFILE))
+        whenever(mUserManagerInternal.getProfileParentId(userId)).thenReturn(parentId)
+        whenever(mUserManagerInternal.getUserInfo(parentId)).thenReturn(
+            UserInfo(userId, "testparent", UserInfo.FLAG_ADMIN))
+
+        val dph = DeletePackageHelper(mPms)
+        val result = dph.deletePackageX("a.data.package", 1L, userId,
+            PackageManager.DELETE_SYSTEM_APP, false)
+
+        assertThat(result).isEqualTo(PackageManager.DELETE_SUCCEEDED)
+    }
 }
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java
index fa3e05a..20cfd59 100644
--- a/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/CameraPrivacyLightControllerTest.java
@@ -24,20 +24,33 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 
 import android.app.AppOpsManager;
 import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
 import android.hardware.lights.Light;
+import android.hardware.lights.LightState;
 import android.hardware.lights.LightsManager;
 import android.hardware.lights.LightsRequest;
+import android.os.Handler;
+import android.os.Looper;
 import android.permission.PermissionManager;
 import android.util.ArraySet;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.R;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
@@ -53,26 +66,43 @@
 
 public class CameraPrivacyLightControllerTest {
 
+    private int mDayColor = 1;
+    private int mNightColor = 0;
+    private int mCameraPrivacyLightAlsAveragingIntervalMillis = 5000;
+    private int mCameraPrivacyLightAlsNightThreshold = (int) getLightSensorValue(15);
+
     private MockitoSession mMockitoSession;
 
     @Mock
     private Context mContext;
 
     @Mock
+    private Resources mResources;
+
+    @Mock
     private LightsManager mLightsManager;
 
     @Mock
     private AppOpsManager mAppOpsManager;
 
     @Mock
+    private SensorManager mSensorManager;
+
+    @Mock
     private LightsManager.LightsSession mLightsSession;
 
+    @Mock
+    private Sensor mLightSensor;
+
     private ArgumentCaptor<AppOpsManager.OnOpActiveChangedListener> mAppOpsListenerCaptor =
             ArgumentCaptor.forClass(AppOpsManager.OnOpActiveChangedListener.class);
 
     private ArgumentCaptor<LightsRequest> mLightsRequestCaptor =
             ArgumentCaptor.forClass(LightsRequest.class);
 
+    private ArgumentCaptor<SensorEventListener> mLightSensorListenerCaptor =
+            ArgumentCaptor.forClass(SensorEventListener.class);
+
     private Set<String> mExemptedPackages = new ArraySet<>();
     private List<Light> mLights = new ArrayList<>();
 
@@ -86,11 +116,22 @@
                 .spyStatic(PermissionManager.class)
                 .startMocking();
 
+        doReturn(mDayColor).when(mContext).getColor(R.color.camera_privacy_light_day);
+        doReturn(mNightColor).when(mContext).getColor(R.color.camera_privacy_light_night);
+
+        doReturn(mResources).when(mContext).getResources();
+        doReturn(mCameraPrivacyLightAlsAveragingIntervalMillis).when(mResources)
+                .getInteger(R.integer.config_cameraPrivacyLightAlsAveragingIntervalMillis);
+        doReturn(mCameraPrivacyLightAlsNightThreshold).when(mResources)
+                .getInteger(R.integer.config_cameraPrivacyLightAlsNightThreshold);
+
         doReturn(mLightsManager).when(mContext).getSystemService(LightsManager.class);
         doReturn(mAppOpsManager).when(mContext).getSystemService(AppOpsManager.class);
+        doReturn(mSensorManager).when(mContext).getSystemService(SensorManager.class);
 
         doReturn(mLights).when(mLightsManager).getLights();
         doReturn(mLightsSession).when(mLightsManager).openSession(anyInt());
+        doReturn(mLightSensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT);
 
         doReturn(mExemptedPackages)
                 .when(() -> PermissionManager.getIndicatorExemptedPackages(any()));
@@ -107,7 +148,7 @@
     @Test
     public void testAppsOpsListenerNotRegisteredWithoutCameraLights() {
         mLights.add(getNextLight(false));
-        new CameraPrivacyLightController(mContext);
+        createCameraPrivacyLightController();
 
         verify(mAppOpsManager, times(0)).startWatchingActive(any(), any(), any());
     }
@@ -116,7 +157,7 @@
     public void testAppsOpsListenerRegisteredWithCameraLight() {
         mLights.add(getNextLight(true));
 
-        new CameraPrivacyLightController(mContext);
+        createCameraPrivacyLightController();
 
         verify(mAppOpsManager, times(1)).startWatchingActive(any(), any(), any());
     }
@@ -128,14 +169,13 @@
             mLights.add(getNextLight(r.nextBoolean()));
         }
 
-        new CameraPrivacyLightController(mContext);
+        createCameraPrivacyLightController();
 
         // Verify no session has been opened at this point.
         verify(mLightsManager, times(0)).openSession(anyInt());
 
         // Set camera op as active.
-        verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
-        mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10101, "pkg1", true);
+        openCamera();
 
         // Verify session has been opened exactly once
         verify(mLightsManager, times(1)).openSession(anyInt());
@@ -161,7 +201,7 @@
     public void testWillOnlyOpenOnceWhenTwoPackagesStartOp() {
         mLights.add(getNextLight(true));
 
-        new CameraPrivacyLightController(mContext);
+        createCameraPrivacyLightController();
 
         verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
 
@@ -176,7 +216,7 @@
     public void testWillCloseOnFinishOp() {
         mLights.add(getNextLight(true));
 
-        new CameraPrivacyLightController(mContext);
+        createCameraPrivacyLightController();
 
         verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
 
@@ -192,7 +232,7 @@
     public void testWillCloseOnFinishOpForAllPackages() {
         mLights.add(getNextLight(true));
 
-        new CameraPrivacyLightController(mContext);
+        createCameraPrivacyLightController();
 
         int numUids = 100;
         List<Integer> uids = new ArrayList<>(numUids);
@@ -226,7 +266,7 @@
         mLights.add(getNextLight(true));
         mExemptedPackages.add("pkg1");
 
-        new CameraPrivacyLightController(mContext);
+        createCameraPrivacyLightController();
 
         verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
 
@@ -235,6 +275,147 @@
         verify(mLightsManager, times(0)).openSession(anyInt());
     }
 
+    @Test
+    public void testNoLightSensor() {
+        mLights.add(getNextLight(true));
+        doReturn(null).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT);
+
+        createCameraPrivacyLightController();
+
+        openCamera();
+
+        verify(mLightsSession).requestLights(mLightsRequestCaptor.capture());
+        LightsRequest lightsRequest = mLightsRequestCaptor.getValue();
+        for (LightState lightState : lightsRequest.getLightStates()) {
+            assertEquals(mDayColor, lightState.getColor());
+        }
+    }
+
+    @Test
+    public void testALSListenerNotRegisteredUntilCameraIsOpened() {
+        mLights.add(getNextLight(true));
+        Sensor sensor = mock(Sensor.class);
+        doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT);
+
+        CameraPrivacyLightController cplc = createCameraPrivacyLightController();
+
+        verify(mSensorManager, never()).registerListener(any(SensorEventListener.class),
+                any(Sensor.class), anyInt(), any(Handler.class));
+
+        openCamera();
+
+        verify(mSensorManager, times(1)).registerListener(mLightSensorListenerCaptor.capture(),
+                any(Sensor.class), anyInt(), any(Handler.class));
+
+        mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10001, "pkg", false);
+        verify(mSensorManager, times(1)).unregisterListener(mLightSensorListenerCaptor.getValue());
+    }
+
+    @Ignore
+    @Test
+    public void testDayColor() {
+        testBrightnessToColor(20, mDayColor);
+    }
+
+    @Ignore
+    @Test
+    public void testNightColor() {
+        testBrightnessToColor(10, mNightColor);
+    }
+
+    private void testBrightnessToColor(int brightnessValue, int color) {
+        mLights.add(getNextLight(true));
+        Sensor sensor = mock(Sensor.class);
+        doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT);
+
+        CameraPrivacyLightController cplc = createCameraPrivacyLightController();
+        cplc.setElapsedRealTime(0);
+
+        openCamera();
+
+        verify(mSensorManager).registerListener(mLightSensorListenerCaptor.capture(),
+                any(Sensor.class), anyInt(), any(Handler.class));
+        SensorEventListener sensorListener = mLightSensorListenerCaptor.getValue();
+        float[] sensorEventValues = new float[1];
+        SensorEvent sensorEvent = new SensorEvent(sensor, 0, 0, sensorEventValues);
+
+        sensorEventValues[0] = getLightSensorValue(brightnessValue);
+        sensorListener.onSensorChanged(sensorEvent);
+
+        verify(mLightsSession, atLeastOnce()).requestLights(mLightsRequestCaptor.capture());
+        for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) {
+            assertEquals(color, lightState.getColor());
+        }
+    }
+
+    @Ignore
+    @Test
+    public void testDayToNightTransistion() {
+        mLights.add(getNextLight(true));
+        Sensor sensor = mock(Sensor.class);
+        doReturn(sensor).when(mSensorManager).getDefaultSensor(Sensor.TYPE_LIGHT);
+
+        CameraPrivacyLightController cplc = createCameraPrivacyLightController();
+        cplc.setElapsedRealTime(0);
+
+        openCamera();
+        // There will be an initial call at brightness 0
+        verify(mLightsSession, times(1)).requestLights(any(LightsRequest.class));
+
+        verify(mSensorManager).registerListener(mLightSensorListenerCaptor.capture(),
+                any(Sensor.class), anyInt(), any(Handler.class));
+        SensorEventListener sensorListener = mLightSensorListenerCaptor.getValue();
+
+        onSensorEvent(cplc, sensorListener, sensor, 0, 20);
+
+        // 5 sec avg = 20
+        onSensorEvent(cplc, sensorListener, sensor, 5000, 30);
+
+        verify(mLightsSession, times(2)).requestLights(mLightsRequestCaptor.capture());
+        for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) {
+            assertEquals(mDayColor, lightState.getColor());
+        }
+
+        // 5 sec avg = 22
+
+        onSensorEvent(cplc, sensorListener, sensor, 6000, 10);
+
+        // 5 sec avg = 18
+
+        onSensorEvent(cplc, sensorListener, sensor, 8000, 5);
+
+        // Should have always been day
+        verify(mLightsSession, times(2)).requestLights(mLightsRequestCaptor.capture());
+        for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) {
+            assertEquals(mDayColor, lightState.getColor());
+        }
+
+        // 5 sec avg = 12
+
+        onSensorEvent(cplc, sensorListener, sensor, 10000, 5);
+
+        // Should now be night
+        verify(mLightsSession, times(3)).requestLights(mLightsRequestCaptor.capture());
+        for (LightState lightState : mLightsRequestCaptor.getValue().getLightStates()) {
+            assertEquals(mNightColor, lightState.getColor());
+        }
+    }
+
+    private void onSensorEvent(CameraPrivacyLightController cplc,
+            SensorEventListener sensorListener, Sensor sensor, long timestamp, int value) {
+        cplc.setElapsedRealTime(timestamp);
+        sensorListener.onSensorChanged(new SensorEvent(sensor, 0, timestamp,
+                new float[] {getLightSensorValue(value)}));
+    }
+
+    // Use the test thread so that the test is deterministic
+    private CameraPrivacyLightController createCameraPrivacyLightController() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+        return new CameraPrivacyLightController(mContext, Looper.myLooper());
+    }
+
     private Light getNextLight(boolean cameraType) {
         Light light = ExtendedMockito.mock(Light.class);
         if (cameraType) {
@@ -245,4 +426,13 @@
         doReturn(mNextLightId++).when(light).getId();
         return light;
     }
+
+    private float getLightSensorValue(int i) {
+        return (float) Math.exp(i / CameraPrivacyLightController.LIGHT_VALUE_MULTIPLIER);
+    }
+
+    private void openCamera() {
+        verify(mAppOpsManager).startWatchingActive(any(), any(), mAppOpsListenerCaptor.capture());
+        mAppOpsListenerCaptor.getValue().onOpActiveChanged(OPSTR_CAMERA, 10001, "pkg", true);
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
index 534d0a1..f9f6fe9 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
@@ -46,6 +46,8 @@
     @Mock
     private CompleteEconomicPolicy mEconomicPolicy;
     @Mock
+    private Analyst mAnalyst;
+    @Mock
     private Context mContext;
     @Mock
     private InternalResourceService mIrs;
@@ -53,8 +55,8 @@
     private Scribe mScribe;
 
     private static class MockScribe extends Scribe {
-        MockScribe(InternalResourceService irs) {
-            super(irs);
+        MockScribe(InternalResourceService irs, Analyst analyst) {
+            super(irs, analyst);
         }
 
         @Override
@@ -74,7 +76,7 @@
         doReturn(mEconomicPolicy).when(mIrs).getCompleteEconomicPolicyLocked();
         doReturn(mIrs).when(mIrs).getLock();
         doReturn(mock(AlarmManager.class)).when(mContext).getSystemService(Context.ALARM_SERVICE);
-        mScribe = new MockScribe(mIrs);
+        mScribe = new MockScribe(mIrs, mAnalyst);
     }
 
     @After
@@ -86,7 +88,7 @@
 
     @Test
     public void testRecordTransaction_UnderMax() {
-        Agent agent = new Agent(mIrs, mScribe);
+        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
         Ledger ledger = new Ledger();
 
         doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked();
@@ -115,7 +117,7 @@
 
     @Test
     public void testRecordTransaction_MaxConsumptionLimit() {
-        Agent agent = new Agent(mIrs, mScribe);
+        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
         Ledger ledger = new Ledger();
 
         doReturn(1000L).when(mIrs).getConsumptionLimitLocked();
@@ -162,7 +164,7 @@
 
     @Test
     public void testRecordTransaction_MaxSatiatedBalance() {
-        Agent agent = new Agent(mIrs, mScribe);
+        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
         Ledger ledger = new Ledger();
 
         doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked();
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
index 14f95e9..721777c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
@@ -16,7 +16,7 @@
 
 package com.android.server.tare;
 
-
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 
 import static org.junit.Assert.assertEquals;
@@ -41,6 +41,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoSession;
 import org.mockito.quality.Strictness;
@@ -66,8 +68,11 @@
     private Scribe mScribeUnderTest;
     private File mTestFileDir;
     private final List<PackageInfo> mInstalledPackages = new ArrayList<>();
+    private final List<Analyst.Report> mReports = new ArrayList<>();
 
     @Mock
+    private Analyst mAnalyst;
+    @Mock
     private InternalResourceService mIrs;
 
     private Context getContext() {
@@ -84,11 +89,12 @@
         when(mIrs.getLock()).thenReturn(new Object());
         when(mIrs.isEnabled()).thenReturn(true);
         when(mIrs.getInstalledPackages()).thenReturn(mInstalledPackages);
+        when(mAnalyst.getReports()).thenReturn(mReports);
         mTestFileDir = new File(getContext().getFilesDir(), "scribe_test");
         //noinspection ResultOfMethodCallIgnored
         mTestFileDir.mkdirs();
         Log.d(TAG, "Saving data to '" + mTestFileDir + "'");
-        mScribeUnderTest = new Scribe(mIrs, mTestFileDir);
+        mScribeUnderTest = new Scribe(mIrs, mAnalyst, mTestFileDir);
 
         addInstalledPackage(TEST_USER_ID, TEST_PACKAGE);
     }
@@ -105,6 +111,62 @@
     }
 
     @Test
+    public void testWritingAnalystReportsToDisk() {
+        ArgumentCaptor<List<Analyst.Report>> reportCaptor =
+                ArgumentCaptor.forClass(List.class);
+
+        InOrder inOrder = inOrder(mAnalyst);
+
+        // Empty set
+        mReports.clear();
+        mScribeUnderTest.writeImmediatelyForTesting();
+        mScribeUnderTest.loadFromDiskLocked();
+        inOrder.verify(mAnalyst).loadReports(reportCaptor.capture());
+        List<Analyst.Report> result = reportCaptor.getValue();
+        assertReportListsEqual(mReports, result);
+
+        Analyst.Report report1 = new Analyst.Report();
+        report1.cumulativeBatteryDischarge = 1;
+        report1.currentBatteryLevel = 2;
+        report1.cumulativeProfit = 3;
+        report1.numProfitableActions = 4;
+        report1.cumulativeLoss = 5;
+        report1.numUnprofitableActions = 6;
+        report1.cumulativeRewards = 7;
+        report1.numRewards = 8;
+        report1.cumulativePositiveRegulations = 9;
+        report1.numPositiveRegulations = 10;
+        report1.cumulativeNegativeRegulations = 11;
+        report1.numNegativeRegulations = 12;
+        mReports.add(report1);
+        mScribeUnderTest.writeImmediatelyForTesting();
+        mScribeUnderTest.loadFromDiskLocked();
+        inOrder.verify(mAnalyst).loadReports(reportCaptor.capture());
+        result = reportCaptor.getValue();
+        assertReportListsEqual(mReports, result);
+
+        Analyst.Report report2 = new Analyst.Report();
+        report2.cumulativeBatteryDischarge = 10;
+        report2.currentBatteryLevel = 20;
+        report2.cumulativeProfit = 30;
+        report2.numProfitableActions = 40;
+        report2.cumulativeLoss = 50;
+        report2.numUnprofitableActions = 60;
+        report2.cumulativeRewards = 70;
+        report2.numRewards = 80;
+        report2.cumulativePositiveRegulations = 90;
+        report2.numPositiveRegulations = 100;
+        report2.cumulativeNegativeRegulations = 110;
+        report2.numNegativeRegulations = 120;
+        mReports.add(report2);
+        mScribeUnderTest.writeImmediatelyForTesting();
+        mScribeUnderTest.loadFromDiskLocked();
+        inOrder.verify(mAnalyst).loadReports(reportCaptor.capture());
+        result = reportCaptor.getValue();
+        assertReportListsEqual(mReports, result);
+    }
+
+    @Test
     public void testWriteHighLevelStateToDisk() {
         long lastReclamationTime = System.currentTimeMillis();
         long remainingConsumableCakes = 2000L;
@@ -277,6 +339,49 @@
         }
     }
 
+    private void assertReportListsEqual(List<Analyst.Report> expected,
+            List<Analyst.Report> actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        assertEquals(expected.size(), actual.size());
+        for (int i = 0; i < expected.size(); ++i) {
+            Analyst.Report eReport = expected.get(i);
+            Analyst.Report aReport = actual.get(i);
+            if (eReport == null) {
+                assertNull(aReport);
+                continue;
+            }
+            assertNotNull(aReport);
+            assertEquals("Reports #" + i + " cumulativeBatteryDischarge are not equal",
+                    eReport.cumulativeBatteryDischarge, aReport.cumulativeBatteryDischarge);
+            assertEquals("Reports #" + i + " currentBatteryLevel are not equal",
+                    eReport.currentBatteryLevel, aReport.currentBatteryLevel);
+            assertEquals("Reports #" + i + " cumulativeProfit are not equal",
+                    eReport.cumulativeProfit, aReport.cumulativeProfit);
+            assertEquals("Reports #" + i + " numProfitableActions are not equal",
+                    eReport.numProfitableActions, aReport.numProfitableActions);
+            assertEquals("Reports #" + i + " cumulativeLoss are not equal",
+                    eReport.cumulativeLoss, aReport.cumulativeLoss);
+            assertEquals("Reports #" + i + " numUnprofitableActions are not equal",
+                    eReport.numUnprofitableActions, aReport.numUnprofitableActions);
+            assertEquals("Reports #" + i + " cumulativeRewards are not equal",
+                    eReport.cumulativeRewards, aReport.cumulativeRewards);
+            assertEquals("Reports #" + i + " numRewards are not equal",
+                    eReport.numRewards, aReport.numRewards);
+            assertEquals("Reports #" + i + " cumulativePositiveRegulations are not equal",
+                    eReport.cumulativePositiveRegulations, aReport.cumulativePositiveRegulations);
+            assertEquals("Reports #" + i + " numPositiveRegulations are not equal",
+                    eReport.numPositiveRegulations, aReport.numPositiveRegulations);
+            assertEquals("Reports #" + i + " cumulativeNegativeRegulations are not equal",
+                    eReport.cumulativeNegativeRegulations, aReport.cumulativeNegativeRegulations);
+            assertEquals("Reports #" + i + " numNegativeRegulations are not equal",
+                    eReport.numNegativeRegulations, aReport.numNegativeRegulations);
+        }
+    }
+
     private void assertTransactionsEqual(Ledger.Transaction expected, Ledger.Transaction actual) {
         if (expected == null) {
             assertNull(actual);
diff --git a/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java b/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java
index 18e0f29..bce99a0 100644
--- a/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java
@@ -416,6 +416,7 @@
                 null /* resolvedType */,
                 null /* requiredPermissions */,
                 null /* excludedPermissions */,
+                null /* excludedPackages */,
                 0 /* appOp */,
                 null /* options */,
                 new ArrayList<>(receivers), // Make a copy to not affect the original list.
diff --git a/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java b/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java
index 5b3a128..98f0603 100644
--- a/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/apphibernation/AppHibernationServiceTest.java
@@ -339,7 +339,7 @@
         ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mIActivityManager, times(2)).broadcastIntentWithFeature(any(), any(),
                 intentArgumentCaptor.capture(), any(), any(), anyInt(), any(), any(), any(), any(),
-                anyInt(), any(), anyBoolean(), anyBoolean(), eq(USER_ID_1));
+                any(), anyInt(), any(), anyBoolean(), anyBoolean(), eq(USER_ID_1));
         List<Intent> capturedIntents = intentArgumentCaptor.getAllValues();
         assertEquals(capturedIntents.get(0).getAction(), Intent.ACTION_LOCKED_BOOT_COMPLETED);
         assertEquals(capturedIntents.get(1).getAction(), Intent.ACTION_BOOT_COMPLETED);
diff --git a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
new file mode 100644
index 0000000..b17c3a1
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.audio;
+
+import com.android.server.audio.SpatializerHelper.SADeviceState;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioSystem;
+import android.util.Log;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import java.util.List;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class SpatializerHelperTest {
+
+    private static final String TAG = "SpatializerHelperTest";
+
+    // the actual class under test
+    private SpatializerHelper mSpatHelper;
+
+    @Mock private AudioService mMockAudioService;
+    @Spy private AudioSystemAdapter mSpyAudioSystem;
+
+    @Before
+    public void setUp() throws Exception {
+        mMockAudioService = mock(AudioService.class);
+        mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
+
+        mSpatHelper = new SpatializerHelper(mMockAudioService, mSpyAudioSystem);
+    }
+
+    @Test
+    public void testSADeviceStateNullAddressCtor() throws Exception {
+        try {
+            SADeviceState devState = new SADeviceState(
+                    AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, null);
+            Assert.fail();
+        } catch (NullPointerException e) { }
+    }
+
+    @Test
+    public void testSADeviceStateStringSerialization() throws Exception {
+        Log.i(TAG, "starting testSADeviceStateStringSerialization");
+        final SADeviceState devState = new SADeviceState(
+                AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "bla");
+        devState.mHasHeadTracker = false;
+        devState.mHeadTrackerEnabled = false;
+        devState.mEnabled = true;
+        final String persistString = devState.toPersistableString();
+        final SADeviceState result = SADeviceState.fromPersistedString(persistString);
+        Log.i(TAG, "original:" + devState);
+        Log.i(TAG, "result  :" + result);
+        Assert.assertEquals(devState, result);
+    }
+
+    @Test
+    public void testSADeviceSettings() throws Exception {
+        Log.i(TAG, "starting testSADeviceSettings");
+        final AudioDeviceAttributes dev1 =
+                new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, "");
+        final AudioDeviceAttributes dev2 =
+                new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "C3:P0:beep");
+        final AudioDeviceAttributes dev3 =
+                new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "R2:D2:bloop");
+
+        doNothing().when(mMockAudioService).persistSpatialAudioDeviceSettings();
+
+        // test with single device
+        mSpatHelper.addCompatibleAudioDevice(dev1);
+        checkAddSettings();
+
+        // test with 2+ devices so separator character is used in list
+        mSpatHelper.addCompatibleAudioDevice(dev2);
+        Assert.assertTrue(mSpatHelper.isAvailableForDevice(dev2));
+        checkAddSettings();
+        Assert.assertTrue(mSpatHelper.isAvailableForDevice(dev2));
+        mSpatHelper.addCompatibleAudioDevice(dev3);
+        checkAddSettings();
+
+        // test adding a device twice in the list
+        mSpatHelper.addCompatibleAudioDevice(dev1);
+        checkAddSettings();
+
+        // test removing a device
+        mSpatHelper.removeCompatibleAudioDevice(dev2);
+        // spatializer could still be run for dev2 (is available) but spatial audio
+        // is disabled for dev2 by removeCompatibleAudioDevice
+        Assert.assertTrue(mSpatHelper.isAvailableForDevice(dev2));
+        List<AudioDeviceAttributes> compatDevices = mSpatHelper.getCompatibleAudioDevices();
+        Assert.assertFalse(compatDevices.stream().anyMatch(dev -> dev.equalTypeAddress(dev2)));
+        checkAddSettings();
+    }
+
+    /**
+     * Gets the string representing the current configuration of the devices, then clears it
+     * and restores the configuration. Verify the new string from the restored settings matches
+     * the original one.
+     */
+    private void checkAddSettings() throws Exception {
+        String settings = mSpatHelper.getSADeviceSettings();
+        Log.i(TAG, "device settings: " + settings);
+        mSpatHelper.clearSADevices();
+        mSpatHelper.setSADeviceSettings(settings);
+        String settingsRestored = mSpatHelper.getSADeviceSettings();
+        Log.i(TAG, "device settingsRestored: " + settingsRestored);
+        Assert.assertEquals(settings, settingsRestored);
+    }
+}
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 77cbb3a..5d9d765 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
@@ -34,6 +34,9 @@
 import android.testing.TestableLooper;
 import android.view.Display;
 import android.view.DisplayInfo;
+import android.view.WindowManager;
+
+import androidx.test.InstrumentationRegistry;
 
 import com.android.server.LocalServices;
 
@@ -79,7 +82,9 @@
         // 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);
+                new Handler(TestableLooper.get(this).getLooper()),
+                InstrumentationRegistry.getTargetContext().getSystemService(WindowManager.class),
+                threadVerifier);
     }
 
     @Test
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 cbb9fd7..f9671e5 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
@@ -77,6 +77,7 @@
 import android.util.ArraySet;
 import android.view.DisplayInfo;
 import android.view.KeyEvent;
+import android.view.WindowManager;
 
 import androidx.test.InstrumentationRegistry;
 
@@ -208,7 +209,8 @@
         // 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);
+                new Handler(TestableLooper.get(this).getLooper()),
+                mContext.getSystemService(WindowManager.class), 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/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 0fc201e..ec6b674 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -6996,22 +6996,23 @@
         dpm.setUserControlDisabledPackages(admin1, testPackages);
 
         verify(getServices().packageManagerInternal)
-                .setDeviceOwnerProtectedPackages(admin1.getPackageName(), testPackages);
+                .setOwnerProtectedPackages(UserHandle.USER_ALL, testPackages);
         assertThat(dpm.getUserControlDisabledPackages(admin1)).isEqualTo(testPackages);
     }
 
     @Test
-    public void testSetUserControlDisabledPackages_failingAsPO() {
+    public void testSetUserControlDisabledPackages_asPO() {
         final List<String> testPackages = new ArrayList<>();
         testPackages.add("package_1");
         testPackages.add("package_2");
         mServiceContext.permissions.add(permission.MANAGE_DEVICE_ADMINS);
         setAsProfileOwner(admin1);
 
-        assertExpectException(SecurityException.class, /* messageRegex= */ null,
-                () -> dpm.setUserControlDisabledPackages(admin1, testPackages));
-        assertExpectException(SecurityException.class, /* messageRegex= */ null,
-                () -> dpm.getUserControlDisabledPackages(admin1));
+        dpm.setUserControlDisabledPackages(admin1, testPackages);
+
+        verify(getServices().packageManagerInternal)
+                .setOwnerProtectedPackages(CALLER_USER_HANDLE, testPackages);
+        assertThat(dpm.getUserControlDisabledPackages(admin1)).isEqualTo(testPackages);
     }
 
     private void configureProfileOwnerOfOrgOwnedDevice(ComponentName who, int userId) {
@@ -7845,7 +7846,7 @@
         dpm.setUserControlDisabledPackages(admin1, packages);
 
         verify(getServices().packageManagerInternal)
-                .setDeviceOwnerProtectedPackages(eq(admin1.getPackageName()), eq(packages));
+                .setOwnerProtectedPackages(eq(UserHandle.USER_ALL), eq(packages));
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index f27b8c2..8112ca8 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -560,8 +560,19 @@
         HdmiCecMessage reportArcInitiated = HdmiCecMessageBuilder.buildReportArcInitiated(
                 ADDR_TV,
                 ADDR_AUDIO_SYSTEM);
-        assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
+        // <Report ARC Initiated> should only be sent after SAD querying is done
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
+
+        // Finish querying SADs
         assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+        mNativeWrapper.clearResultMessages();
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index f7983ca..3228e82 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -139,11 +139,12 @@
     }
 
     @Test
-    public void noResponse_queryAgain_emptyResult() {
+    public void noResponse_queryAgainOnce_emptyResult() {
         RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM,
                 mCallback);
         action.start();
         mTestLooper.dispatchAll();
+        assertThat(mSupportedSads).isNull();
 
         HdmiCecMessage expected1 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
                 mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
@@ -153,45 +154,90 @@
         mTestLooper.moveTimeForward(TIMEOUT_MS);
         mTestLooper.dispatchAll();
         assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
-        mNativeWrapper.clearResultMessages();
         mTestLooper.moveTimeForward(TIMEOUT_MS);
         mTestLooper.dispatchAll();
 
+        assertThat(mSupportedSads).isNotNull();
+        assertThat(mSupportedSads.size()).isEqualTo(0);
+
         HdmiCecMessage expected2 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
                 mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
                 CODECS_TO_QUERY_2.stream().mapToInt(i -> i).toArray());
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected2);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected2);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-
         HdmiCecMessage expected3 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
                 mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
                 CODECS_TO_QUERY_3.stream().mapToInt(i -> i).toArray());
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected3);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected3);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-
         HdmiCecMessage expected4 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
                 mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
                 CODECS_TO_QUERY_4.stream().mapToInt(i -> i).toArray());
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected4);
-        mNativeWrapper.clearResultMessages();
+
         mTestLooper.moveTimeForward(TIMEOUT_MS);
         mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected4);
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
         mTestLooper.moveTimeForward(TIMEOUT_MS);
         mTestLooper.dispatchAll();
 
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected2);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected3);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected4);
+        assertThat(mSupportedSads.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void unrecognizedOpcode_dontQueryAgain_emptyResult() {
+        RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM,
+                mCallback);
+        action.start();
+        mTestLooper.dispatchAll();
+        assertThat(mSupportedSads).isNull();
+
+        HdmiCecMessage unrecognizedOpcode = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                Constants.ADDR_AUDIO_SYSTEM, mTvLogicalAddress,
+                Constants.MESSAGE_REQUEST_SHORT_AUDIO_DESCRIPTOR,
+                Constants.ABORT_UNRECOGNIZED_OPCODE);
+
+        HdmiCecMessage expected1 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
+                mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
+                CODECS_TO_QUERY_1.stream().mapToInt(i -> i).toArray());
+        assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
+        action.processCommand(unrecognizedOpcode);
+        mTestLooper.dispatchAll();
+
+        assertThat(mSupportedSads).isNotNull();
+        assertThat(mSupportedSads.size()).isEqualTo(0);
+
+        HdmiCecMessage expected2 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
+                mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
+                CODECS_TO_QUERY_2.stream().mapToInt(i -> i).toArray());
+        HdmiCecMessage expected3 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
+                mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
+                CODECS_TO_QUERY_3.stream().mapToInt(i -> i).toArray());
+        HdmiCecMessage expected4 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
+                mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
+                CODECS_TO_QUERY_4.stream().mapToInt(i -> i).toArray());
+
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected2);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected3);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected4);
         assertThat(mSupportedSads.size()).isEqualTo(0);
     }
 
@@ -455,11 +501,12 @@
     }
 
     @Test
-    public void invalidMessageLength_queryAgain() {
+    public void invalidMessageLength_queryAgainOnce() {
         RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM,
                 mCallback);
         action.start();
         mTestLooper.dispatchAll();
+        assertThat(mSupportedSads).isNull();
 
         HdmiCecMessage expected1 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
                 mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
@@ -482,63 +529,35 @@
         mTestLooper.moveTimeForward(TIMEOUT_MS);
         mTestLooper.dispatchAll();
 
+        assertThat(mSupportedSads).isNotNull();
+        assertThat(mSupportedSads.size()).isEqualTo(0);
+
         HdmiCecMessage expected2 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
                 mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
                 CODECS_TO_QUERY_2.stream().mapToInt(i -> i).toArray());
-        byte[] sadsToRespond_2 = new byte[]{
-                0x05, 0x18, 0x4A,
-                0x06, 0x64, 0x5A,
-                0x07,
-                0x08, 0x20, 0x0A};
-        HdmiCecMessage response2 = HdmiCecMessageBuilder.buildReportShortAudioDescriptor(
-                Constants.ADDR_AUDIO_SYSTEM, mTvLogicalAddress, sadsToRespond_2);
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected2);
-        mNativeWrapper.clearResultMessages();
-        action.processCommand(response2);
-        mTestLooper.dispatchAll();
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected2);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-
         HdmiCecMessage expected3 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
                 mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
                 CODECS_TO_QUERY_3.stream().mapToInt(i -> i).toArray());
-        byte[] sadsToRespond_3 = new byte[0];
-        HdmiCecMessage response3 = HdmiCecMessageBuilder.buildReportShortAudioDescriptor(
-                Constants.ADDR_AUDIO_SYSTEM, mTvLogicalAddress, sadsToRespond_3);
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected3);
-        mNativeWrapper.clearResultMessages();
-        action.processCommand(response3);
-        mTestLooper.dispatchAll();
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected3);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-
         HdmiCecMessage expected4 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
                 mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
                 CODECS_TO_QUERY_4.stream().mapToInt(i -> i).toArray());
-        byte[] sadsToRespond_4 = new byte[]{
-                0x0D, 0x18, 0x4A,
-                0x0E, 0x64, 0x5A,
-                0x0F, 0x4B};
-        HdmiCecMessage response4 = HdmiCecMessageBuilder.buildReportShortAudioDescriptor(
-                Constants.ADDR_AUDIO_SYSTEM, mTvLogicalAddress, sadsToRespond_4);
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected4);
-        mNativeWrapper.clearResultMessages();
-        action.processCommand(response4);
+
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
         mTestLooper.dispatchAll();
         mTestLooper.moveTimeForward(TIMEOUT_MS);
         mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected4);
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
         mTestLooper.moveTimeForward(TIMEOUT_MS);
         mTestLooper.dispatchAll();
 
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected2);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected3);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(expected4);
         assertThat(mSupportedSads.size()).isEqualTo(0);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
index 8f5b0e1..ab292ab 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
@@ -546,6 +546,18 @@
         assertThat(backingApexFile).isNull();
     }
 
+    @Test
+    public void testActiveApexChanged() throws RemoteException {
+        ApexInfo apex1 = createApexInfo(
+                "com.apex1", 37, true, true, new File("/data/apex/active/com.apex@37.apex"));
+        apex1.activeApexChanged = true;
+        apex1.preinstalledModulePath = apex1.modulePath;
+        when(mApexService.getActivePackages()).thenReturn(new ApexInfo[]{apex1});
+        final ApexManager.ActiveApexInfo activeApex = mApexManager.getActiveApexInfos().get(0);
+        assertThat(activeApex.apexModuleName).isEqualTo("com.apex1");
+        assertThat(activeApex.activeApexChanged).isTrue();
+    }
+
     private ApexInfo createApexInfoForTestPkg(boolean isActive, boolean isFactory, int version) {
         File apexFile = extractResource(TEST_APEX_PKG,  TEST_APEX_FILE_NAME);
         ApexInfo apexInfo = new ApexInfo();
diff --git a/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java b/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java
index 7974718..9674ebd 100644
--- a/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/AppsFilterImplTest.java
@@ -22,7 +22,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doAnswer;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -35,6 +35,8 @@
 import android.content.pm.SigningDetails;
 import android.content.pm.UserInfo;
 import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
 import android.os.Process;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
@@ -74,7 +76,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.Executor;
 
 @Presubmit
 @RunWith(JUnit4.class)
@@ -103,7 +104,7 @@
     @Mock
     PackageDataSnapshot mSnapshot;
     @Mock
-    Executor mMockExecutor;
+    Handler mMockHandler;
     @Mock
     PackageManagerInternal mPmInternal;
 
@@ -210,10 +211,12 @@
         when(mSnapshot.getUserInfos()).thenReturn(USER_INFO_LIST);
         when(mPmInternal.snapshot()).thenReturn(mSnapshot);
 
-        doAnswer(invocation -> {
-            ((Runnable) invocation.getArgument(0)).run();
-            return new Object();
-        }).when(mMockExecutor).execute(any(Runnable.class));
+        // Can't mock postDelayed because of some weird bug in Mockito.
+        when(mMockHandler.sendMessageDelayed(any(Message.class), anyLong())).thenAnswer(
+                invocation -> {
+                    ((Message) invocation.getArgument(0)).getCallback().run();
+                    return null;
+                });
 
         when(mFeatureConfigMock.isGloballyEnabled()).thenReturn(true);
         when(mFeatureConfigMock.packageIsEnabled(any(AndroidPackage.class))).thenAnswer(
@@ -226,7 +229,7 @@
     public void testSystemReadyPropogates() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
         watcher.register();
         appsFilter.onSystemReady(mPmInternal);
@@ -238,7 +241,7 @@
     public void testQueriesAction_FilterMatches() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
         watcher.register();
         simulateAddBasicAndroid(appsFilter);
@@ -263,7 +266,7 @@
     public void testQueriesProtectedAction_FilterDoesNotMatch() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
         watcher.register();
         final Signature frameworkSignature = Mockito.mock(Signature.class);
@@ -312,7 +315,7 @@
     public void testQueriesProvider_FilterMatches() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
         watcher.register();
         simulateAddBasicAndroid(appsFilter);
@@ -337,7 +340,7 @@
     public void testOnUserUpdated_FilterMatches() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
 
         appsFilter.onSystemReady(mPmInternal);
@@ -385,7 +388,7 @@
     public void testQueriesDifferentProvider_Filters() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
         watcher.register();
         simulateAddBasicAndroid(appsFilter);
@@ -410,7 +413,7 @@
     public void testQueriesProviderWithSemiColon_FilterMatches() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -429,7 +432,7 @@
     public void testQueriesAction_NoMatchingAction_Filters() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -446,7 +449,7 @@
     public void testQueriesAction_NoMatchingActionFilterLowSdk_DoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -468,7 +471,7 @@
     public void testNoQueries_Filters() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -486,7 +489,7 @@
     public void testNoUsesLibrary_Filters() throws Exception {
         final AppsFilterImpl appsFilter = new AppsFilterImpl(mFeatureConfigMock,
                 new String[]{}, /* systemAppsQueryable */ false, /* overlayProvider */ null,
-                mMockExecutor);
+                mMockHandler);
 
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
@@ -513,7 +516,7 @@
     public void testUsesLibrary_DoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter = new AppsFilterImpl(mFeatureConfigMock,
                 new String[]{}, /* systemAppsQueryable */ false, /* overlayProvider */ null,
-                mMockExecutor);
+                mMockHandler);
 
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
@@ -541,7 +544,7 @@
     public void testUsesOptionalLibrary_DoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter = new AppsFilterImpl(mFeatureConfigMock,
                 new String[]{}, /* systemAppsQueryable */ false, /* overlayProvider */ null,
-                mMockExecutor);
+                mMockHandler);
 
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
@@ -569,7 +572,7 @@
     public void testUsesLibrary_ShareUid_DoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter = new AppsFilterImpl(mFeatureConfigMock,
                 new String[]{}, /* systemAppsQueryable */ false, /* overlayProvider */ null,
-                mMockExecutor);
+                mMockHandler);
 
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
@@ -602,7 +605,7 @@
     public void testForceQueryable_SystemDoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -622,7 +625,7 @@
     public void testForceQueryable_NonSystemFilters() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -641,7 +644,7 @@
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock,
                         new String[]{"com.some.package"}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -661,7 +664,7 @@
     public void testSystemSignedTarget_DoesntFilter() throws CertificateException {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         appsFilter.onSystemReady(mPmInternal);
 
         final Signature frameworkSignature = Mockito.mock(Signature.class);
@@ -692,7 +695,7 @@
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock,
                         new String[]{"com.some.package"}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -711,7 +714,7 @@
     public void testSystemQueryable_DoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{},
-                        true /* system force queryable */, null, mMockExecutor);
+                        true /* system force queryable */, null, mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -730,7 +733,7 @@
     public void testQueriesPackage_DoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -750,7 +753,7 @@
                 .thenReturn(false);
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -768,7 +771,7 @@
     public void testSystemUid_DoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -785,7 +788,7 @@
     public void testSystemUidSecondaryUser_DoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -803,7 +806,7 @@
     public void testNonSystemUid_NoCallingSetting_Filters() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -818,7 +821,7 @@
     public void testNoTargetPackage_filters() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -876,7 +879,7 @@
                         return Collections.emptyMap();
                     }
                 },
-                mMockExecutor);
+                mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -966,7 +969,7 @@
                         return Collections.emptyMap();
                     }
                 },
-                mMockExecutor);
+                mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -990,7 +993,7 @@
     public void testInitiatingApp_DoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -1009,7 +1012,7 @@
     public void testUninstalledInitiatingApp_Filters() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
@@ -1028,7 +1031,7 @@
     public void testOriginatingApp_Filters() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
         watcher.register();
         simulateAddBasicAndroid(appsFilter);
@@ -1054,7 +1057,7 @@
     public void testInstallingApp_DoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
         watcher.register();
         simulateAddBasicAndroid(appsFilter);
@@ -1080,7 +1083,7 @@
     public void testInstrumentation_DoesntFilter() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
         watcher.register();
         simulateAddBasicAndroid(appsFilter);
@@ -1109,7 +1112,7 @@
     public void testWhoCanSee() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
         watcher.register();
         simulateAddBasicAndroid(appsFilter);
@@ -1184,7 +1187,7 @@
     public void testOnChangeReport() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
         watcher.register();
         simulateAddBasicAndroid(appsFilter);
@@ -1259,7 +1262,7 @@
     public void testOnChangeReportedFilter() throws Exception {
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
         final WatchableTester watcher = new WatchableTester(appsFilter, "onChange filter");
@@ -1286,7 +1289,7 @@
         when(mFeatureConfigMock.snapshot()).thenReturn(mFeatureConfigMock);
         final AppsFilterImpl appsFilter =
                 new AppsFilterImpl(mFeatureConfigMock, new String[]{}, false, null,
-                        mMockExecutor);
+                        mMockHandler);
         simulateAddBasicAndroid(appsFilter);
         appsFilter.onSystemReady(mPmInternal);
 
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
index 952200d..2fd7853 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
@@ -23,6 +23,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -65,6 +66,7 @@
 import com.android.server.pm.parsing.pkg.PackageImpl;
 import com.android.server.pm.parsing.pkg.ParsedPackage;
 import com.android.server.pm.permission.CompatibilityPermissionInfo;
+import com.android.server.pm.pkg.PackageUserState;
 import com.android.server.pm.pkg.PackageUserStateInternal;
 import com.android.server.pm.pkg.component.ParsedActivity;
 import com.android.server.pm.pkg.component.ParsedActivityImpl;
@@ -85,6 +87,7 @@
 import com.android.server.pm.pkg.component.ParsedServiceImpl;
 import com.android.server.pm.pkg.component.ParsedUsesPermission;
 import com.android.server.pm.pkg.component.ParsedUsesPermissionImpl;
+import com.android.server.pm.pkg.parsing.PackageInfoWithoutStateUtils;
 import com.android.server.pm.pkg.parsing.ParsingPackage;
 
 import org.junit.Before;
@@ -596,6 +599,38 @@
         }
     }
 
+    @Test
+    public void testNoComponentMetadataIsCoercedToNullForInfoObject() throws Exception {
+        final File testFile = extractFile(TEST_APP4_APK);
+        try {
+            final ParsedPackage pkg = new TestPackageParser2().parsePackage(testFile, 0, false);
+            ApplicationInfo appInfo = PackageInfoWithoutStateUtils.generateApplicationInfo(pkg, 0,
+                    PackageUserState.DEFAULT, 0);
+            for (ParsedActivity activity : pkg.getActivities()) {
+                assertNotNull(activity.getMetaData());
+                assertNull(PackageInfoWithoutStateUtils.generateActivityInfoUnchecked(activity, 0,
+                        appInfo).metaData);
+            }
+            for (ParsedProvider provider : pkg.getProviders()) {
+                assertNotNull(provider.getMetaData());
+                assertNull(PackageInfoWithoutStateUtils.generateProviderInfoUnchecked(provider, 0,
+                        appInfo).metaData);
+            }
+            for (ParsedActivity receiver : pkg.getReceivers()) {
+                assertNotNull(receiver.getMetaData());
+                assertNull(PackageInfoWithoutStateUtils.generateActivityInfoUnchecked(receiver, 0,
+                        appInfo).metaData);
+            }
+            for (ParsedService service : pkg.getServices()) {
+                assertNotNull(service.getMetaData());
+                assertNull(PackageInfoWithoutStateUtils.generateServiceInfoUnchecked(service, 0,
+                        appInfo).metaData);
+            }
+        } finally {
+            testFile.delete();
+        }
+    }
+
     /**
      * A trivial subclass of package parser that only caches the package name, and throws away
      * all other information.
diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
index 8167b44..758a56f 100644
--- a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigTest.java
@@ -360,67 +360,6 @@
             .containsExactly("android.permission.BAR");
     }
 
-    @Test
-    public void pruneVendorApexPrivappAllowlists_removeVendor()
-            throws Exception {
-        File apexDir = createTempSubfolder("apex");
-
-        // Read non-vendor apex permission allowlists
-        final String allowlistNonVendorContents =
-                "<privapp-permissions package=\"com.android.apk_in_non_vendor_apex\">"
-                        + "<permission name=\"android.permission.FOO\"/>"
-                        + "<deny-permission name=\"android.permission.BAR\"/>"
-                        + "</privapp-permissions>";
-        File nonVendorPermDir =
-                createTempSubfolder("apex/com.android.non_vendor/etc/permissions");
-        File nonVendorPermissionFile =
-                createTempFile(nonVendorPermDir, "permissions.xml", allowlistNonVendorContents);
-        XmlPullParser nonVendorParser = readXmlUntilStartTag(nonVendorPermissionFile);
-        mSysConfig.readApexPrivAppPermissions(nonVendorParser, nonVendorPermissionFile,
-                apexDir.toPath());
-
-        // Read vendor apex permission allowlists
-        final String allowlistVendorContents =
-                "<privapp-permissions package=\"com.android.apk_in_vendor_apex\">"
-                        + "<permission name=\"android.permission.BAZ\"/>"
-                        + "<deny-permission name=\"android.permission.BAT\"/>"
-                        + "</privapp-permissions>";
-        File vendorPermissionFile =
-                createTempFile(createTempSubfolder("apex/com.android.vendor/etc/permissions"),
-                        "permissions.xml", allowlistNonVendorContents);
-        XmlPullParser vendorParser = readXmlUntilStartTag(vendorPermissionFile);
-        mSysConfig.readApexPrivAppPermissions(vendorParser, vendorPermissionFile,
-                apexDir.toPath());
-
-        // Read allowed vendor apex list
-        final String allowedVendorContents =
-                "<config>\n"
-                        + "    <allowed-vendor-apex package=\"com.android.vendor\" "
-                        + "installerPackage=\"com.installer\" />\n"
-                        + "</config>";
-        final File allowedVendorFolder = createTempSubfolder("folder");
-        createTempFile(allowedVendorFolder, "vendor-apex-allowlist.xml", allowedVendorContents);
-        readPermissions(allowedVendorFolder, /* Grant all permission flags */ ~0);
-
-        // Finally, prune non-vendor allowlists.
-        // There is no guarantee in which order the above reads will be done, however pruning
-        // will always happen last.
-        mSysConfig.pruneVendorApexPrivappAllowlists();
-
-        assertThat(mSysConfig.getApexPrivAppPermissions("com.android.non_vendor",
-                "com.android.apk_in_non_vendor_apex"))
-            .containsExactly("android.permission.FOO");
-        assertThat(mSysConfig.getApexPrivAppDenyPermissions("com.android.non_vendor",
-                "com.android.apk_in_non_vendor_apex"))
-            .containsExactly("android.permission.BAR");
-        assertThat(mSysConfig.getApexPrivAppPermissions("com.android.vendor",
-                "com.android.apk_in_vendor_apex"))
-            .isNull();
-        assertThat(mSysConfig.getApexPrivAppDenyPermissions("com.android.vendor",
-                "com.android.apk_in_vendor_apex"))
-            .isNull();
-    }
-
     /**
      * Tests that readPermissions works correctly for a library with on-bootclasspath-before
      * and on-bootclasspath-since.
diff --git a/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java b/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java
new file mode 100644
index 0000000..2b527a2
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.tare;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Test that the Analyst processes transactions correctly. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class AnalystTest {
+
+    @Test
+    public void testInitialState() {
+        final Analyst analyst = new Analyst();
+        assertEquals(0, analyst.getReports().size());
+    }
+
+    @Test
+    public void testBatteryLevelChange() {
+        final Analyst analyst = new Analyst();
+
+        Analyst.Report expected = new Analyst.Report();
+        expected.currentBatteryLevel = 55;
+        analyst.noteBatteryLevelChange(55);
+        assertEquals(1, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+
+        // Discharging
+        analyst.noteBatteryLevelChange(54);
+        expected.currentBatteryLevel = 54;
+        expected.cumulativeBatteryDischarge = 1;
+        assertEquals(1, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+        analyst.noteBatteryLevelChange(50);
+        expected.currentBatteryLevel = 50;
+        expected.cumulativeBatteryDischarge = 5;
+        assertEquals(1, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+
+        // Charging
+        analyst.noteBatteryLevelChange(51);
+        expected.currentBatteryLevel = 51;
+        assertEquals(1, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+        analyst.noteBatteryLevelChange(55);
+        expected.currentBatteryLevel = 55;
+        assertEquals(1, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+
+        // Reset
+        analyst.noteBatteryLevelChange(100);
+        assertEquals(2, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(0));
+        expected.currentBatteryLevel = 100;
+        expected.cumulativeBatteryDischarge = 0;
+        assertReportsEqual(expected, analyst.getReports().get(1));
+    }
+
+    @Test
+    public void testTransaction() {
+        runTestTransactions(new Analyst(), new Analyst.Report(), 1);
+    }
+
+    @Test
+    public void testTransaction_PeriodChange() {
+        final Analyst analyst = new Analyst();
+
+        Analyst.Report expected = new Analyst.Report();
+        expected.currentBatteryLevel = 55;
+        analyst.noteBatteryLevelChange(55);
+
+        runTestTransactions(analyst, expected, 1);
+
+        expected.currentBatteryLevel = 49;
+        expected.cumulativeBatteryDischarge = 6;
+        analyst.noteBatteryLevelChange(49);
+
+        runTestTransactions(analyst, expected, 1);
+
+        expected = new Analyst.Report();
+        expected.currentBatteryLevel = 100;
+        analyst.noteBatteryLevelChange(100);
+        expected.cumulativeBatteryDischarge = 0;
+
+        runTestTransactions(analyst, expected, 2);
+    }
+
+    private void runTestTransactions(Analyst analyst, Analyst.Report lastExpectedReport,
+            int numExpectedReports) {
+        Analyst.Report expected = lastExpectedReport;
+
+        // Profit
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_ACTION, null, -51, 1));
+        expected.cumulativeProfit += 50;
+        expected.numProfitableActions += 1;
+        assertEquals(numExpectedReports, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1));
+
+        // Loss
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_ACTION, null, -51, 100));
+        expected.cumulativeLoss += 49;
+        expected.numUnprofitableActions += 1;
+        assertEquals(numExpectedReports, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1));
+
+        // Reward
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD, null, 51, 0));
+        expected.cumulativeRewards += 51;
+        expected.numRewards += 1;
+        assertEquals(numExpectedReports, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1));
+
+        // Regulations
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION, null, 25, 0));
+        expected.cumulativePositiveRegulations += 25;
+        expected.numPositiveRegulations += 1;
+        assertEquals(numExpectedReports, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1));
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION, null, -25, 0));
+        expected.cumulativeNegativeRegulations += 25;
+        expected.numNegativeRegulations += 1;
+        assertEquals(numExpectedReports, analyst.getReports().size());
+        assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1));
+
+        // No-ops
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_ACTION, null, -100, 100));
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION, null, 0, 0));
+        analyst.noteTransaction(
+                new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD, null, 0, 0));
+        assertEquals(numExpectedReports, analyst.getReports().size());
+    }
+
+    @Test
+    public void testLoadReports() {
+        final Analyst analyst = new Analyst();
+
+        List<Analyst.Report> expected = new ArrayList<>();
+        analyst.loadReports(expected);
+        assertReportListsEqual(expected, analyst.getReports());
+
+        Analyst.Report report1 = new Analyst.Report();
+        report1.cumulativeBatteryDischarge = 1;
+        report1.currentBatteryLevel = 2;
+        report1.cumulativeProfit = 3;
+        report1.numProfitableActions = 4;
+        report1.cumulativeLoss = 5;
+        report1.numUnprofitableActions = 6;
+        report1.cumulativeRewards = 7;
+        report1.numRewards = 8;
+        report1.cumulativePositiveRegulations = 9;
+        report1.numPositiveRegulations = 10;
+        report1.cumulativeNegativeRegulations = 11;
+        report1.numNegativeRegulations = 12;
+        expected.add(report1);
+        analyst.loadReports(expected);
+        assertReportListsEqual(expected, analyst.getReports());
+
+        Analyst.Report report2 = new Analyst.Report();
+        report2.cumulativeBatteryDischarge = 10;
+        report2.currentBatteryLevel = 20;
+        report2.cumulativeProfit = 30;
+        report2.numProfitableActions = 40;
+        report2.cumulativeLoss = 50;
+        report2.numUnprofitableActions = 60;
+        report2.cumulativeRewards = 70;
+        report2.numRewards = 80;
+        report2.cumulativePositiveRegulations = 90;
+        report2.numPositiveRegulations = 100;
+        report2.cumulativeNegativeRegulations = 110;
+        report2.numNegativeRegulations = 120;
+        expected.add(report2);
+        analyst.loadReports(expected);
+        assertReportListsEqual(expected, analyst.getReports());
+    }
+
+    private void assertReportsEqual(Analyst.Report expected, Analyst.Report actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        assertEquals(expected.cumulativeBatteryDischarge, actual.cumulativeBatteryDischarge);
+        assertEquals(expected.currentBatteryLevel, actual.currentBatteryLevel);
+        assertEquals(expected.cumulativeProfit, actual.cumulativeProfit);
+        assertEquals(expected.numProfitableActions, actual.numProfitableActions);
+        assertEquals(expected.cumulativeLoss, actual.cumulativeLoss);
+        assertEquals(expected.numUnprofitableActions, actual.numUnprofitableActions);
+        assertEquals(expected.cumulativeRewards, actual.cumulativeRewards);
+        assertEquals(expected.numRewards, actual.numRewards);
+        assertEquals(expected.cumulativePositiveRegulations, actual.cumulativePositiveRegulations);
+        assertEquals(expected.numPositiveRegulations, actual.numPositiveRegulations);
+        assertEquals(expected.cumulativeNegativeRegulations, actual.cumulativeNegativeRegulations);
+        assertEquals(expected.numNegativeRegulations, actual.numNegativeRegulations);
+    }
+
+    private void assertReportListsEqual(List<Analyst.Report> expected,
+            List<Analyst.Report> actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        assertEquals(expected.size(), actual.size());
+        for (int i = 0; i < expected.size(); ++i) {
+            assertReportsEqual(expected.get(i), actual.get(i));
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/GnssTimeUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/GnssTimeUpdateServiceTest.java
index 8ac6dfb..aad5cd6 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/GnssTimeUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/GnssTimeUpdateServiceTest.java
@@ -85,6 +85,9 @@
         when(mMockContext.getSystemService(AlarmManager.class))
                 .thenReturn(mMockAlarmManager);
 
+        when(mMockLocationManager.hasProvider(LocationManager.GPS_PROVIDER))
+                .thenReturn(true);
+
         LocalServices.addService(LocationManagerInternal.class, mLocationManagerInternal);
 
         mGnssTimeUpdateService =
diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
index fb4d84c..c016406 100644
--- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java
@@ -179,7 +179,9 @@
     private AppStandbyController mController;
 
     private CountDownLatch mStateChangedLatch = new CountDownLatch(1);
+    private CountDownLatch mQuotaBumpLatch = new CountDownLatch(1);
     private String mLatchPkgName = null;
+    private int mLatchUserId = -1;
     private AppIdleStateChangeListener mListener = new AppIdleStateChangeListener() {
         @Override
         public void onAppIdleStateChanged(String packageName, int userId,
@@ -188,6 +190,16 @@
             if (mLatchPkgName != null && !mLatchPkgName.equals(packageName)) return;
             mStateChangedLatch.countDown();
         }
+
+        @Override
+        public void triggerTemporaryQuotaBump(String packageName, int userId) {
+            // Ignore events not related to mLatchPkgName, if set.
+            if ((mLatchPkgName != null && !mLatchPkgName.equals(packageName))
+                    || (mLatchUserId != -1 && mLatchUserId != userId)) {
+                return;
+            }
+            mQuotaBumpLatch.countDown();
+        }
     };
 
     static class MyContextWrapper extends ContextWrapper {
@@ -880,20 +892,27 @@
     }
 
     @Test
-    public void testNotificationEvent() throws Exception {
+    public void testNotificationEvent_bucketPromotion() throws Exception {
+        mInjector.mPropertiesChangedListener
+                .onPropertiesChanged(mInjector.getDeviceConfigProperties());
+
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
         assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
         mInjector.mElapsedRealtime = 1;
+        rearmQuotaBumpLatch(PACKAGE_1, USER_ID);
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
         assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
 
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
         assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
+        assertFalse(mQuotaBumpLatch.await(1, TimeUnit.SECONDS));
     }
 
     @Test
-    public void testNotificationEvent_changePromotedBucket() throws Exception {
+    public void testNotificationEvent_bucketPromotion_changePromotedBucket() throws Exception {
+        mInjector.mPropertiesChangedListener
+                .onPropertiesChanged(mInjector.getDeviceConfigProperties());
         mController.forceIdleState(PACKAGE_1, USER_ID, true);
         reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
         assertEquals(STANDBY_BUCKET_WORKING_SET, getStandbyBucket(mController, PACKAGE_1));
@@ -909,6 +928,28 @@
     }
 
     @Test
+    public void testNotificationEvent_quotaBump() throws Exception {
+        mInjector.mSettingsBuilder
+                .setBoolean("trigger_quota_bump_on_notification_seen", true);
+        mInjector.mSettingsBuilder
+                .setInt("notification_seen_promoted_bucket", STANDBY_BUCKET_NEVER);
+        mInjector.mPropertiesChangedListener
+                .onPropertiesChanged(mInjector.getDeviceConfigProperties());
+
+        reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
+        assertEquals(STANDBY_BUCKET_ACTIVE, getStandbyBucket(mController, PACKAGE_1));
+        mInjector.mElapsedRealtime = RARE_THRESHOLD * 2;
+        setAndAssertBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM);
+
+        rearmQuotaBumpLatch(PACKAGE_1, USER_ID);
+        mInjector.mElapsedRealtime += 1;
+
+        reportEvent(mController, NOTIFICATION_SEEN, mInjector.mElapsedRealtime, PACKAGE_1);
+        assertTrue(mQuotaBumpLatch.await(1, TimeUnit.SECONDS));
+        assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1));
+    }
+
+    @Test
     @FlakyTest(bugId = 185169504)
     public void testSlicePinnedEvent() throws Exception {
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
@@ -1384,6 +1425,9 @@
     @Test
     @FlakyTest(bugId = 185169504)
     public void testCascadingTimeouts() throws Exception {
+        mInjector.mPropertiesChangedListener
+                .onPropertiesChanged(mInjector.getDeviceConfigProperties());
+
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
         assertBucket(STANDBY_BUCKET_ACTIVE);
 
@@ -1408,6 +1452,9 @@
     @Test
     @FlakyTest(bugId = 185169504)
     public void testOverlappingTimeouts() throws Exception {
+        mInjector.mPropertiesChangedListener
+                .onPropertiesChanged(mInjector.getDeviceConfigProperties());
+
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
         assertBucket(STANDBY_BUCKET_ACTIVE);
 
@@ -1498,6 +1545,9 @@
     @Test
     @FlakyTest(bugId = 185169504)
     public void testPredictionNotOverridden() throws Exception {
+        mInjector.mPropertiesChangedListener
+                .onPropertiesChanged(mInjector.getDeviceConfigProperties());
+
         reportEvent(mController, USER_INTERACTION, 0, PACKAGE_1);
         assertBucket(STANDBY_BUCKET_ACTIVE);
 
@@ -2096,7 +2146,7 @@
     private void setAndAssertBucket(String pkg, int user, int bucket, int reason) throws Exception {
         rearmLatch(pkg);
         mController.setAppStandbyBucket(pkg, user, bucket, reason);
-        mStateChangedLatch.await(100, TimeUnit.MILLISECONDS);
+        mStateChangedLatch.await(1, TimeUnit.SECONDS);
         assertEquals("Failed to set package bucket", bucket,
                 getStandbyBucket(mController, PACKAGE_1));
     }
@@ -2109,4 +2159,10 @@
     private void rearmLatch() {
         rearmLatch(null);
     }
+
+    private void rearmQuotaBumpLatch(String pkgName, int userId) {
+        mLatchPkgName = pkgName;
+        mLatchUserId = userId;
+        mQuotaBumpLatch = new CountDownLatch(1);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java
index b907c62..64950aa 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationScalerTest.java
@@ -277,6 +277,6 @@
         Settings.System.putIntForUser(
                 mContextSpy.getContentResolver(), settingName, value, UserHandle.USER_CURRENT);
         // FakeSettingsProvider don't support testing triggering ContentObserver yet.
-        mVibrationSettings.update();
+        mVibrationSettings.mSettingObserver.onChange(false);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationSettingsTest.java
index 0a50e79..4ef6530 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationSettingsTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationSettingsTest.java
@@ -54,6 +54,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.media.AudioManager;
 import android.os.Handler;
+import android.os.PowerManager;
 import android.os.PowerManagerInternal;
 import android.os.PowerSaveState;
 import android.os.Process;
@@ -80,6 +81,10 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * Tests for {@link VibrationSettings}.
  *
@@ -146,6 +151,8 @@
         mVibrationSettings = new VibrationSettings(mContextSpy,
                 new Handler(mTestLooper.getLooper()), mVibrationConfigMock);
 
+        mockGoToSleep(/* goToSleepTime= */ 0, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
+
         // Simulate System defaults.
         setUserSetting(Settings.System.HAPTIC_FEEDBACK_ENABLED, 1);
         setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 0);
@@ -164,18 +171,10 @@
     public void addListener_settingsChangeTriggerListener() {
         mVibrationSettings.addListener(mListenerMock);
 
-        setUserSetting(Settings.System.VIBRATE_INPUT_DEVICES, 1);
-        setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 0);
-        setUserSetting(Settings.System.APPLY_RAMPING_RINGER, 0);
-        setUserSetting(Settings.System.ALARM_VIBRATION_INTENSITY, VIBRATION_INTENSITY_OFF);
-        setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, VIBRATION_INTENSITY_OFF);
-        setUserSetting(Settings.System.MEDIA_VIBRATION_INTENSITY, VIBRATION_INTENSITY_OFF);
-        setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_OFF);
-        setUserSetting(Settings.System.HAPTIC_FEEDBACK_ENABLED, 0);
-        setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
-        setUserSetting(Settings.System.HARDWARE_HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
+        mVibrationSettings.mSettingObserver.onChange(false);
+        mVibrationSettings.mSettingObserver.onChange(false);
 
-        verify(mListenerMock, times(10)).onChange();
+        verify(mListenerMock, times(2)).onChange();
     }
 
     @Test
@@ -204,35 +203,24 @@
 
     @Test
     public void shouldIgnoreVibration_fromBackground_doesNotIgnoreUsagesFromAllowlist() {
-        int[] expectedAllowedVibrations = new int[] {
+        Set<Integer> expectedAllowedVibrations = new HashSet<>(Arrays.asList(
                 USAGE_RINGTONE,
                 USAGE_ALARM,
                 USAGE_NOTIFICATION,
                 USAGE_COMMUNICATION_REQUEST,
                 USAGE_HARDWARE_FEEDBACK,
-                USAGE_PHYSICAL_EMULATION,
-        };
+                USAGE_PHYSICAL_EMULATION
+        ));
 
         mVibrationSettings.mUidObserver.onUidStateChanged(
                 UID, ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
 
-        for (int usage : expectedAllowedVibrations) {
-            assertVibrationNotIgnoredForUsage(usage);
-        }
-    }
-
-    @Test
-    public void shouldIgnoreVibration_fromBackground_ignoresUsagesNotInAllowlist() {
-        int[] expectedIgnoredVibrations = new int[] {
-                USAGE_TOUCH,
-                USAGE_UNKNOWN,
-        };
-
-        mVibrationSettings.mUidObserver.onUidStateChanged(
-                UID, ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0);
-
-        for (int usage : expectedIgnoredVibrations) {
-            assertVibrationIgnoredForUsage(usage, Vibration.Status.IGNORED_BACKGROUND);
+        for (int usage : ALL_USAGES) {
+            if (expectedAllowedVibrations.contains(usage)) {
+                assertVibrationNotIgnoredForUsage(usage);
+            } else {
+                assertVibrationIgnoredForUsage(usage, Vibration.Status.IGNORED_BACKGROUND);
+            }
         }
     }
 
@@ -248,33 +236,22 @@
 
     @Test
     public void shouldIgnoreVibration_inBatterySaverMode_doesNotIgnoreUsagesFromAllowlist() {
-        int[] expectedAllowedVibrations = new int[] {
+        Set<Integer> expectedAllowedVibrations = new HashSet<>(Arrays.asList(
                 USAGE_RINGTONE,
                 USAGE_ALARM,
                 USAGE_COMMUNICATION_REQUEST,
-        };
-
-        mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
-
-        for (int usage : expectedAllowedVibrations) {
-            assertVibrationNotIgnoredForUsage(usage);
-        }
-    }
-
-    @Test
-    public void shouldIgnoreVibration_inBatterySaverMode_ignoresUsagesNotInAllowlist() {
-        int[] expectedIgnoredVibrations = new int[] {
-                USAGE_NOTIFICATION,
-                USAGE_HARDWARE_FEEDBACK,
                 USAGE_PHYSICAL_EMULATION,
-                USAGE_TOUCH,
-                USAGE_UNKNOWN,
-        };
+                USAGE_HARDWARE_FEEDBACK
+        ));
 
         mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
 
-        for (int usage : expectedIgnoredVibrations) {
-            assertVibrationIgnoredForUsage(usage, Vibration.Status.IGNORED_FOR_POWER);
+        for (int usage : ALL_USAGES) {
+            if (expectedAllowedVibrations.contains(usage)) {
+                assertVibrationNotIgnoredForUsage(usage);
+            } else {
+                assertVibrationIgnoredForUsage(usage, Vibration.Status.IGNORED_FOR_POWER);
+            }
         }
     }
 
@@ -479,50 +456,112 @@
     }
 
     @Test
-    public void shouldCancelVibrationOnScreenOff_withNonSystemPackageAndUid_returnsAlwaysTrue() {
+    public void shouldCancelVibrationOnScreenOff_withEventBeforeVibration_returnsAlwaysFalse() {
+        long vibrateStartTime = 100;
+        mockGoToSleep(vibrateStartTime - 10, PowerManager.GO_TO_SLEEP_REASON_APPLICATION);
+
         for (int usage : ALL_USAGES) {
-            assertTrue(mVibrationSettings.shouldCancelVibrationOnScreenOff(UID, "some.app", usage));
+            // Non-system vibration
+            assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
+                    UID, "some.app", usage, vibrateStartTime));
+            // Vibration with UID zero
+            assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
+                    /* uid= */ 0, "", usage, vibrateStartTime));
+            // System vibration
+            assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
+                    Process.SYSTEM_UID, "", usage, vibrateStartTime));
+            // SysUI vibration
+            assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
+                    UID, SYSUI_PACKAGE_NAME, usage, vibrateStartTime));
+        }
+    }
+
+    @Test
+    public void shouldCancelVibrationOnScreenOff_withSleepReasonInAllowlist_returnsAlwaysFalse() {
+        long vibrateStartTime = 100;
+        int[] allowedSleepReasons = new int[] {
+                PowerManager.GO_TO_SLEEP_REASON_TIMEOUT,
+                PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE,
+        };
+
+        for (int sleepReason : allowedSleepReasons) {
+            mockGoToSleep(vibrateStartTime + 10, sleepReason);
+
+            for (int usage : ALL_USAGES) {
+                // Non-system vibration
+                assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
+                        UID, "some.app", usage, vibrateStartTime));
+                // Vibration with UID zero
+                assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
+                        /* uid= */ 0, "", usage, vibrateStartTime));
+                // System vibration
+                assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
+                        Process.SYSTEM_UID, "", usage, vibrateStartTime));
+                // SysUI vibration
+                assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
+                        UID, SYSUI_PACKAGE_NAME, usage, vibrateStartTime));
+            }
+        }
+    }
+
+    @Test
+    public void shouldCancelVibrationOnScreenOff_withNonSystem_returnsTrueIfReasonNotInAllowlist() {
+        long vibrateStartTime = 100;
+        mockGoToSleep(vibrateStartTime + 10, PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON);
+
+        for (int usage : ALL_USAGES) {
+            assertTrue(mVibrationSettings.shouldCancelVibrationOnScreenOff(
+                    UID, "some.app", usage, vibrateStartTime));
         }
     }
 
     @Test
     public void shouldCancelVibrationOnScreenOff_withUidZero_returnsFalseForTouchAndHardware() {
+        long vibrateStartTime = 100;
+        mockGoToSleep(vibrateStartTime + 10, PowerManager.GO_TO_SLEEP_REASON_DEVICE_ADMIN);
+
         for (int usage : ALL_USAGES) {
             if (usage == USAGE_TOUCH || usage == USAGE_HARDWARE_FEEDBACK
                     || usage == USAGE_PHYSICAL_EMULATION) {
                 assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
-                        /* uid= */ 0, "", usage));
+                        /* uid= */ 0, "", usage, vibrateStartTime));
             } else {
                 assertTrue(mVibrationSettings.shouldCancelVibrationOnScreenOff(
-                        /* uid= */ 0, "", usage));
+                        /* uid= */ 0, "", usage, vibrateStartTime));
             }
         }
     }
 
     @Test
     public void shouldCancelVibrationOnScreenOff_withSystemUid_returnsFalseForTouchAndHardware() {
+        long vibrateStartTime = 100;
+        mockGoToSleep(vibrateStartTime + 10, PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD);
+
         for (int usage : ALL_USAGES) {
             if (usage == USAGE_TOUCH || usage == USAGE_HARDWARE_FEEDBACK
                     || usage == USAGE_PHYSICAL_EMULATION) {
                 assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
-                        Process.SYSTEM_UID, "", usage));
+                        Process.SYSTEM_UID, "", usage, vibrateStartTime));
             } else {
                 assertTrue(mVibrationSettings.shouldCancelVibrationOnScreenOff(
-                        Process.SYSTEM_UID, "", usage));
+                        Process.SYSTEM_UID, "", usage, vibrateStartTime));
             }
         }
     }
 
     @Test
-    public void shouldCancelVibrationOnScreenOff_withSysUi_returnsFalseForTouchAndHardware() {
+    public void shouldCancelVibrationOnScreenOff_withSysUiPkg_returnsFalseForTouchAndHardware() {
+        long vibrateStartTime = 100;
+        mockGoToSleep(vibrateStartTime + 10, PowerManager.GO_TO_SLEEP_REASON_HDMI);
+
         for (int usage : ALL_USAGES) {
             if (usage == USAGE_TOUCH || usage == USAGE_HARDWARE_FEEDBACK
                     || usage == USAGE_PHYSICAL_EMULATION) {
                 assertFalse(mVibrationSettings.shouldCancelVibrationOnScreenOff(
-                        UID, SYSUI_PACKAGE_NAME, usage));
+                        UID, SYSUI_PACKAGE_NAME, usage, vibrateStartTime));
             } else {
                 assertTrue(mVibrationSettings.shouldCancelVibrationOnScreenOff(
-                        UID, SYSUI_PACKAGE_NAME, usage));
+                        UID, SYSUI_PACKAGE_NAME, usage, vibrateStartTime));
             }
         }
     }
@@ -581,7 +620,6 @@
     public void getCurrentIntensity_noHardwareFeedbackValueUsesHapticFeedbackValue() {
         setDefaultIntensity(USAGE_HARDWARE_FEEDBACK, VIBRATION_INTENSITY_MEDIUM);
         setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
-        mVibrationSettings.update();
         assertEquals(VIBRATION_INTENSITY_OFF, mVibrationSettings.getCurrentIntensity(USAGE_TOUCH));
         // If haptic feedback is off, fallback to default value.
         assertEquals(VIBRATION_INTENSITY_MEDIUM,
@@ -590,7 +628,6 @@
                 mVibrationSettings.getCurrentIntensity(USAGE_PHYSICAL_EMULATION));
 
         setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
-        mVibrationSettings.update();
         assertEquals(VIBRATION_INTENSITY_HIGH,
                 mVibrationSettings.getCurrentIntensity(USAGE_TOUCH));
         // If haptic feedback is on, fallback to that value.
@@ -648,19 +685,25 @@
         Settings.System.putStringForUser(
                 mContextSpy.getContentResolver(), settingName, null, UserHandle.USER_CURRENT);
         // FakeSettingsProvider doesn't support testing triggering ContentObserver yet.
-        mVibrationSettings.update();
+        mVibrationSettings.mSettingObserver.onChange(false);
     }
 
     private void setUserSetting(String settingName, int value) {
         Settings.System.putIntForUser(
                 mContextSpy.getContentResolver(), settingName, value, UserHandle.USER_CURRENT);
         // FakeSettingsProvider doesn't support testing triggering ContentObserver yet.
-        mVibrationSettings.update();
+        mVibrationSettings.mSettingObserver.onChange(false);
     }
 
     private void setRingerMode(int ringerMode) {
         mAudioManager.setRingerModeInternal(ringerMode);
         assertEquals(ringerMode, mAudioManager.getRingerModeInternal());
-        mVibrationSettings.update();
+        mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy,
+                new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
+    }
+
+    private void mockGoToSleep(long sleepTime, int reason) {
+        when(mPowerManagerInternalMock.getLastGoToSleep()).thenReturn(
+                new PowerManager.SleepData(sleepTime, reason));
     }
 }
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 80dee10..a7dc851 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -9573,6 +9573,8 @@
 
     @Test
     public void testMaybeShowReviewPermissionsNotification_unknown() {
+        reset(mMockNm);
+
         // Set up various possible states of the settings int and confirm whether or not the
         // notification is shown as expected
 
@@ -9586,6 +9588,8 @@
 
     @Test
     public void testMaybeShowReviewPermissionsNotification_shouldShow() {
+        reset(mMockNm);
+
         // If state is SHOULD_SHOW, it ... should show
         Settings.Global.putInt(mContext.getContentResolver(),
                 Settings.Global.REVIEW_PERMISSIONS_NOTIFICATION_STATE,
@@ -9598,6 +9602,8 @@
 
     @Test
     public void testMaybeShowReviewPermissionsNotification_alreadyShown() {
+        reset(mMockNm);
+
         // 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,
@@ -9614,6 +9620,8 @@
 
     @Test
     public void testMaybeShowReviewPermissionsNotification_reshown() {
+        reset(mMockNm);
+
         // 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(),
@@ -9627,6 +9635,8 @@
 
     @Test
     public void testRescheduledReviewPermissionsNotification() {
+        reset(mMockNm);
+
         // 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
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java
index 3a352cb..4c7e843 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java
@@ -17,7 +17,6 @@
 
 import static android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT;
 import static android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED;
-import static android.content.pm.PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED;
 import static android.content.pm.PackageManager.FLAG_PERMISSION_SYSTEM_FIXED;
 import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
@@ -26,8 +25,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static junit.framework.Assert.fail;
-
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
@@ -54,7 +51,6 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -62,14 +58,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.Parameter;
-import java.lang.reflect.Type;
-import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 
 @SmallTest
@@ -88,7 +77,7 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mPermissionHelper = new PermissionHelper(mPmi, mPackageManager, mPermManager, false);
+        mPermissionHelper = new PermissionHelper(mPmi, mPackageManager, mPermManager);
         PackageInfo testPkgInfo = new PackageInfo();
         testPkgInfo.requestedPermissions = new String[]{ Manifest.permission.POST_NOTIFICATIONS };
         when(mPackageManager.getPackageInfo(anyString(), anyLong(), anyInt()))
@@ -97,12 +86,12 @@
 
     @Test
     public void testHasPermission() throws Exception {
-        when(mPmi.checkPostNotificationsPermissionGrantedOrLegacyAccess(anyInt()))
+        when(mPmi.checkUidPermission(anyInt(), anyString()))
                 .thenReturn(PERMISSION_GRANTED);
 
         assertThat(mPermissionHelper.hasPermission(1)).isTrue();
 
-        when(mPmi.checkPostNotificationsPermissionGrantedOrLegacyAccess(anyInt()))
+        when(mPmi.checkUidPermission(anyInt(), anyString()))
                 .thenReturn(PERMISSION_DENIED);
 
         assertThat(mPermissionHelper.hasPermission(1)).isFalse();
@@ -194,79 +183,12 @@
         verify(mPermManager).grantRuntimePermission(
                 "pkg", Manifest.permission.POST_NOTIFICATIONS, 10);
         verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
-                FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_REVIEW_REQUIRED,
-                FLAG_PERMISSION_USER_SET, true, 10);
-    }
-
-    @Test
-    public void testSetNotificationPermission_grantReviewRequired() throws Exception {
-        when(mPmi.checkPermission(anyString(), anyString(), anyInt()))
-                .thenReturn(PERMISSION_DENIED);
-
-        mPermissionHelper.setNotificationPermission("pkg", 10, true, false, true);
-
-        verify(mPermManager, never()).revokeRuntimePermission(
-                "pkg", Manifest.permission.POST_NOTIFICATIONS, 10, "PermissionHelper");
-        verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
-                FLAG_PERMISSION_REVIEW_REQUIRED, FLAG_PERMISSION_REVIEW_REQUIRED, true, 10);
-    }
-
-    @Test
-    public void testSetNotificationPermission_pkgPerm_grantReviewRequired() throws Exception {
-        when(mPmi.checkPermission(anyString(), anyString(), anyInt()))
-                .thenReturn(PERMISSION_DENIED);
-
-        PermissionHelper.PackagePermission pkgPerm = new PermissionHelper.PackagePermission(
-                "pkg", 10, true, false);
-        mPermissionHelper.setNotificationPermission(pkgPerm);
-
-        verify(mPermManager, never()).revokeRuntimePermission(
-                "pkg", Manifest.permission.POST_NOTIFICATIONS, 10, "PermissionHelper");
-        verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
-                FLAG_PERMISSION_REVIEW_REQUIRED, FLAG_PERMISSION_REVIEW_REQUIRED, true, 10);
-    }
-
-    @Test
-    public void testSetNotificationPermission_pkgPerm_notUserSet_grantedByDefaultPermNotSet()
-            throws Exception {
-        when(mPmi.checkPermission(anyString(), anyString(), anyInt()))
-                .thenReturn(PERMISSION_DENIED);
-        when(mPermManager.getPermissionFlags(anyString(),
-                eq(Manifest.permission.POST_NOTIFICATIONS),
-                anyInt())).thenReturn(FLAG_PERMISSION_GRANTED_BY_DEFAULT);
-        PermissionHelper.PackagePermission pkgPerm = new PermissionHelper.PackagePermission(
-                "pkg", 10, true, false);
-
-        mPermissionHelper.setNotificationPermission(pkgPerm);
-        verify(mPermManager, never()).revokeRuntimePermission(
-                anyString(), anyString(), anyInt(), anyString());
-        verify(mPermManager, never()).updatePermissionFlags(
-                anyString(), anyString(), anyInt(), anyInt(), anyBoolean(), anyInt());
-    }
-
-    @Test
-    public void testSetNotificationPermission_pkgPerm_userSet_grantedByDefaultPermSet()
-            throws Exception {
-        when(mPmi.checkPermission(anyString(), anyString(), anyInt()))
-                .thenReturn(PERMISSION_DENIED);
-        when(mPermManager.getPermissionFlags(anyString(),
-                eq(Manifest.permission.POST_NOTIFICATIONS),
-                anyInt())).thenReturn(FLAG_PERMISSION_GRANTED_BY_DEFAULT);
-        PermissionHelper.PackagePermission pkgPerm = new PermissionHelper.PackagePermission(
-                "pkg", 10, true, true);
-
-        mPermissionHelper.setNotificationPermission(pkgPerm);
-        verify(mPermManager).grantRuntimePermission(
-                "pkg", Manifest.permission.POST_NOTIFICATIONS, 10);
-        verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
-                FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_REVIEW_REQUIRED,
-                FLAG_PERMISSION_USER_SET, true, 10);
+                FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, 10);
     }
 
     @Test
     public void testSetNotificationPermission_pkgPerm_grantedByDefaultPermSet_allUserSet()
             throws Exception {
-        mPermissionHelper = new PermissionHelper(mPmi, mPackageManager, mPermManager, true);
         when(mPmi.checkPermission(anyString(), anyString(), anyInt()))
                 .thenReturn(PERMISSION_DENIED);
         when(mPermManager.getPermissionFlags(anyString(),
@@ -279,8 +201,7 @@
         verify(mPermManager).grantRuntimePermission(
                 "pkg", Manifest.permission.POST_NOTIFICATIONS, 10);
         verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
-                FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_REVIEW_REQUIRED,
-                FLAG_PERMISSION_USER_SET, true, 10);
+                FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, 10);
     }
 
     @Test
@@ -293,8 +214,7 @@
         verify(mPermManager).revokeRuntimePermission(
                 eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10), anyString());
         verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
-                FLAG_PERMISSION_USER_SET | FLAG_PERMISSION_REVIEW_REQUIRED,
-                FLAG_PERMISSION_USER_SET, true, 10);
+                FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, 10);
     }
 
     @Test
@@ -306,8 +226,8 @@
 
         verify(mPermManager).grantRuntimePermission(
                 "pkg", Manifest.permission.POST_NOTIFICATIONS, 10);
-        verify(mPermManager, never()).updatePermissionFlags(
-                anyString(), anyString(), anyInt(), anyInt(), anyBoolean(), anyInt());
+        verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
+                0, FLAG_PERMISSION_USER_SET, true, 10);
     }
 
     @Test
@@ -319,8 +239,8 @@
 
         verify(mPermManager).revokeRuntimePermission(
                 eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10), anyString());
-        verify(mPermManager, never()).updatePermissionFlags(
-                anyString(), anyString(), anyInt(), anyInt(), anyBoolean(), anyInt());
+        verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
+                0, FLAG_PERMISSION_USER_SET, true, 10);
     }
 
     @Test
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 a5cec7e..8d50cea 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -85,6 +85,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.Signature;
+import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.media.AudioAttributes;
@@ -121,6 +122,8 @@
 import com.android.server.UiServiceTestCase;
 import com.android.server.notification.PermissionHelper.PackagePermission;
 
+import com.google.common.collect.ImmutableList;
+
 import org.json.JSONArray;
 import org.json.JSONObject;
 import org.junit.Before;
@@ -3523,8 +3526,37 @@
 
     @Test
     public void testUpdateNotificationChannel_fixedPermission() {
+        List<UserInfo> users = ImmutableList.of(new UserInfo(UserHandle.USER_SYSTEM, "user0", 0));
         when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true);
+        PackageInfo pm = new PackageInfo();
+        pm.packageName = PKG_O;
+        pm.applicationInfo = new ApplicationInfo();
+        pm.applicationInfo.uid = UID_O;
+        List<PackageInfo> packages = ImmutableList.of(pm);
+        when(mPm.getInstalledPackagesAsUser(any(), anyInt())).thenReturn(packages);
+        mHelper.updateFixedImportance(users);
 
+        assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O));
+
+        NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false);
+
+        NotificationChannel update = new NotificationChannel("a", "a", IMPORTANCE_NONE);
+        update.setAllowBubbles(false);
+
+        mHelper.updateNotificationChannel(PKG_O, UID_O, update, true);
+
+        assertEquals(IMPORTANCE_HIGH,
+                mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).getImportance());
+        assertEquals(false,
+                mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false).canBubble());
+    }
+
+    @Test
+    public void testUpdateNotificationChannel_defaultApp() {
+        ArraySet<Pair<String, Integer>> toAdd = new ArraySet<>();
+        toAdd.add(new Pair(PKG_O, UID_O));
+        mHelper.updateDefaultApps(0, null, toAdd);
         NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
         mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false);
 
@@ -3595,6 +3627,58 @@
     }
 
     @Test
+    public void testUpdateFixedImportance_multiUser() {
+        NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
+        NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_LOW);
+        NotificationChannel c = new NotificationChannel("c", "c", IMPORTANCE_DEFAULT);
+        // different uids, same package
+        mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false);
+        mHelper.createNotificationChannel(PKG_O, UID_O, b, false, false);
+        mHelper.createNotificationChannel(PKG_O, UserHandle.PER_USER_RANGE + 1, c, true, true);
+
+        UserInfo user = new UserInfo();
+        user.id = 0;
+        List<UserInfo> users = ImmutableList.of(user);
+        when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true);
+        PackageInfo pm = new PackageInfo();
+        pm.packageName = PKG_O;
+        pm.applicationInfo = new ApplicationInfo();
+        pm.applicationInfo.uid = UID_O;
+        List<PackageInfo> packages = ImmutableList.of(pm);
+        when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages);
+        mHelper.updateFixedImportance(users);
+
+        assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false)
+                .isImportanceLockedByCriticalDeviceFunction());
+        assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, b.getId(), false)
+                .isImportanceLockedByCriticalDeviceFunction());
+        assertFalse(mHelper.getNotificationChannel(
+                        PKG_O, UserHandle.PER_USER_RANGE + 1, c.getId(), false)
+                .isImportanceLockedByCriticalDeviceFunction());
+    }
+
+    @Test
+    public void testUpdateFixedImportance_channelDoesNotExistYet() {
+        UserInfo user = new UserInfo();
+        user.id = 0;
+        List<UserInfo> users = ImmutableList.of(user);
+        when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true);
+        PackageInfo pm = new PackageInfo();
+        pm.packageName = PKG_O;
+        pm.applicationInfo = new ApplicationInfo();
+        pm.applicationInfo.uid = UID_O;
+        List<PackageInfo> packages = ImmutableList.of(pm);
+        when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages);
+        mHelper.updateFixedImportance(users);
+
+        NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false);
+
+        assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false)
+                .isImportanceLockedByCriticalDeviceFunction());
+    }
+
+    @Test
     public void testUpdateDefaultApps_add_multiUser() {
         NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
         NotificationChannel b = new NotificationChannel("b", "b", IMPORTANCE_LOW);
@@ -3759,6 +3843,62 @@
     }
 
     @Test
+    public void testUpdateFixedImportance_thenDefaultAppsRemoves() {
+        UserInfo user = new UserInfo();
+        user.id = 0;
+        List<UserInfo> users = ImmutableList.of(user);
+        when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true);
+        PackageInfo pm = new PackageInfo();
+        pm.packageName = PKG_O;
+        pm.applicationInfo = new ApplicationInfo();
+        pm.applicationInfo.uid = UID_O;
+        List<PackageInfo> packages = ImmutableList.of(pm);
+        when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages);
+        mHelper.updateFixedImportance(users);
+
+        ArraySet<String> toRemove = new ArraySet<>();
+        toRemove.add(PKG_O);
+        mHelper.updateDefaultApps(0, toRemove, null);
+
+        assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O));
+
+        NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false);
+
+        // Still locked by permission if not role
+        assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false)
+                .isImportanceLockedByCriticalDeviceFunction());
+    }
+
+    @Test
+    public void testUpdateDefaultApps_thenNotFixedPermission() {
+        ArraySet<Pair<String, Integer>> toAdd = new ArraySet<>();
+        toAdd.add(new Pair(PKG_O, UID_O));
+        mHelper.updateDefaultApps(0, null, toAdd);
+
+        UserInfo user = new UserInfo();
+        user.id = 0;
+        List<UserInfo> users = ImmutableList.of(user);
+        when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(false);
+        PackageInfo pm = new PackageInfo();
+        pm.packageName = PKG_O;
+        pm.applicationInfo = new ApplicationInfo();
+        pm.applicationInfo.uid = UID_O;
+        List<PackageInfo> packages = ImmutableList.of(pm);
+        when(mPm.getInstalledPackagesAsUser(any(), eq(0))).thenReturn(packages);
+        mHelper.updateFixedImportance(users);
+
+        assertTrue(mHelper.isImportanceLocked(PKG_O, UID_O));
+
+        NotificationChannel a = new NotificationChannel("a", "a", IMPORTANCE_HIGH);
+        mHelper.createNotificationChannel(PKG_O, UID_O, a, true, false);
+
+        // Still locked by role if not permission
+        assertTrue(mHelper.getNotificationChannel(PKG_O, UID_O, a.getId(), false)
+                .isImportanceLockedByCriticalDeviceFunction());
+    }
+
+    @Test
     public void testChannelXml_backupDefaultApp() throws Exception {
         NotificationChannel channel1 =
                 new NotificationChannel("id1", "name1", NotificationManager.IMPORTANCE_HIGH);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 533540e..12e5653 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -611,10 +611,9 @@
         activity.setRequestedOrientation(activityCurOrientation == ORIENTATION_LANDSCAPE
                 ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_LANDSCAPE);
 
-        // Asserts fixed orientation request is ignored, and the orientation is not changed
-        // (fill Task).
-        assertEquals(activityCurOrientation, activity.getConfiguration().orientation);
-        assertFalse(activity.isLetterboxedForFixedOrientationAndAspectRatio());
+        // Asserts fixed orientation request is not ignored, and the orientation is changed.
+        assertNotEquals(activityCurOrientation, activity.getConfiguration().orientation);
+        assertTrue(activity.isLetterboxedForFixedOrientationAndAspectRatio());
     }
 
     @Test
@@ -2631,7 +2630,11 @@
             DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
                     "splash_screen_exception_list", DEFAULT_COMPONENT_PACKAGE_NAME, false);
             testLegacySplashScreen(Build.VERSION_CODES.R, TYPE_PARAMETER_LEGACY_SPLASH_SCREEN);
-            testLegacySplashScreen(Build.VERSION_CODES.S, 0);
+            testLegacySplashScreen(Build.VERSION_CODES.S, TYPE_PARAMETER_LEGACY_SPLASH_SCREEN);
+            testLegacySplashScreen(Build.VERSION_CODES.TIRAMISU,
+                    TYPE_PARAMETER_LEGACY_SPLASH_SCREEN);
+            // Above T
+            testLegacySplashScreen(Build.VERSION_CODES.TIRAMISU + 1, 0);
         } finally {
             try {
                 DeviceConfig.setProperties(properties);
@@ -2854,6 +2857,11 @@
         assertTrue(activity2.isResizeable());
         activity1.reparent(taskFragment1, POSITION_TOP);
 
+        // Adds an Activity which doesn't have shared starting data, and verify if it blocks
+        // starting window removal.
+        final ActivityRecord activity3 = new ActivityBuilder(mAtm).build();
+        taskFragment2.addChild(activity3, POSITION_TOP);
+
         verify(activity1.getSyncTransaction()).reparent(eq(startingWindow.mSurfaceControl),
                 eq(task.mSurfaceControl));
         assertEquals(activity1.mStartingData, startingWindow.mStartingData);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index 8474a36..77f884c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -1128,6 +1128,41 @@
         verify(activity).setDropInputMode(DropInputMode.NONE);
     }
 
+    /**
+     * We don't need to drop input for fully trusted embedding (system app, and embedding in the
+     * same app). This will allow users to do fast tapping.
+     */
+    @Test
+    public void testOverrideTaskFragmentAdapter_noInputProtectedForFullyTrustedAnimation() {
+        final Task task = createTask(mDisplayContent);
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
+        setupTaskFragmentRemoteAnimation(organizer, task.mTaskId, remoteAnimationRunner);
+
+        // Create a TaskFragment with only trusted embedded activity
+        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .createActivityCount(1)
+                .setOrganizer(organizer)
+                .build();
+        final ActivityRecord activity = taskFragment.getChildAt(0).asActivityRecord();
+        prepareActivityForAppTransition(activity);
+        final int uid = mAtm.mTaskFragmentOrganizerController.getTaskFragmentOrganizerUid(
+                getITaskFragmentOrganizer(organizer));
+        doReturn(true).when(task).isFullyTrustedEmbedding(uid);
+        spyOn(mDisplayContent.mAppTransition);
+
+        // Prepare and start transition.
+        prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
+
+        // The animation will be animated remotely by client, but input should not be dropped for
+        // fully trusted.
+        assertTrue(remoteAnimationRunner.isAnimationStarted());
+        verify(activity, never()).setDropInputForAnimation(true);
+        verify(activity, never()).setDropInputMode(DropInputMode.ALL);
+    }
+
     @Test
     public void testTransitionGoodToGoForTaskFragments() {
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
@@ -1197,8 +1232,7 @@
             TestRemoteAnimationRunner remoteAnimationRunner) {
         final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
                 remoteAnimationRunner, 10, 1);
-        final ITaskFragmentOrganizer iOrganizer =
-                ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder());
+        final ITaskFragmentOrganizer iOrganizer = getITaskFragmentOrganizer(organizer);
         final RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
         definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, adapter);
         definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, adapter);
@@ -1208,6 +1242,11 @@
                 definition);
     }
 
+    private static ITaskFragmentOrganizer getITaskFragmentOrganizer(
+            TaskFragmentOrganizer organizer) {
+        return ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder());
+    }
+
     private void prepareAndTriggerAppTransition(@Nullable ActivityRecord openingActivity,
             @Nullable ActivityRecord closingActivity, @Nullable TaskFragment changingTaskFragment) {
         if (openingActivity != null) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
index 3beb7f2..55a7c1b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
@@ -64,6 +64,11 @@
         }
 
         @Override
+        public SurfaceControl.Transaction getSyncTransaction() {
+            return mTransaction;
+        }
+
+        @Override
         public SurfaceControl.Transaction getPendingTransaction() {
             return mTransaction;
         }
@@ -102,6 +107,11 @@
         }
 
         @Override
+        public SurfaceControl.Transaction getSyncTransaction() {
+            return mHostTransaction;
+        }
+
+        @Override
         public SurfaceControl.Transaction getPendingTransaction() {
             return mHostTransaction;
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
index 6d02226..ffa21fa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsStateControllerTest.java
@@ -449,6 +449,36 @@
         assertNotNull(app.getInsetsState().peekSource(ITYPE_NAVIGATION_BAR));
     }
 
+    @UseTestDisplay(addWindows = W_INPUT_METHOD)
+    @Test
+    public void testGetInsetsHintForNewControl() {
+        final WindowState app1 = createTestWindow("app1");
+        final WindowState app2 = createTestWindow("app2");
+
+        makeWindowVisible(mImeWindow);
+        final InsetsSourceProvider imeInsetsProvider = getController().getSourceProvider(ITYPE_IME);
+        imeInsetsProvider.setWindowContainer(mImeWindow, null, null);
+        imeInsetsProvider.updateSourceFrame(mImeWindow.getFrame());
+
+        imeInsetsProvider.updateControlForTarget(app1, false);
+        imeInsetsProvider.onPostLayout();
+        final InsetsSourceControl control1 = imeInsetsProvider.getControl(app1);
+        assertNotNull(control1);
+        assertEquals(imeInsetsProvider.getSource().getFrame().height(),
+                control1.getInsetsHint().bottom);
+
+        // Simulate the IME control target updated from app1 to app2 when IME insets was invisible.
+        imeInsetsProvider.setServerVisible(false);
+        imeInsetsProvider.updateControlForTarget(app2, false);
+
+        // Verify insetsHint of the new control is same as last IME source frame after the layout.
+        imeInsetsProvider.onPostLayout();
+        final InsetsSourceControl control2 = imeInsetsProvider.getControl(app2);
+        assertNotNull(control2);
+        assertEquals(imeInsetsProvider.getSource().getFrame().height(),
+                control2.getInsetsHint().bottom);
+    }
+
     private WindowState createTestWindow(String name) {
         final WindowState win = createWindow(null, TYPE_APPLICATION, name);
         win.setHasSurface(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java
index 9fc9489..1d14dc3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LockTaskControllerTest.java
@@ -82,6 +82,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.telephony.CellBroadcastUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.LocalServices;
 import com.android.server.statusbar.StatusBarManagerInternal;
@@ -297,6 +298,22 @@
     }
 
     @Test
+    public void testLockTaskViolation_wirelessEmergencyAlerts() {
+        // GIVEN one task record with allowlisted auth that is in lock task mode
+        Task tr = getTask(LOCK_TASK_AUTH_ALLOWLISTED);
+        mLockTaskController.startLockTaskMode(tr, false, TEST_UID);
+
+        // GIVEN cellbroadcast task necessary for emergency warning alerts
+        Task cellbroadcastreceiver = getTask(
+                new Intent().setComponent(
+                        CellBroadcastUtils.getDefaultCellBroadcastAlertDialogComponent(mContext)),
+                LOCK_TASK_AUTH_PINNABLE);
+
+        // THEN the cellbroadcast task should all be allowed
+        assertFalse(mLockTaskController.isLockTaskModeViolation(cellbroadcastreceiver));
+    }
+
+    @Test
     public void testStopLockTaskMode() throws Exception {
         // GIVEN one task record with allowlisted auth that is in lock task mode
         Task tr = getTask(LOCK_TASK_AUTH_ALLOWLISTED);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
index 021568d..eba276f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
@@ -61,6 +61,7 @@
 import android.util.SparseBooleanArray;
 import android.view.IRecentsAnimationRunner;
 import android.view.SurfaceControl;
+import android.view.WindowManager.LayoutParams;
 import android.window.TaskSnapshot;
 
 import androidx.test.filters.SmallTest;
@@ -162,6 +163,30 @@
     }
 
     @Test
+    public void testLaunchAndStartRecents_expectTargetAndVisible() throws Exception {
+        mWm.setRecentsAnimationController(mController);
+        final ActivityRecord homeActivity = createHomeActivity();
+        final Task task = createTask(mDefaultDisplay);
+        // Emulate that activity1 has just launched activity2, but app transition has not yet been
+        // executed.
+        final ActivityRecord activity1 = createActivityRecord(task);
+        activity1.setVisible(true);
+        activity1.mVisibleRequested = false;
+        activity1.addWindow(createWindowState(new LayoutParams(TYPE_BASE_APPLICATION), activity1));
+
+        final ActivityRecord activity2 = createActivityRecord(task);
+        activity2.setVisible(false);
+        activity2.mVisibleRequested = true;
+
+        mDefaultDisplay.getConfiguration().windowConfiguration.setRotation(
+                mDefaultDisplay.getRotation());
+        initializeRecentsAnimationController(mController, homeActivity);
+        mController.startAnimation();
+        verify(mMockRunner, never()).onAnimationCanceled(null /* taskIds */,
+                null /* taskSnapshots */);
+    }
+
+    @Test
     public void testWallpaperIncluded_expectTarget() throws Exception {
         mWm.setRecentsAnimationController(mController);
         final ActivityRecord homeActivity = createHomeActivity();
diff --git a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
index 8ef9ada..700fadd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RootWindowContainerTests.java
@@ -1097,7 +1097,7 @@
         TaskChangeNotificationController controller = mAtm.getTaskChangeNotificationController();
         spyOn(controller);
         mWm.mRoot.lockAllProfileTasks(profileUserId);
-        verify(controller).notifyTaskProfileLocked(eq(task.mTaskId), eq(profileUserId));
+        verify(controller).notifyTaskProfileLocked(any());
 
         // Create the work lock activity on top of the task
         final ActivityRecord workLockActivity = new ActivityBuilder(mAtm).setTask(task).build();
@@ -1107,7 +1107,7 @@
         // Make sure the listener won't be notified again.
         clearInvocations(controller);
         mWm.mRoot.lockAllProfileTasks(profileUserId);
-        verify(controller, never()).notifyTaskProfileLocked(anyInt(), anyInt());
+        verify(controller, never()).notifyTaskProfileLocked(any());
     }
 
     /**
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 76fb7ff..35d8129 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -1876,6 +1876,28 @@
     }
 
     @Test
+    public void testResizableFixedOrientationAppInSplitScreen_letterboxForDifferentOrientation() {
+        setUpDisplaySizeWithApp(1000, 2800);
+        final TestSplitOrganizer organizer =
+                new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+
+        // Resizable landscape-only activity.
+        prepareLimitedBounds(mActivity, SCREEN_ORIENTATION_LANDSCAPE, /* isUnresizable= */ false);
+
+        final Rect originalBounds = new Rect(mActivity.getBounds());
+
+        // Move activity to split screen which takes half of the screen.
+        mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test");
+        organizer.mPrimary.setBounds(0, 0, 1000, 1400);
+        assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode());
+        assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
+
+        // Resizable activity is not in size compat mode but in the letterbox for fixed orientation.
+        assertFitted();
+        assertTrue(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+    }
+
+    @Test
     public void testSupportsNonResizableInSplitScreen_fillTaskForSameOrientation() {
         // Support non resizable in multi window
         mAtm.mDevEnableNonResizableMultiWindow = true;
diff --git a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
index f5d915d..8425844 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java
@@ -90,13 +90,50 @@
     public void packageFromDeviceConfigIgnored() {
         setExceptionListAndWaitForCallback("com.test.nosplashscreen1,com.test.nosplashscreen2");
 
-        assertIsException("com.test.nosplashscreen1", null);
-        assertIsException("com.test.nosplashscreen2", null);
+        // In list, up to T included
+        assertIsException("com.test.nosplashscreen1", VERSION_CODES.R);
+        assertIsException("com.test.nosplashscreen1", VERSION_CODES.S);
+        assertIsException("com.test.nosplashscreen1", VERSION_CODES.TIRAMISU);
 
-        assertIsNotException("com.test.nosplashscreen1", VERSION_CODES.S, null);
-        assertIsNotException("com.test.nosplashscreen2", VERSION_CODES.S, null);
-        assertIsNotException("com.test.splashscreen", VERSION_CODES.S, null);
-        assertIsNotException("com.test.splashscreen", VERSION_CODES.R, null);
+        // In list, after T
+        assertIsNotException("com.test.nosplashscreen2", VERSION_CODES.TIRAMISU + 1);
+        assertIsNotException("com.test.nosplashscreen2", VERSION_CODES.CUR_DEVELOPMENT);
+
+        // Not in list, up to T included
+        assertIsNotException("com.test.splashscreen", VERSION_CODES.S);
+        assertIsNotException("com.test.splashscreen", VERSION_CODES.R);
+        assertIsNotException("com.test.splashscreen", VERSION_CODES.TIRAMISU);
+    }
+
+    @Test
+    public void metaDataOptOut() {
+        String packageName = "com.test.nosplashscreen_opt_out";
+        setExceptionListAndWaitForCallback(packageName);
+
+        Bundle metaData = new Bundle();
+        ApplicationInfo activityInfo = new ApplicationInfo();
+        activityInfo.metaData = metaData;
+
+        // No Exceptions
+        metaData.putBoolean("android.splashscreen.exception_opt_out", true);
+        assertIsNotException(packageName, VERSION_CODES.R, activityInfo);
+        assertIsNotException(packageName, VERSION_CODES.S, activityInfo);
+        assertIsNotException(packageName, VERSION_CODES.TIRAMISU, activityInfo);
+
+        // Exception up to T
+        metaData.putBoolean("android.splashscreen.exception_opt_out", false);
+        assertIsException(packageName, VERSION_CODES.R, activityInfo);
+        assertIsException(packageName, VERSION_CODES.S, activityInfo);
+        assertIsException(packageName, VERSION_CODES.TIRAMISU, activityInfo);
+
+        // No Exception after T
+        assertIsNotException(packageName, VERSION_CODES.TIRAMISU + 1, activityInfo);
+        assertIsNotException(packageName, VERSION_CODES.CUR_DEVELOPMENT, activityInfo);
+
+        // Edge Cases
+        activityInfo.metaData = null;
+        assertIsException(packageName, VERSION_CODES.R, activityInfo);
+        assertIsException(packageName, VERSION_CODES.R);
     }
 
     private void setExceptionListAndWaitForCallback(String commaSeparatedList) {
@@ -116,42 +153,25 @@
         }
     }
 
-    @Test
-    public void metaDataOptOut() {
-        String packageName = "com.test.nosplashscreen_opt_out";
-        setExceptionListAndWaitForCallback(packageName);
-
-        Bundle metaData = new Bundle();
-        ApplicationInfo activityInfo = new ApplicationInfo();
-        activityInfo.metaData = metaData;
-
-        // No Exceptions
-        metaData.putBoolean("android.splashscreen.exception_opt_out", true);
-        assertIsNotException(packageName, VERSION_CODES.R, activityInfo);
-        assertIsNotException(packageName, VERSION_CODES.S, activityInfo);
-
-        // Exception Pre S
-        metaData.putBoolean("android.splashscreen.exception_opt_out", false);
-        assertIsException(packageName, activityInfo);
-        assertIsNotException(packageName, VERSION_CODES.S, activityInfo);
-
-        // Edge Cases
-        activityInfo.metaData = null;
-        assertIsException(packageName, activityInfo);
-        assertIsException(packageName, null);
+    private void assertIsNotException(String packageName, int targetSdk) {
+        assertIsNotException(packageName, targetSdk, null);
     }
 
     private void assertIsNotException(String packageName, int targetSdk,
             ApplicationInfo activityInfo) {
         assertFalse(String.format("%s (sdk=%d) should have not been considered as an exception",
-                packageName, targetSdk),
+                        packageName, targetSdk),
                 mList.isException(packageName, targetSdk, () -> activityInfo));
     }
 
+    private void assertIsException(String packageName, int targetSdk) {
+        assertIsException(packageName, targetSdk, null);
+    }
+
     private void assertIsException(String packageName,
-            ApplicationInfo activityInfo) {
+            int targetSdk, ApplicationInfo activityInfo) {
         assertTrue(String.format("%s (sdk=%d) should have been considered as an exception",
-                packageName, VERSION_CODES.R),
-                mList.isException(packageName, VERSION_CODES.R, () -> activityInfo));
+                        packageName, targetSdk),
+                mList.isException(packageName, targetSdk, () -> activityInfo));
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
index 79ba175..ff0063c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
@@ -338,6 +338,11 @@
         }
 
         @Override
+        public SurfaceControl.Transaction getSyncTransaction() {
+            return mTransaction;
+        }
+
+        @Override
         public Transaction getPendingTransaction() {
             return mTransaction;
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 75b5e73..5340a79 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -511,11 +511,12 @@
     @Test
     public void testApplyTransaction_reparentActivityToTaskFragment_triggerLifecycleUpdate()
             throws RemoteException {
-        final ActivityRecord activity = createActivityRecord(mDefaultDisplay);
+        final Task task = createTask(mDisplayContent);
+        final ActivityRecord activity = createActivityRecord(task);
         mOrganizer.applyTransaction(mTransaction);
         mController.registerOrganizer(mIOrganizer);
         mTaskFragment = new TaskFragmentBuilder(mAtm)
-                .setCreateParentTask()
+                .setParentTask(task)
                 .setFragmentToken(mFragmentToken)
                 .build();
         mAtm.mWindowOrganizerController.mLaunchTaskFragments
diff --git a/services/usage/OWNERS b/services/usage/OWNERS
index 6c20e74..26d9b10 100644
--- a/services/usage/OWNERS
+++ b/services/usage/OWNERS
@@ -3,4 +3,5 @@
 huiyu@google.com
 yamasani@google.com
 
-per-file *StorageStats* = file:/core/java/android/os/storage/OWNERS
\ No newline at end of file
+per-file *StorageStats* = file:/core/java/android/os/storage/OWNERS
+per-file *Broadcast* = sudheersai@google.com
\ No newline at end of file
diff --git a/services/usage/java/com/android/server/usage/BroadcastEvent.java b/services/usage/java/com/android/server/usage/BroadcastEvent.java
index ceb79c1..e56a541 100644
--- a/services/usage/java/com/android/server/usage/BroadcastEvent.java
+++ b/services/usage/java/com/android/server/usage/BroadcastEvent.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.util.LongArrayQueue;
 
 import java.util.Objects;
 
@@ -30,6 +31,7 @@
     private String mTargetPackage;
     private int mTargetUserId;
     private long mIdForResponseEvent;
+    private final LongArrayQueue mTimestampsMs;
 
     BroadcastEvent(int sourceUid, @NonNull String targetPackage, @UserIdInt int targetUserId,
             long idForResponseEvent) {
@@ -37,6 +39,7 @@
         mTargetPackage = targetPackage;
         mTargetUserId = targetUserId;
         mIdForResponseEvent = idForResponseEvent;
+        mTimestampsMs = new LongArrayQueue();
     }
 
     public int getSourceUid() {
@@ -55,6 +58,14 @@
         return mIdForResponseEvent;
     }
 
+    public LongArrayQueue getTimestampsMs() {
+        return mTimestampsMs;
+    }
+
+    public void addTimestampMs(long timestampMs) {
+        mTimestampsMs.addLast(timestampMs);
+    }
+
     @Override
     public boolean equals(@Nullable Object obj) {
         if (this == obj) {
diff --git a/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java b/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java
index 76d2fe7..7e3990d 100644
--- a/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java
+++ b/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java
@@ -24,8 +24,10 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManager.ProcessState;
 import android.app.usage.BroadcastResponseStats;
+import android.os.SystemClock;
 import android.os.UserHandle;
-import android.util.LongSparseArray;
+import android.util.ArraySet;
+import android.util.LongArrayQueue;
 import android.util.Slog;
 import android.util.SparseArray;
 
@@ -89,14 +91,14 @@
             return;
         }
         synchronized (mLock) {
-            final LongSparseArray<BroadcastEvent> broadcastEvents =
+            final ArraySet<BroadcastEvent> broadcastEvents =
                     getOrCreateBroadcastEventsLocked(targetPackage, targetUser);
-            final BroadcastEvent broadcastEvent = new BroadcastEvent(
+            final BroadcastEvent broadcastEvent = getOrCreateBroadcastEvent(broadcastEvents,
                     sourceUid, targetPackage, targetUser.getIdentifier(), idForResponseEvent);
-            broadcastEvents.append(timestampMs, broadcastEvent);
-            final BroadcastResponseStats responseStats =
-                    getOrCreateBroadcastResponseStats(broadcastEvent);
-            responseStats.incrementBroadcastsDispatchedCount(1);
+            broadcastEvent.addTimestampMs(timestampMs);
+
+            // Delete any old broadcast event related data so that we don't keep accumulating them.
+            recordAndPruneOldBroadcastDispatchTimestamps(broadcastEvent);
         }
     }
 
@@ -120,47 +122,90 @@
             @NonNull String packageName, UserHandle user, @ElapsedRealtimeLong long timestampMs) {
         mLogger.logNotificationEvent(event, packageName, user, timestampMs);
         synchronized (mLock) {
-            final LongSparseArray<BroadcastEvent> broadcastEvents =
+            final ArraySet<BroadcastEvent> broadcastEvents =
                     getBroadcastEventsLocked(packageName, user);
             if (broadcastEvents == null) {
                 return;
             }
-            // TODO (206518114): Add LongSparseArray.removeAtRange()
+            final long broadcastResponseWindowDurationMs =
+                    mAppStandby.getBroadcastResponseWindowDurationMs();
+            final long broadcastsSessionWithResponseDurationMs =
+                    mAppStandby.getBroadcastSessionsWithResponseDurationMs();
+            final boolean recordAllBroadcastsSessionsWithinResponseWindow =
+                    mAppStandby.shouldNoteResponseEventForAllBroadcastSessions();
             for (int i = broadcastEvents.size() - 1; i >= 0; --i) {
-                final long dispatchTimestampMs = broadcastEvents.keyAt(i);
-                final long elapsedDurationMs = timestampMs - dispatchTimestampMs;
-                if (elapsedDurationMs <= 0) {
-                    continue;
-                }
-                if (dispatchTimestampMs >= timestampMs) {
-                    continue;
-                }
-                if (elapsedDurationMs <= mAppStandby.getBroadcastResponseWindowDurationMs()) {
-                    final BroadcastEvent broadcastEvent = broadcastEvents.valueAt(i);
-                    final BroadcastResponseStats responseStats =
-                            getBroadcastResponseStats(broadcastEvent);
-                    if (responseStats == null) {
-                        continue;
+                final BroadcastEvent broadcastEvent = broadcastEvents.valueAt(i);
+                recordAndPruneOldBroadcastDispatchTimestamps(broadcastEvent);
+
+                final LongArrayQueue dispatchTimestampsMs = broadcastEvent.getTimestampsMs();
+                long broadcastsSessionEndTimestampMs = 0;
+                // We only need to look at the broadcast events that occurred before
+                // this notification related event.
+                while (dispatchTimestampsMs.size() > 0
+                        && dispatchTimestampsMs.peekFirst() < timestampMs) {
+                    final long dispatchTimestampMs = dispatchTimestampsMs.peekFirst();
+                    final long elapsedDurationMs = timestampMs - dispatchTimestampMs;
+                    // Only increment the counts if the broadcast was sent not too long ago, as
+                    // decided by 'broadcastResponseWindowDurationMs' and is part of a new session.
+                    // That is, it occurred 'broadcastsSessionWithResponseDurationMs' after the
+                    // previously handled broadcast event which is represented by
+                    // 'broadcastsSessionEndTimestampMs'.
+                    if (elapsedDurationMs <= broadcastResponseWindowDurationMs
+                            && dispatchTimestampMs >= broadcastsSessionEndTimestampMs) {
+                        if (broadcastsSessionEndTimestampMs != 0
+                                && !recordAllBroadcastsSessionsWithinResponseWindow) {
+                            break;
+                        }
+                        final BroadcastResponseStats responseStats =
+                                getOrCreateBroadcastResponseStats(broadcastEvent);
+                        responseStats.incrementBroadcastsDispatchedCount(1);
+                        broadcastsSessionEndTimestampMs = dispatchTimestampMs
+                                + broadcastsSessionWithResponseDurationMs;
+                        switch (event) {
+                            case NOTIFICATION_EVENT_TYPE_POSTED:
+                                responseStats.incrementNotificationsPostedCount(1);
+                                break;
+                            case NOTIFICATION_EVENT_TYPE_UPDATED:
+                                responseStats.incrementNotificationsUpdatedCount(1);
+                                break;
+                            case NOTIFICATION_EVENT_TYPE_CANCELLED:
+                                responseStats.incrementNotificationsCancelledCount(1);
+                                break;
+                            default:
+                                Slog.wtf(TAG, "Unknown event: " + event);
+                        }
                     }
-                    switch (event) {
-                        case NOTIFICATION_EVENT_TYPE_POSTED:
-                            responseStats.incrementNotificationsPostedCount(1);
-                            break;
-                        case NOTIFICATION_EVENT_TYPE_UPDATED:
-                            responseStats.incrementNotificationsUpdatedCount(1);
-                            break;
-                        case NOTIFICATION_EVENT_TYPE_CANCELLED:
-                            responseStats.incrementNotificationsCancelledCount(1);
-                            break;
-                        default:
-                            Slog.wtf(TAG, "Unknown event: " + event);
-                    }
+                    dispatchTimestampsMs.removeFirst();
                 }
-                broadcastEvents.removeAt(i);
+                if (dispatchTimestampsMs.size() == 0) {
+                    broadcastEvents.removeAt(i);
+                }
             }
         }
     }
 
+    @GuardedBy("mLock")
+    private void recordAndPruneOldBroadcastDispatchTimestamps(BroadcastEvent broadcastEvent) {
+        final LongArrayQueue timestampsMs = broadcastEvent.getTimestampsMs();
+        final long broadcastResponseWindowDurationMs =
+                mAppStandby.getBroadcastResponseWindowDurationMs();
+        final long broadcastsSessionDurationMs =
+                mAppStandby.getBroadcastSessionsDurationMs();
+        final long nowElapsedMs = SystemClock.elapsedRealtime();
+        long broadcastsSessionEndTimestampMs = 0;
+        while (timestampsMs.size() > 0
+                && timestampsMs.peekFirst() < (nowElapsedMs - broadcastResponseWindowDurationMs)) {
+            final long eventTimestampMs = timestampsMs.peekFirst();
+            if (eventTimestampMs >= broadcastsSessionEndTimestampMs) {
+                final BroadcastResponseStats responseStats =
+                        getOrCreateBroadcastResponseStats(broadcastEvent);
+                responseStats.incrementBroadcastsDispatchedCount(1);
+                broadcastsSessionEndTimestampMs = eventTimestampMs + broadcastsSessionDurationMs;
+            }
+            timestampsMs.removeFirst();
+        }
+    }
+
     @NonNull List<BroadcastResponseStats> queryBroadcastResponseStats(int callingUid,
             @Nullable String packageName, @IntRange(from = 0) long id, @UserIdInt int userId) {
         final List<BroadcastResponseStats> broadcastResponseStatsList = new ArrayList<>();
@@ -247,7 +292,7 @@
 
     @GuardedBy("mLock")
     @Nullable
-    private LongSparseArray<BroadcastEvent> getBroadcastEventsLocked(
+    private ArraySet<BroadcastEvent> getBroadcastEventsLocked(
             @NonNull String packageName, UserHandle user) {
         final UserBroadcastEvents userBroadcastEvents = mUserBroadcastEvents.get(
                 user.getIdentifier());
@@ -259,7 +304,7 @@
 
     @GuardedBy("mLock")
     @NonNull
-    private LongSparseArray<BroadcastEvent> getOrCreateBroadcastEventsLocked(
+    private ArraySet<BroadcastEvent> getOrCreateBroadcastEventsLocked(
             @NonNull String packageName, UserHandle user) {
         UserBroadcastEvents userBroadcastEvents = mUserBroadcastEvents.get(user.getIdentifier());
         if (userBroadcastEvents == null) {
@@ -272,16 +317,6 @@
     @GuardedBy("mLock")
     @Nullable
     private BroadcastResponseStats getBroadcastResponseStats(
-            @NonNull BroadcastEvent broadcastEvent) {
-        final int sourceUid = broadcastEvent.getSourceUid();
-        final SparseArray<UserBroadcastResponseStats> responseStatsForUid =
-                mUserResponseStats.get(sourceUid);
-        return getBroadcastResponseStats(responseStatsForUid, broadcastEvent);
-    }
-
-    @GuardedBy("mLock")
-    @Nullable
-    private BroadcastResponseStats getBroadcastResponseStats(
             @Nullable SparseArray<UserBroadcastResponseStats> responseStatsForUid,
             @NonNull BroadcastEvent broadcastEvent) {
         if (responseStatsForUid == null) {
@@ -315,6 +350,20 @@
         return userResponseStats.getOrCreateBroadcastResponseStats(broadcastEvent);
     }
 
+    private static BroadcastEvent getOrCreateBroadcastEvent(
+            ArraySet<BroadcastEvent> broadcastEvents,
+            int sourceUid, String targetPackage, int targetUserId, long idForResponseEvent) {
+        final BroadcastEvent broadcastEvent = new BroadcastEvent(
+                sourceUid, targetPackage, targetUserId, idForResponseEvent);
+        final int index = broadcastEvents.indexOf(broadcastEvent);
+        if (index >= 0) {
+            return broadcastEvents.valueAt(index);
+        } else {
+            broadcastEvents.add(broadcastEvent);
+            return broadcastEvent;
+        }
+    }
+
     void dump(@NonNull IndentingPrintWriter ipw) {
         ipw.println("Broadcast response stats:");
         ipw.increaseIndent();
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 6f89bb2..078177b 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -2768,18 +2768,9 @@
                 throw new IllegalArgumentException("id needs to be >=0");
             }
 
-            final int result = getContext().checkCallingOrSelfPermission(
-                    android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS);
-            // STOPSHIP (206518114): Temporarily check for PACKAGE_USAGE_STATS permission as well
-            // until the clients switch to using the new permission.
-            if (result != PackageManager.PERMISSION_GRANTED) {
-                if (!hasPermission(callingPackage)) {
-                    throw new SecurityException(
-                            "Caller does not have the permission needed to call this API; "
-                                    + "callingPackage=" + callingPackage
-                                    + ", callingUid=" + Binder.getCallingUid());
-                }
-            }
+            getContext().enforceCallingOrSelfPermission(
+                    android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS,
+                    "queryBroadcastResponseStats");
             final int callingUid = Binder.getCallingUid();
             userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), callingUid,
                     userId, false /* allowAll */, false /* requireFull */,
@@ -2801,18 +2792,9 @@
             }
 
 
-            final int result = getContext().checkCallingOrSelfPermission(
-                    android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS);
-            // STOPSHIP (206518114): Temporarily check for PACKAGE_USAGE_STATS permission as well
-            // until the clients switch to using the new permission.
-            if (result != PackageManager.PERMISSION_GRANTED) {
-                if (!hasPermission(callingPackage)) {
-                    throw new SecurityException(
-                            "Caller does not have the permission needed to call this API; "
-                                    + "callingPackage=" + callingPackage
-                                    + ", callingUid=" + Binder.getCallingUid());
-                }
-            }
+            getContext().enforceCallingOrSelfPermission(
+                    android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS,
+                    "clearBroadcastResponseStats");
             final int callingUid = Binder.getCallingUid();
             userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), callingUid,
                     userId, false /* allowAll */, false /* requireFull */,
@@ -2825,18 +2807,9 @@
         public void clearBroadcastEvents(@NonNull String callingPackage, @UserIdInt int userId) {
             Objects.requireNonNull(callingPackage);
 
-            final int result = getContext().checkCallingOrSelfPermission(
-                    android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS);
-            // STOPSHIP (206518114): Temporarily check for PACKAGE_USAGE_STATS permission as well
-            // until the clients switch to using the new permission.
-            if (result != PackageManager.PERMISSION_GRANTED) {
-                if (!hasPermission(callingPackage)) {
-                    throw new SecurityException(
-                            "Caller does not have the permission needed to call this API; "
-                                    + "callingPackage=" + callingPackage
-                                    + ", callingUid=" + Binder.getCallingUid());
-                }
-            }
+            getContext().enforceCallingOrSelfPermission(
+                    android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS,
+                    "clearBroadcastEvents");
             final int callingUid = Binder.getCallingUid();
             userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), callingUid,
                     userId, false /* allowAll */, false /* requireFull */,
diff --git a/services/usage/java/com/android/server/usage/UserBroadcastEvents.java b/services/usage/java/com/android/server/usage/UserBroadcastEvents.java
index 0ec59c3..fcef010 100644
--- a/services/usage/java/com/android/server/usage/UserBroadcastEvents.java
+++ b/services/usage/java/com/android/server/usage/UserBroadcastEvents.java
@@ -19,7 +19,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.util.ArrayMap;
-import android.util.LongSparseArray;
+import android.util.ArraySet;
+import android.util.LongArrayQueue;
 import android.util.TimeUtils;
 
 import com.android.internal.util.IndentingPrintWriter;
@@ -30,17 +31,17 @@
      * Here targetPackage refers to the package receiving the broadcast and BroadcastEvent objects
      * corresponding to each broadcast it is receiving.
      */
-    private ArrayMap<String, LongSparseArray<BroadcastEvent>> mBroadcastEvents = new ArrayMap();
+    private ArrayMap<String, ArraySet<BroadcastEvent>> mBroadcastEvents = new ArrayMap();
 
-    @Nullable LongSparseArray<BroadcastEvent> getBroadcastEvents(@NonNull String packageName) {
+    @Nullable ArraySet<BroadcastEvent> getBroadcastEvents(@NonNull String packageName) {
         return mBroadcastEvents.get(packageName);
     }
 
-    @NonNull LongSparseArray<BroadcastEvent> getOrCreateBroadcastEvents(
+    @NonNull ArraySet<BroadcastEvent> getOrCreateBroadcastEvents(
             @NonNull String packageName) {
-        LongSparseArray<BroadcastEvent> broadcastEvents = mBroadcastEvents.get(packageName);
+        ArraySet<BroadcastEvent> broadcastEvents = mBroadcastEvents.get(packageName);
         if (broadcastEvents == null) {
-            broadcastEvents = new LongSparseArray<>();
+            broadcastEvents = new ArraySet<>();
             mBroadcastEvents.put(packageName, broadcastEvents);
         }
         return broadcastEvents;
@@ -56,7 +57,7 @@
 
     void clear(int uid) {
         for (int i = mBroadcastEvents.size() - 1; i >= 0; --i) {
-            final LongSparseArray<BroadcastEvent> broadcastEvents = mBroadcastEvents.valueAt(i);
+            final ArraySet<BroadcastEvent> broadcastEvents = mBroadcastEvents.valueAt(i);
             for (int j = broadcastEvents.size() - 1; j >= 0; --j) {
                 if (broadcastEvents.valueAt(j).getSourceUid() == uid) {
                     broadcastEvents.removeAt(j);
@@ -68,18 +69,26 @@
     void dump(@NonNull IndentingPrintWriter ipw) {
         for (int i = 0; i < mBroadcastEvents.size(); ++i) {
             final String packageName = mBroadcastEvents.keyAt(i);
-            final LongSparseArray<BroadcastEvent> broadcastEvents = mBroadcastEvents.valueAt(i);
+            final ArraySet<BroadcastEvent> broadcastEvents = mBroadcastEvents.valueAt(i);
             ipw.println(packageName + ":");
             ipw.increaseIndent();
             if (broadcastEvents.size() == 0) {
                 ipw.println("<empty>");
             } else {
                 for (int j = 0; j < broadcastEvents.size(); ++j) {
-                    final long timestampMs = broadcastEvents.keyAt(j);
                     final BroadcastEvent broadcastEvent = broadcastEvents.valueAt(j);
-                    TimeUtils.formatDuration(timestampMs, ipw);
-                    ipw.print(": ");
                     ipw.println(broadcastEvent);
+                    ipw.increaseIndent();
+                    final LongArrayQueue timestampsMs = broadcastEvent.getTimestampsMs();
+                    for (int timestampIdx = 0; timestampIdx < timestampsMs.size(); ++timestampIdx) {
+                        if (timestampIdx > 0) {
+                            ipw.print(',');
+                        }
+                        final long timestampMs = timestampsMs.get(timestampIdx);
+                        TimeUtils.formatDuration(timestampMs, ipw);
+                    }
+                    ipw.println();
+                    ipw.decreaseIndent();
                 }
             }
             ipw.decreaseIndent();
diff --git a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
index 2893f80..dc96c66 100644
--- a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
+++ b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
@@ -89,38 +89,36 @@
 
     private final Object mLock = new Object();
     private boolean mIsOpen;
+    private boolean mServerAvailable;
 
     private UsbMidiPacketConverter mUsbMidiPacketConverter;
 
     private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() {
-
         @Override
         public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
             MidiDeviceInfo deviceInfo = status.getDeviceInfo();
             int numInputPorts = deviceInfo.getInputPortCount();
             int numOutputPorts = deviceInfo.getOutputPortCount();
-            boolean hasOpenPorts = false;
+            int numOpenPorts = 0;
 
             for (int i = 0; i < numInputPorts; i++) {
                 if (status.isInputPortOpen(i)) {
-                    hasOpenPorts = true;
-                    break;
+                    numOpenPorts++;
                 }
             }
 
-            if (!hasOpenPorts) {
-                for (int i = 0; i < numOutputPorts; i++) {
-                    if (status.getOutputPortOpenCount(i) > 0) {
-                        hasOpenPorts = true;
-                        break;
-                    }
+            for (int i = 0; i < numOutputPorts; i++) {
+                if (status.getOutputPortOpenCount(i) > 0) {
+                    numOpenPorts += status.getOutputPortOpenCount(i);
                 }
             }
 
             synchronized (mLock) {
-                if (hasOpenPorts && !mIsOpen) {
+                Log.d(TAG, "numOpenPorts: " + numOpenPorts + " isOpen: " + mIsOpen
+                        + " mServerAvailable: " + mServerAvailable);
+                if ((numOpenPorts > 0) && !mIsOpen && mServerAvailable) {
                     openLocked();
-                } else if (!hasOpenPorts && mIsOpen) {
+                } else if ((numOpenPorts == 0) && mIsOpen) {
                     closeLocked();
                 }
             }
@@ -348,7 +346,7 @@
                                 final UsbRequest response = connectionFinal.requestWait();
                                 if (response != request) {
                                     Log.w(TAG, "Unexpected response");
-                                    continue;
+                                    break;
                                 }
                                 int bytesRead = byteBuffer.position();
 
@@ -462,7 +460,7 @@
         mContext = context;
         MidiManager midiManager = context.getSystemService(MidiManager.class);
         if (midiManager == null) {
-            Log.e(TAG, "No MidiManager in UsbDirectMidiDevice.create()");
+            Log.e(TAG, "No MidiManager in UsbDirectMidiDevice.register()");
             return false;
         }
 
@@ -499,6 +497,7 @@
                 mUsbDevice.getSerialNumber());
         properties.putParcelable(MidiDeviceInfo.PROPERTY_USB_DEVICE, mUsbDevice);
 
+        mServerAvailable = true;
         mServer = midiManager.createDeviceServer(mMidiInputPortReceivers, mNumInputs,
                 null, null, properties, MidiDeviceInfo.TYPE_USB, mDefaultMidiProtocol, mCallback);
         if (mServer == null) {
@@ -514,6 +513,7 @@
             if (mIsOpen) {
                 closeLocked();
             }
+            mServerAvailable = false;
         }
 
         if (mServer != null) {
diff --git a/services/usb/java/com/android/server/usb/UsbMidiDevice.java b/services/usb/java/com/android/server/usb/UsbMidiDevice.java
index 275f217..67955e1 100644
--- a/services/usb/java/com/android/server/usb/UsbMidiDevice.java
+++ b/services/usb/java/com/android/server/usb/UsbMidiDevice.java
@@ -71,40 +71,38 @@
 
     private final Object mLock = new Object();
     private boolean mIsOpen;
+    private boolean mServerAvailable;
 
     // pipe file descriptor for signalling input thread to exit
     // only accessed from JNI code
     private int mPipeFD = -1;
 
     private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() {
-
         @Override
         public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
             MidiDeviceInfo deviceInfo = status.getDeviceInfo();
-            int inputPorts = deviceInfo.getInputPortCount();
-            int outputPorts = deviceInfo.getOutputPortCount();
-            boolean hasOpenPorts = false;
+            int numInputPorts = deviceInfo.getInputPortCount();
+            int numOutputPorts = deviceInfo.getOutputPortCount();
+            int numOpenPorts = 0;
 
-            for (int i = 0; i < inputPorts; i++) {
+            for (int i = 0; i < numInputPorts; i++) {
                 if (status.isInputPortOpen(i)) {
-                    hasOpenPorts = true;
-                    break;
+                    numOpenPorts++;
                 }
             }
 
-            if (!hasOpenPorts) {
-                for (int i = 0; i < outputPorts; i++) {
-                    if (status.getOutputPortOpenCount(i) > 0) {
-                        hasOpenPorts = true;
-                        break;
-                    }
+            for (int i = 0; i < numOutputPorts; i++) {
+                if (status.getOutputPortOpenCount(i) > 0) {
+                    numOpenPorts += status.getOutputPortOpenCount(i);
                 }
             }
 
             synchronized (mLock) {
-                if (hasOpenPorts && !mIsOpen) {
+                Log.d(TAG, "numOpenPorts: " + numOpenPorts + " isOpen: " + mIsOpen
+                        + " mServerAvailable: " + mServerAvailable);
+                if ((numOpenPorts > 0) && !mIsOpen && mServerAvailable) {
                     openLocked();
-                } else if (!hasOpenPorts && mIsOpen) {
+                } else if ((numOpenPorts == 0) && mIsOpen) {
                     closeLocked();
                 }
             }
@@ -298,10 +296,11 @@
     private boolean register(Context context, Bundle properties) {
         MidiManager midiManager = (MidiManager)context.getSystemService(Context.MIDI_SERVICE);
         if (midiManager == null) {
-            Log.e(TAG, "No MidiManager in UsbMidiDevice.create()");
+            Log.e(TAG, "No MidiManager in UsbMidiDevice.register()");
             return false;
         }
 
+        mServerAvailable = true;
         mServer = midiManager.createDeviceServer(mMidiInputPortReceivers, mNumInputs,
                 null, null, properties, MidiDeviceInfo.TYPE_USB,
                 MidiDeviceInfo.PROTOCOL_UNKNOWN, mCallback);
@@ -318,6 +317,7 @@
             if (mIsOpen) {
                 closeLocked();
             }
+            mServerAvailable = false;
         }
 
         if (mServer != null) {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 2cb3982..22e0d08 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -124,6 +124,10 @@
     private static final long RESET_DEBUG_HOTWORD_LOGGING_TIMEOUT_MILLIS = 60 * 60 * 1000; // 1 hour
     private static final int MAX_ISOLATED_PROCESS_NUMBER = 10;
 
+    // The error codes are used for onError callback
+    private static final int HOTWORD_DETECTION_SERVICE_DIED = -1;
+    private static final int CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION = -2;
+
     // Hotword metrics
     private static final int METRICS_INIT_UNKNOWN_TIMEOUT =
             HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_UNKNOWN_TIMEOUT;
@@ -420,19 +424,24 @@
                     Slog.d(TAG, "onDetected");
                 }
                 synchronized (mLock) {
-                    if (mPerformingSoftwareHotwordDetection) {
-                        enforcePermissionsForDataDelivery();
-                        mSoftwareCallback.onDetected(result, null, null);
-                        mPerformingSoftwareHotwordDetection = false;
-                        if (result != null) {
-                            Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result)
-                                    + " bits from hotword trusted process");
-                            if (mDebugHotwordLogging) {
-                                Slog.i(TAG, "Egressed detected result: " + result);
-                            }
-                        }
-                    } else {
+                    if (!mPerformingSoftwareHotwordDetection) {
                         Slog.i(TAG, "Hotword detection has already completed");
+                        return;
+                    }
+                    mPerformingSoftwareHotwordDetection = false;
+                    try {
+                        enforcePermissionsForDataDelivery();
+                    } catch (SecurityException e) {
+                        mSoftwareCallback.onError();
+                        return;
+                    }
+                    mSoftwareCallback.onDetected(result, null, null);
+                    if (result != null) {
+                        Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result)
+                                + " bits from hotword trusted process");
+                        if (mDebugHotwordLogging) {
+                            Slog.i(TAG, "Egressed detected result: " + result);
+                        }
                     }
                 }
             }
@@ -513,19 +522,24 @@
             public void onDetected(HotwordDetectedResult result) throws RemoteException {
                 Slog.v(TAG, "onDetected");
                 synchronized (mLock) {
-                    if (mValidatingDspTrigger) {
-                        mValidatingDspTrigger = false;
-                        enforcePermissionsForDataDelivery();
-                        externalCallback.onKeyphraseDetected(recognitionEvent, result);
-                        if (result != null) {
-                            Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result)
-                                    + " bits from hotword trusted process");
-                            if (mDebugHotwordLogging) {
-                                Slog.i(TAG, "Egressed detected result: " + result);
-                            }
-                        }
-                    } else {
+                    if (!mValidatingDspTrigger) {
                         Slog.i(TAG, "Ignored hotword detected since trigger has been handled");
+                        return;
+                    }
+                    mValidatingDspTrigger = false;
+                    try {
+                        enforcePermissionsForDataDelivery();
+                    } catch (SecurityException e) {
+                        externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION);
+                        return;
+                    }
+                    externalCallback.onKeyphraseDetected(recognitionEvent, result);
+                    if (result != null) {
+                        Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(result)
+                                + " bits from hotword trusted process");
+                        if (mDebugHotwordLogging) {
+                            Slog.i(TAG, "Egressed detected result: " + result);
+                        }
                     }
                 }
             }
@@ -595,7 +609,8 @@
                         HotwordMetricsLogger.writeKeyphraseTriggerEvent(
                                 mDetectorType,
                                 METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION);
-                        throw e;
+                        externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION);
+                        return;
                     }
                     externalCallback.onKeyphraseDetected(recognitionEvent, result);
                     if (result != null) {
@@ -885,7 +900,13 @@
                                     throws RemoteException {
                                 bestEffortClose(serviceAudioSink);
                                 bestEffortClose(serviceAudioSource);
-                                enforcePermissionsForDataDelivery();
+                                try {
+                                    enforcePermissionsForDataDelivery();
+                                } catch (SecurityException e) {
+                                    bestEffortClose(audioSource);
+                                    callback.onError();
+                                    return;
+                                }
                                 callback.onDetected(triggerResult, null /* audioFormat */,
                                         null /* audioStream */);
                                 if (triggerResult != null) {
@@ -984,7 +1005,7 @@
 
                 Slog.w(TAG, "binderDied");
                 try {
-                    mCallback.onError(-1);
+                    mCallback.onError(HOTWORD_DETECTION_SERVICE_DIED);
                 } catch (RemoteException e) {
                     Slog.w(TAG, "Failed to report onError status: " + e);
                 }
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index 980ea5c..432af3a 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -2895,7 +2895,19 @@
             if (key != null) {
                 final Object value = bundle.get(key);
                 final Object newValue = newBundle.get(key);
-                if (!Objects.equals(value, newValue)) {
+                if (!newBundle.containsKey(key)) {
+                    return false;
+                }
+                if (value instanceof Bundle && newValue instanceof Bundle) {
+                    if (!areBundlesEqual((Bundle) value, (Bundle) newValue)) {
+                        return false;
+                    }
+                }
+                if (value instanceof byte[] && newValue instanceof byte[]) {
+                    if (!Arrays.equals((byte[]) value, (byte[]) newValue)) {
+                        return false;
+                    }
+                } else if (!Objects.equals(value, newValue)) {
                     return false;
                 }
             }
diff --git a/telephony/common/android/telephony/LocationAccessPolicy.java b/telephony/common/android/telephony/LocationAccessPolicy.java
index 85d59a2..d4b6c91 100644
--- a/telephony/common/android/telephony/LocationAccessPolicy.java
+++ b/telephony/common/android/telephony/LocationAccessPolicy.java
@@ -316,9 +316,11 @@
             return LocationPermissionResult.ALLOWED;
         }
 
-        // Check the system-wide requirements. If the location main switch is off or
-        // the app's profile isn't in foreground, return a soft denial.
-        if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid)) {
+        // Check the system-wide requirements. If the location main switch is off and the caller is
+        // not in the allowlist of apps that always have loation access or the app's profile
+        // isn't in the foreground, return a soft denial.
+        if (!checkSystemLocationAccess(context, query.callingUid, query.callingPid,
+                query.callingPackage)) {
             return LocationPermissionResult.DENIED_SOFT;
         }
 
@@ -344,15 +346,16 @@
         return LocationPermissionResult.ALLOWED;
     }
 
-
     private static boolean checkManifestPermission(Context context, int pid, int uid,
             String permissionToCheck) {
         return context.checkPermission(permissionToCheck, pid, uid)
                 == PackageManager.PERMISSION_GRANTED;
     }
 
-    private static boolean checkSystemLocationAccess(@NonNull Context context, int uid, int pid) {
-        if (!isLocationModeEnabled(context, UserHandle.getUserHandleForUid(uid).getIdentifier())) {
+    private static boolean checkSystemLocationAccess(@NonNull Context context, int uid, int pid,
+            @NonNull String callingPackage) {
+        if (!isLocationModeEnabled(context, UserHandle.getUserHandleForUid(uid).getIdentifier())
+                && !isLocationBypassAllowed(context, callingPackage)) {
             if (DBG) Log.w(TAG, "Location disabled, failed, (" + uid + ")");
             return false;
         }
@@ -361,7 +364,10 @@
         return isCurrentProfile(context, uid) || checkInteractAcrossUsersFull(context, pid, uid);
     }
 
-    private static boolean isLocationModeEnabled(@NonNull Context context, @UserIdInt int userId) {
+    /**
+     * @return Whether location is enabled for the given user.
+     */
+    public static boolean isLocationModeEnabled(@NonNull Context context, @UserIdInt int userId) {
         LocationManager locationManager = context.getSystemService(LocationManager.class);
         if (locationManager == null) {
             Log.w(TAG, "Couldn't get location manager, denying location access");
@@ -370,6 +376,24 @@
         return locationManager.isLocationEnabledForUser(UserHandle.of(userId));
     }
 
+    private static boolean isLocationBypassAllowed(@NonNull Context context,
+            @NonNull String callingPackage) {
+        for (String bypassPackage : getLocationBypassPackages(context)) {
+            if (callingPackage.equals(bypassPackage)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return An array of packages that are always allowed to access location.
+     */
+    public static @NonNull String[] getLocationBypassPackages(@NonNull Context context) {
+        return context.getResources().getStringArray(
+                com.android.internal.R.array.config_serviceStateLocationAllowedPackages);
+    }
+
     private static boolean checkInteractAcrossUsersFull(
             @NonNull Context context, int pid, int uid) {
         return checkManifestPermission(context, pid, uid,
diff --git a/telephony/common/com/android/internal/telephony/CellBroadcastUtils.java b/telephony/common/com/android/internal/telephony/CellBroadcastUtils.java
index 6c63755..6181329 100644
--- a/telephony/common/com/android/internal/telephony/CellBroadcastUtils.java
+++ b/telephony/common/com/android/internal/telephony/CellBroadcastUtils.java
@@ -16,6 +16,7 @@
 
 package com.android.internal.telephony;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -62,4 +63,17 @@
 
         return packageName;
     }
+
+    /**
+     * Utility method to get cellbroadcast alert dialog component name
+     */
+    public static ComponentName getDefaultCellBroadcastAlertDialogComponent(Context context) {
+        String cellBroadcastReceiverPackageName =
+                getDefaultCellBroadcastReceiverPackageName(context);
+        if (TextUtils.isEmpty(cellBroadcastReceiverPackageName)) {
+            return null;
+        }
+        return ComponentName.createRelative(cellBroadcastReceiverPackageName,
+                "com.android.cellbroadcastreceiver.CellBroadcastAlertDialog");
+    }
 }
diff --git a/telephony/common/com/android/internal/telephony/SmsApplication.java b/telephony/common/com/android/internal/telephony/SmsApplication.java
index 78b0b84..4924a82 100644
--- a/telephony/common/com/android/internal/telephony/SmsApplication.java
+++ b/telephony/common/com/android/internal/telephony/SmsApplication.java
@@ -68,7 +68,6 @@
 public final class SmsApplication {
     static final String LOG_TAG = "SmsApplication";
     public static final String PHONE_PACKAGE_NAME = "com.android.phone";
-    public static final String BLUETOOTH_PACKAGE_NAME = "com.android.bluetooth.services";
     public static final String MMS_SERVICE_PACKAGE_NAME = "com.android.mms.service";
     public static final String TELEPHONY_PROVIDER_PACKAGE_NAME = "com.android.providers.telephony";
 
@@ -541,11 +540,13 @@
         PackageManager packageManager = context.getPackageManager();
         AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
 
+        final String bluetoothPackageName = context.getResources()
+                .getString(com.android.internal.R.string.config_systemBluetoothStack);
         // Assign permission to special system apps
         assignExclusiveSmsPermissionsToSystemApp(context, packageManager, appOps,
                 PHONE_PACKAGE_NAME, true);
         assignExclusiveSmsPermissionsToSystemApp(context, packageManager, appOps,
-                BLUETOOTH_PACKAGE_NAME, true);
+                bluetoothPackageName, false);
         assignExclusiveSmsPermissionsToSystemApp(context, packageManager, appOps,
                 MMS_SERVICE_PACKAGE_NAME, true);
         assignExclusiveSmsPermissionsToSystemApp(context, packageManager, appOps,
@@ -1128,8 +1129,11 @@
             return false;
         }
         final String defaultSmsPackage = getDefaultSmsApplicationPackageName(context);
+        final String bluetoothPackageName = context.getResources()
+                .getString(com.android.internal.R.string.config_systemBluetoothStack);
+
         if ((defaultSmsPackage != null && defaultSmsPackage.equals(packageName))
-                || BLUETOOTH_PACKAGE_NAME.equals(packageName)) {
+                || bluetoothPackageName.equals(packageName)) {
             return true;
         }
         return false;
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 71ffd6e..b6f8652 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -9290,6 +9290,7 @@
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
+                networkTypeBitmask = checkNetworkTypeBitmask(networkTypeBitmask);
                 return telephony.setAllowedNetworkTypesForReason(getSubId(),
                         TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_USER, networkTypeBitmask);
             }
@@ -9300,6 +9301,20 @@
     }
 
     /**
+     * If {@link #NETWORK_TYPE_BITMASK_LTE_CA} bit is set, convert it to NETWORK_TYPE_BITMASK_LTE.
+     *
+     * @param networkTypeBitmask The networkTypeBitmask being checked
+     * @return The checked/converted networkTypeBitmask
+     */
+    private long checkNetworkTypeBitmask(@NetworkTypeBitMask long networkTypeBitmask) {
+        if ((networkTypeBitmask & NETWORK_TYPE_BITMASK_LTE_CA) != 0) {
+            networkTypeBitmask ^= NETWORK_TYPE_BITMASK_LTE_CA;
+            networkTypeBitmask |= NETWORK_TYPE_BITMASK_LTE;
+        }
+        return networkTypeBitmask;
+    }
+
+    /**
      * Set the allowed network types of the device. This is for carrier or privileged apps to
      * enable/disable certain network types on the device. The user preferred network types should
      * be set through {@link #setPreferredNetworkTypeBitmask}.
@@ -9325,6 +9340,7 @@
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
+                allowedNetworkTypes = checkNetworkTypeBitmask(allowedNetworkTypes);
                 return telephony.setAllowedNetworkTypesForReason(getSubId(),
                         TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_CARRIER, allowedNetworkTypes);
             }
@@ -9410,6 +9426,7 @@
         try {
             ITelephony telephony = getITelephony();
             if (telephony != null) {
+                allowedNetworkTypes = checkNetworkTypeBitmask(allowedNetworkTypes);
                 telephony.setAllowedNetworkTypesForReason(getSubId(), reason,
                         allowedNetworkTypes);
             } else {
@@ -13727,7 +13744,11 @@
      */
     public static final long NETWORK_TYPE_BITMASK_LTE = (1 << (NETWORK_TYPE_LTE -1));
     /**
+     * NOT USED; this bitmask is exposed accidentally, will be deprecated in U.
+     * If used, will be converted to {@link #NETWORK_TYPE_BITMASK_LTE}.
      * network type bitmask indicating the support of radio tech LTE CA (carrier aggregation).
+     *
+     * @see #NETWORK_TYPE_BITMASK_LTE
      */
     public static final long NETWORK_TYPE_BITMASK_LTE_CA = (1 << (NETWORK_TYPE_LTE_CA -1));
 
@@ -16985,4 +17006,41 @@
         }
         mTelephonyRegistryMgr.removeCarrierPrivilegesCallback(callback);
     }
+
+    /**
+     * set removable eSIM as default eUICC.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE)
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_EUICC)
+    public void setRemovableEsimAsDefaultEuicc(boolean isDefault) {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                telephony.setRemovableEsimAsDefaultEuicc(isDefault, getOpPackageName());
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in setRemovableEsimAsDefault: " + e);
+        }
+    }
+
+    /**
+     * Returns whether the removable eSIM is default eUICC or not.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+    @RequiresFeature(PackageManager.FEATURE_TELEPHONY_EUICC)
+    public boolean isRemovableEsimDefaultEuicc() {
+        try {
+            ITelephony telephony = getITelephony();
+            if (telephony != null) {
+                return telephony.isRemovableEsimDefaultEuicc(getOpPackageName());
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in isRemovableEsimDefaultEuicc: " + e);
+        }
+        return false;
+    }
 }
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index eede9dc..a673807 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -25,18 +25,17 @@
 import android.annotation.SystemApi;
 import android.app.Activity;
 import android.app.PendingIntent;
-import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
 import android.content.pm.PackageManager;
-import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyFrameworkInitializer;
 import android.telephony.TelephonyManager;
 import android.telephony.UiccCardInfo;
@@ -1209,18 +1208,16 @@
             return;
         }
         try {
-            // TODO: Uncomment below compat change code once callers are ported to use
-            //  switchToSubscription with portIndex for disable operation.
-            // if (subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
-            //        && getIEuiccController().isCompatChangeEnabled(mContext.getOpPackageName(),
-            //        SWITCH_WITHOUT_PORT_INDEX_EXCEPTION_ON_DISABLE)) {
-            //    // Apps targeting on Android T and beyond will get exception whenever
-            //    // switchToSubscription without portIndex is called with INVALID_SUBSCRIPTION_ID.
-            //    Log.e(TAG, "switchToSubscription without portIndex is not allowed for"
-            //            + " disable operation");
-            //    throw new IllegalArgumentException("Must use switchToSubscription with portIndex"
-            //            + " API for disable operation");
-            // }
+            if (subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                     && getIEuiccController().isCompatChangeEnabled(mContext.getOpPackageName(),
+                     SWITCH_WITHOUT_PORT_INDEX_EXCEPTION_ON_DISABLE)) {
+                // Apps targeting on Android T and beyond will get exception whenever
+                // switchToSubscription without portIndex is called with INVALID_SUBSCRIPTION_ID.
+                Log.e(TAG, "switchToSubscription without portIndex is not allowed for"
+                        + " disable operation");
+                throw new IllegalArgumentException("Must use switchToSubscription with portIndex"
+                        + " API for disable operation");
+            }
             getIEuiccController().switchToSubscription(mCardId,
                     subscriptionId, mContext.getOpPackageName(), callbackIntent);
         } catch (RemoteException e) {
diff --git a/telephony/java/android/telephony/ims/feature/RcsFeature.java b/telephony/java/android/telephony/ims/feature/RcsFeature.java
index 843827b..1862412 100644
--- a/telephony/java/android/telephony/ims/feature/RcsFeature.java
+++ b/telephony/java/android/telephony/ims/feature/RcsFeature.java
@@ -118,8 +118,10 @@
         @Override
         public void setCapabilityExchangeEventListener(
                 @Nullable ICapabilityExchangeEventListener listener) throws RemoteException {
-            CapabilityExchangeEventListener listenerWrapper =
-                    new CapabilityExchangeAidlWrapper(listener);
+            // Set the listener wrapper to null if the listener passed in is null. This will notify
+            // the RcsFeature to trigger the destruction of active capability exchange interface.
+            CapabilityExchangeEventListener listenerWrapper = listener != null
+                    ? new CapabilityExchangeAidlWrapper(listener) : null;
             executeMethodAsync(() -> mReference.setCapabilityExchangeEventListener(listenerWrapper),
                     "setCapabilityExchangeEventListener");
         }
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 7d116f9..0ce6b14 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -2553,4 +2553,18 @@
      * for the slot, or {@code null} if none is resolved
      */
     String getCarrierServicePackageNameForLogicalSlot(int logicalSlotIndex);
+
+    /**
+     * set removable eSIM as default eUICC.
+     *
+     * @hide
+     */
+    void setRemovableEsimAsDefaultEuicc(boolean isDefault, String callingPackage);
+
+    /**
+     * Returns whether the removable eSIM is default eUICC or not.
+     *
+     * @hide
+     */
+    boolean isRemovableEsimDefaultEuicc(String callingPackage);
 }
diff --git a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
index 7ee19fb..052ce3a 100644
--- a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
+++ b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java
@@ -22,6 +22,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNotNull;
 import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.matches;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
@@ -214,7 +215,7 @@
         ApplicationInfo bluetoothApplicationInfo = new ApplicationInfo();
         bluetoothApplicationInfo.uid = FAKE_BT_UID;
         bluetoothPackageInfo.applicationInfo = bluetoothApplicationInfo;
-        when(mPackageManager.getPackageInfo(eq(SmsApplication.BLUETOOTH_PACKAGE_NAME), anyInt()))
+        when(mPackageManager.getPackageInfo(matches(".*android.bluetooth.services"), anyInt()))
                 .thenReturn(bluetoothPackageInfo);
 
         PackageInfo telephonyProviderPackageInfo = new PackageInfo();
diff --git a/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java b/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java
index 294f5c1..9c6d852 100644
--- a/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java
+++ b/tests/vcn/java/com/android/server/vcn/util/PersistableBundleUtilsTest.java
@@ -268,6 +268,15 @@
     }
 
     @Test
+    public void testToFromDiskStableBytes() throws Exception {
+        final PersistableBundle testBundle = getTestBundle();
+        final PersistableBundle result =
+                PersistableBundleUtils.fromDiskStableBytes(
+                        PersistableBundleUtils.toDiskStableBytes(testBundle));
+        assertTrue(PersistableBundleUtils.isEqual(testBundle, result));
+    }
+
+    @Test
     public void testEquality_identical() throws Exception {
         final PersistableBundle left = getTestBundle();
         final PersistableBundle right = getTestBundle();