Merge "Add a helper script to apply lint fixes"
diff --git a/apct-tests/perftests/core/src/android/graphics/perftests/TypefaceSerializationPerfTest.java b/apct-tests/perftests/core/src/android/graphics/perftests/TypefaceSerializationPerfTest.java
index 3a23b54..bc8fc53 100644
--- a/apct-tests/perftests/core/src/android/graphics/perftests/TypefaceSerializationPerfTest.java
+++ b/apct-tests/perftests/core/src/android/graphics/perftests/TypefaceSerializationPerfTest.java
@@ -59,9 +59,8 @@
         Typeface.loadPreinstalledSystemFontMap();
     }
 
-    @ManualBenchmarkState.ManualBenchmarkTest(
-            warmupDurationNs = WARMUP_DURATION_NS,
-            targetTestDurationNs = TARGET_TEST_DURATION_NS)
+    // testSerializeFontMap uses the default targetTestDurationNs, which is much longer than
+    // TARGET_TEST_DURATION_NS, in order to stabilize test results.
     @Test
     public void testSerializeFontMap() throws Exception {
         Map<String, Typeface> systemFontMap = Typeface.getSystemFontMap();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 794362b..4d5eef2 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -310,11 +310,25 @@
      */
     boolean mReportedActive;
 
+    /**
+     * Track the most recently completed jobs (that had been executing and were stopped for any
+     * reason, including successful completion).
+     */
     private int mLastCompletedJobIndex = 0;
     private final JobStatus[] mLastCompletedJobs = new JobStatus[NUM_COMPLETED_JOB_HISTORY];
     private final long[] mLastCompletedJobTimeElapsed = new long[NUM_COMPLETED_JOB_HISTORY];
 
     /**
+     * Track the most recently cancelled jobs (that had internal reason
+     * {@link JobParameters#INTERNAL_STOP_REASON_CANCELED}.
+     */
+    private int mLastCancelledJobIndex = 0;
+    private final JobStatus[] mLastCancelledJobs =
+            new JobStatus[DEBUG ? NUM_COMPLETED_JOB_HISTORY : 0];
+    private final long[] mLastCancelledJobTimeElapsed =
+            new long[DEBUG ? NUM_COMPLETED_JOB_HISTORY : 0];
+
+    /**
      * A mapping of which uids are currently in the foreground to their effective bias.
      */
     final SparseIntArray mUidBiasOverride = new SparseIntArray();
@@ -1398,6 +1412,12 @@
             startTrackingJobLocked(incomingJob, cancelled);
         }
         reportActiveLocked();
+        if (mLastCancelledJobs.length > 0
+                && internalReasonCode == JobParameters.INTERNAL_STOP_REASON_CANCELED) {
+            mLastCancelledJobs[mLastCancelledJobIndex] = cancelled;
+            mLastCancelledJobTimeElapsed[mLastCancelledJobIndex] = sElapsedRealtimeClock.millis();
+            mLastCancelledJobIndex = (mLastCancelledJobIndex + 1) % mLastCancelledJobs.length;
+        }
     }
 
     void updateUidState(int uid, int procState) {
@@ -2555,9 +2575,9 @@
     }
 
     private boolean isComponentUsable(@NonNull JobStatus job) {
-        final ServiceInfo service = job.serviceInfo;
+        final String processName = job.serviceProcessName;
 
-        if (service == null) {
+        if (processName == null) {
             if (DEBUG) {
                 Slog.v(TAG, "isComponentUsable: " + job.toShortString()
                         + " component not present");
@@ -2566,8 +2586,7 @@
         }
 
         // Everything else checked out so far, so this is the final yes/no check
-        final boolean appIsBad = mActivityManagerInternal.isAppBad(
-                service.processName, service.applicationInfo.uid);
+        final boolean appIsBad = mActivityManagerInternal.isAppBad(processName, job.getUid());
         if (DEBUG && appIsBad) {
             Slog.i(TAG, "App is bad for " + job.toShortString() + " so not runnable");
         }
@@ -3848,6 +3867,38 @@
             pw.decreaseIndent();
             pw.println();
 
+            boolean recentCancellationsPrinted = false;
+            for (int r = 1; r <= mLastCancelledJobs.length; ++r) {
+                // Print most recent first
+                final int idx = (mLastCancelledJobIndex + mLastCancelledJobs.length - r)
+                        % mLastCancelledJobs.length;
+                job = mLastCancelledJobs[idx];
+                if (job != null) {
+                    if (!predicate.test(job)) {
+                        continue;
+                    }
+                    if (!recentCancellationsPrinted) {
+                        pw.println();
+                        pw.println("Recently cancelled jobs:");
+                        pw.increaseIndent();
+                        recentCancellationsPrinted = true;
+                    }
+                    TimeUtils.formatDuration(mLastCancelledJobTimeElapsed[idx], nowElapsed, pw);
+                    pw.println();
+                    // Double indent for readability
+                    pw.increaseIndent();
+                    pw.increaseIndent();
+                    pw.println(job.toShortString());
+                    job.dump(pw, true, nowElapsed);
+                    pw.decreaseIndent();
+                    pw.decreaseIndent();
+                }
+            }
+            if (!recentCancellationsPrinted) {
+                pw.decreaseIndent();
+                pw.println();
+            }
+
             if (filterUid == -1) {
                 pw.println();
                 pw.print("mReadyToRock="); pw.println(mReadyToRock);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
index 59cd82e..3bbc5a3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
@@ -176,12 +176,13 @@
             Slog.d(TAG, "maybeReportNewChargingStateLocked: "
                     + powerConnected + "/" + stablePower + "/" + batteryNotLow);
         }
-        mFlexibilityController.setConstraintSatisfied(
-                JobStatus.CONSTRAINT_CHARGING, mService.isBatteryCharging());
-        mFlexibilityController
-            .setConstraintSatisfied(JobStatus.CONSTRAINT_BATTERY_NOT_LOW, batteryNotLow);
-
         final long nowElapsed = sElapsedRealtimeClock.millis();
+
+        mFlexibilityController.setConstraintSatisfied(
+                JobStatus.CONSTRAINT_CHARGING, mService.isBatteryCharging(), nowElapsed);
+        mFlexibilityController.setConstraintSatisfied(
+                        JobStatus.CONSTRAINT_BATTERY_NOT_LOW, batteryNotLow, nowElapsed);
+
         for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
             final JobStatus ts = mTrackedTasks.valueAt(i);
             if (ts.hasChargingConstraint()) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java
index aca381f..9b59560 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ComponentController.java
@@ -93,7 +93,12 @@
         }
     };
 
-    private final SparseArrayMap<ComponentName, ServiceInfo> mServiceInfoCache =
+    /**
+     * Cache containing the processName of the ServiceInfo (see {@link ServiceInfo#processName})
+     * if the Service exists and is available.
+     * {@code null} will be stored if the service is currently unavailable.
+     */
+    private final SparseArrayMap<ComponentName, String> mServiceProcessCache =
             new SparseArrayMap<>();
 
     private final ComponentStateUpdateFunctor mComponentStateUpdateFunctor =
@@ -135,18 +140,18 @@
     @Override
     @GuardedBy("mLock")
     public void onUserRemovedLocked(int userId) {
-        mServiceInfoCache.delete(userId);
+        mServiceProcessCache.delete(userId);
     }
 
     @Nullable
     @GuardedBy("mLock")
-    private ServiceInfo getServiceInfoLocked(JobStatus jobStatus) {
+    private String getServiceProcessLocked(JobStatus jobStatus) {
         final ComponentName service = jobStatus.getServiceComponent();
         final int userId = jobStatus.getUserId();
-        if (mServiceInfoCache.contains(userId, service)) {
+        if (mServiceProcessCache.contains(userId, service)) {
             // Return whatever is in the cache, even if it's null. When something changes, we
             // clear the cache.
-            return mServiceInfoCache.get(userId, service);
+            return mServiceProcessCache.get(userId, service);
         }
 
         ServiceInfo si;
@@ -165,30 +170,31 @@
             // Write null to the cache so we don't keep querying PM.
             si = null;
         }
-        mServiceInfoCache.add(userId, service, si);
+        final String processName = si == null ? null : si.processName;
+        mServiceProcessCache.add(userId, service, processName);
 
-        return si;
+        return processName;
     }
 
     @GuardedBy("mLock")
     private boolean updateComponentEnabledStateLocked(JobStatus jobStatus) {
-        final ServiceInfo service = getServiceInfoLocked(jobStatus);
+        final String processName = getServiceProcessLocked(jobStatus);
 
-        if (DEBUG && service == null) {
+        if (DEBUG && processName == null) {
             Slog.v(TAG, jobStatus.toShortString() + " component not present");
         }
-        final ServiceInfo ogService = jobStatus.serviceInfo;
-        jobStatus.serviceInfo = service;
-        return !Objects.equals(ogService, service);
+        final String ogProcess = jobStatus.serviceProcessName;
+        jobStatus.serviceProcessName = processName;
+        return !Objects.equals(ogProcess, processName);
     }
 
     @GuardedBy("mLock")
     private void clearComponentsForPackageLocked(final int userId, final String pkg) {
-        final int uIdx = mServiceInfoCache.indexOfKey(userId);
-        for (int c = mServiceInfoCache.numElementsForKey(userId) - 1; c >= 0; --c) {
-            final ComponentName cn = mServiceInfoCache.keyAt(uIdx, c);
+        final int uIdx = mServiceProcessCache.indexOfKey(userId);
+        for (int c = mServiceProcessCache.numElementsForKey(userId) - 1; c >= 0; --c) {
+            final ComponentName cn = mServiceProcessCache.keyAt(uIdx, c);
             if (cn.getPackageName().equals(pkg)) {
-                mServiceInfoCache.delete(userId, cn);
+                mServiceProcessCache.delete(userId, cn);
             }
         }
     }
@@ -207,7 +213,7 @@
 
     private void updateComponentStateForUser(final int userId) {
         synchronized (mLock) {
-            mServiceInfoCache.delete(userId);
+            mServiceProcessCache.delete(userId);
             updateComponentStatesLocked(jobStatus -> {
                 // Using user ID instead of source user ID because the service will run under the
                 // user ID, not source user ID.
@@ -247,15 +253,15 @@
     @Override
     @GuardedBy("mLock")
     public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
-        for (int u = 0; u < mServiceInfoCache.numMaps(); ++u) {
-            final int userId = mServiceInfoCache.keyAt(u);
-            for (int p = 0; p < mServiceInfoCache.numElementsForKey(userId); ++p) {
-                final ComponentName componentName = mServiceInfoCache.keyAt(u, p);
+        for (int u = 0; u < mServiceProcessCache.numMaps(); ++u) {
+            final int userId = mServiceProcessCache.keyAt(u);
+            for (int p = 0; p < mServiceProcessCache.numElementsForKey(userId); ++p) {
+                final ComponentName componentName = mServiceProcessCache.keyAt(u, p);
                 pw.print(userId);
                 pw.print("-");
                 pw.print(componentName);
                 pw.print(": ");
-                pw.print(mServiceInfoCache.valueAt(u, p));
+                pw.print(mServiceProcessCache.valueAt(u, p));
                 pw.println();
             }
         }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index 2e41dfd..3ca1ad5 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -137,9 +137,9 @@
             new PrefetchController.PrefetchChangedListener() {
                 @Override
                 public void onPrefetchCacheUpdated(ArraySet<JobStatus> jobs, int userId,
-                        String pkgName, long prevEstimatedLaunchTime, long newEstimatedLaunchTime) {
+                        String pkgName, long prevEstimatedLaunchTime,
+                        long newEstimatedLaunchTime, long nowElapsed) {
                     synchronized (mLock) {
-                        final long nowElapsed = sElapsedRealtimeClock.millis();
                         final long prefetchThreshold =
                                 mPrefetchController.getLaunchTimeThresholdMs();
                         boolean jobWasInPrefetchWindow  = prevEstimatedLaunchTime
@@ -158,8 +158,8 @@
                             if (!js.hasFlexibilityConstraint()) {
                                 continue;
                             }
-                            mFlexibilityTracker.resetJobNumDroppedConstraints(js);
-                            mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js);
+                            mFlexibilityTracker.resetJobNumDroppedConstraints(js, nowElapsed);
+                            mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js, nowElapsed);
                         }
                     }
                 }
@@ -191,7 +191,7 @@
             js.setTrackingController(JobStatus.TRACKING_FLEXIBILITY);
             final long nowElapsed = sElapsedRealtimeClock.millis();
             js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
-            mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js);
+            mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js, nowElapsed);
         }
     }
 
@@ -239,7 +239,7 @@
      * Changes flexibility constraint satisfaction for affected jobs.
      */
     @VisibleForTesting
-    void setConstraintSatisfied(int constraint, boolean state) {
+    void setConstraintSatisfied(int constraint, boolean state, long nowElapsed) {
         synchronized (mLock) {
             final boolean old = (mSatisfiedFlexibleConstraints & constraint) != 0;
             if (old == state) {
@@ -255,8 +255,6 @@
             // The rest did not have a change in state and are still satisfied or unsatisfied.
             final int numConstraintsToUpdate = Math.max(curSatisfied, prevSatisfied);
 
-            final long nowElapsed = sElapsedRealtimeClock.millis();
-
             // In order to get the range of all potentially satisfied jobs, we start at the number
             // of satisfied system-wide constraints and iterate to the max number of potentially
             // satisfied constraints, determined by how many job-specific constraints exist.
@@ -329,10 +327,9 @@
 
     @VisibleForTesting
     @GuardedBy("mLock")
-    int getCurPercentOfLifecycleLocked(JobStatus js) {
+    int getCurPercentOfLifecycleLocked(JobStatus js, long nowElapsed) {
         final long earliest = getLifeCycleBeginningElapsedLocked(js);
         final long latest = getLifeCycleEndElapsedLocked(js, earliest);
-        final long nowElapsed = sElapsedRealtimeClock.millis();
         if (latest == NO_LIFECYCLE_END || earliest >= nowElapsed) {
             return 0;
         }
@@ -414,8 +411,8 @@
                                 .getJobsByNumRequiredConstraints(j);
                         for (int i = 0; i < jobs.size(); i++) {
                             JobStatus js = jobs.valueAt(i);
-                            mFlexibilityTracker.resetJobNumDroppedConstraints(js);
-                            mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js);
+                            mFlexibilityTracker.resetJobNumDroppedConstraints(js, nowElapsed);
+                            mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js, nowElapsed);
                             if (js.setFlexibilityConstraintSatisfied(
                                     nowElapsed, isFlexibilitySatisfiedLocked(js))) {
                                 changedJobs.add(js);
@@ -479,8 +476,8 @@
             mTrackedJobs.get(js.getNumRequiredFlexibleConstraints() - 1).remove(js);
         }
 
-        public void resetJobNumDroppedConstraints(JobStatus js) {
-            final int curPercent = getCurPercentOfLifecycleLocked(js);
+        public void resetJobNumDroppedConstraints(JobStatus js, long nowElapsed) {
+            final int curPercent = getCurPercentOfLifecycleLocked(js, nowElapsed);
             int toDrop = 0;
             final int jsMaxFlexibleConstraints = NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS
                     + (js.getPreferUnmetered() ? 1 : 0);
@@ -489,7 +486,8 @@
                     toDrop++;
                 }
             }
-            adjustJobsRequiredConstraints(js, js.getNumDroppedFlexibleConstraints() - toDrop);
+            adjustJobsRequiredConstraints(
+                    js, js.getNumDroppedFlexibleConstraints() - toDrop, nowElapsed);
         }
 
         /** Returns all tracked jobs. */
@@ -502,13 +500,12 @@
          * Returns false if the job status's number of flexible constraints is now 0.
          * Jobs with 0 required flexible constraints are removed from the tracker.
          */
-        public boolean adjustJobsRequiredConstraints(JobStatus js, int n) {
+        public boolean adjustJobsRequiredConstraints(JobStatus js, int n, long nowElapsed) {
             if (n == 0) {
                 return false;
             }
             remove(js);
             js.adjustNumRequiredFlexibleConstraints(n);
-            final long nowElapsed = sElapsedRealtimeClock.millis();
             js.setFlexibilityConstraintSatisfied(nowElapsed, isFlexibilitySatisfiedLocked(js));
             if (js.getNumRequiredFlexibleConstraints() <= 0) {
                 maybeStopTrackingJobLocked(js, null, false);
@@ -553,7 +550,7 @@
             return js.getSourceUserId() == userId;
         }
 
-        public void scheduleDropNumConstraintsAlarm(JobStatus js) {
+        public void scheduleDropNumConstraintsAlarm(JobStatus js, long nowElapsed) {
             long nextTimeElapsed;
             synchronized (mLock) {
                 final long earliest = getLifeCycleBeginningElapsedLocked(js);
@@ -567,7 +564,7 @@
 
                 if (latest - nextTimeElapsed < mDeadlineProximityLimitMs) {
                     mFlexibilityTracker.adjustJobsRequiredConstraints(
-                            js, -js.getNumRequiredFlexibleConstraints());
+                            js, -js.getNumRequiredFlexibleConstraints(), nowElapsed);
                     return;
                 }
                 addAlarm(js, nextTimeElapsed);
@@ -578,21 +575,21 @@
         protected void processExpiredAlarms(@NonNull ArraySet<JobStatus> expired) {
             synchronized (mLock) {
                 ArraySet<JobStatus> changedJobs = new ArraySet<>();
+                final long nowElapsed = sElapsedRealtimeClock.millis();
                 for (int i = 0; i < expired.size(); i++) {
                     JobStatus js = expired.valueAt(i);
                     boolean wasFlexibilitySatisfied = js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE);
 
                     final long earliest = getLifeCycleBeginningElapsedLocked(js);
                     final long latest = getLifeCycleEndElapsedLocked(js, earliest);
-                    final long nowElapsed = sElapsedRealtimeClock.millis();
 
                     if (latest - nowElapsed < mDeadlineProximityLimitMs) {
                         mFlexibilityTracker.adjustJobsRequiredConstraints(js,
-                                -js.getNumRequiredFlexibleConstraints());
+                                -js.getNumRequiredFlexibleConstraints(), nowElapsed);
                     } else {
                         long nextTimeElapsed =
                                 getNextConstraintDropTimeElapsedLocked(js, earliest, latest);
-                        if (mFlexibilityTracker.adjustJobsRequiredConstraints(js, -1)
+                        if (mFlexibilityTracker.adjustJobsRequiredConstraints(js, -1,  nowElapsed)
                                 && nextTimeElapsed != NO_LIFECYCLE_END) {
                             mFlexibilityAlarmQueue.addAlarm(js, nextTimeElapsed);
                         }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
index d5750f8..dd06217 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
@@ -95,9 +95,10 @@
      */
     @Override
     public void reportNewIdleState(boolean isIdle) {
-        mFlexibilityController.setConstraintSatisfied(JobStatus.CONSTRAINT_IDLE, isIdle);
         synchronized (mLock) {
             final long nowElapsed = sElapsedRealtimeClock.millis();
+            mFlexibilityController.setConstraintSatisfied(
+                    JobStatus.CONSTRAINT_IDLE, isIdle, nowElapsed);
             for (int i = mTrackedTasks.size()-1; i >= 0; i--) {
                 mTrackedTasks.valueAt(i).setIdleConstraintSatisfied(nowElapsed, isIdle);
             }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 57c7317..4ce6b321 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -35,7 +35,6 @@
 import android.app.job.JobWorkItem;
 import android.content.ClipData;
 import android.content.ComponentName;
-import android.content.pm.ServiceInfo;
 import android.net.Network;
 import android.net.NetworkRequest;
 import android.net.Uri;
@@ -356,7 +355,7 @@
     public ArraySet<Uri> changedUris;
     public ArraySet<String> changedAuthorities;
     public Network network;
-    public ServiceInfo serviceInfo;
+    public String serviceProcessName;
 
     /** The evaluated bias of the job when it started running. */
     public int lastEvaluatedBias;
@@ -1757,7 +1756,7 @@
         // run if its constraints are satisfied).
         // DeviceNotDozing implicit constraint must be satisfied
         // NotRestrictedInBackground implicit constraint must be satisfied
-        return mReadyNotDozing && mReadyNotRestrictedInBg && (serviceInfo != null)
+        return mReadyNotDozing && mReadyNotRestrictedInBg && (serviceProcessName != null)
                 && (mReadyDeadlineSatisfied || isConstraintsSatisfied(satisfiedConstraints));
     }
 
@@ -2296,7 +2295,7 @@
             pw.println(mReadyDynamicSatisfied);
         }
         pw.print("readyComponentEnabled: ");
-        pw.println(serviceInfo != null);
+        pw.println(serviceProcessName != null);
         if ((getFlags() & JobInfo.FLAG_EXPEDITED) != 0) {
             pw.print("expeditedQuotaApproved: ");
             pw.print(mExpeditedQuotaApproved);
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 0945b7e..e04cec3 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
@@ -105,7 +105,7 @@
     public interface PrefetchChangedListener {
         /** Callback to inform listeners when estimated launch times change. */
         void onPrefetchCacheUpdated(ArraySet<JobStatus> jobs, int userId, String pkgName,
-                long prevEstimatedLaunchTime, long newEstimatedLaunchTime);
+                long prevEstimatedLaunchTime, long newEstimatedLaunchTime, long nowElapsed);
     }
 
     @SuppressWarnings("FieldCanBeLocal")
@@ -308,8 +308,9 @@
                     final long nowElapsed = sElapsedRealtimeClock.millis();
                     updateThresholdAlarmLocked(userId, pkgName, now, nowElapsed);
                     for (int i = 0; i < mPrefetchChangedListeners.size(); i++) {
-                        mPrefetchChangedListeners.valueAt(i).onPrefetchCacheUpdated(jobs,
-                                userId, pkgName, prevEstimatedLaunchTime, newEstimatedLaunchTime);
+                        mPrefetchChangedListeners.valueAt(i).onPrefetchCacheUpdated(
+                                jobs, userId, pkgName, prevEstimatedLaunchTime,
+                                newEstimatedLaunchTime, nowElapsed);
                     }
                     if (maybeUpdateConstraintForPkgLocked(now, nowElapsed, userId, pkgName)) {
                         mStateChangedListener.onControllerStateChanged(jobs);
diff --git a/core/api/current.txt b/core/api/current.txt
index 32bdec0..e043339 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -12448,6 +12448,16 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.SigningInfo> CREATOR;
   }
 
+  public final class UserProperties implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getShowInLauncher();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.UserProperties> CREATOR;
+    field public static final int SHOW_IN_LAUNCHER_NO = 2; // 0x2
+    field public static final int SHOW_IN_LAUNCHER_SEPARATE = 1; // 0x1
+    field public static final int SHOW_IN_LAUNCHER_WITH_PARENT = 0; // 0x0
+  }
+
   public final class VersionedPackage implements android.os.Parcelable {
     ctor public VersionedPackage(@NonNull String, int);
     ctor public VersionedPackage(@NonNull String, long);
@@ -16903,6 +16913,7 @@
     method public int describeContents();
     method public int getFormat();
     method public int getHeight();
+    method public long getId();
     method public int getLayers();
     method public long getUsage();
     method public int getWidth();
@@ -32250,6 +32261,7 @@
     method public android.os.UserHandle getUserForSerialNumber(long);
     method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.CREATE_USERS", "android.permission.QUERY_USERS", android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED}) public String getUserName();
     method public java.util.List<android.os.UserHandle> getUserProfiles();
+    method @NonNull @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.QUERY_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.content.pm.UserProperties getUserProperties(@NonNull android.os.UserHandle);
     method public android.os.Bundle getUserRestrictions();
     method @RequiresPermission(anyOf={"android.permission.MANAGE_USERS", "android.permission.INTERACT_ACROSS_USERS"}, conditional=true) public android.os.Bundle getUserRestrictions(android.os.UserHandle);
     method public boolean hasUserRestriction(String);
@@ -51967,6 +51979,7 @@
     method public boolean isTextEntryKey();
     method public boolean isTextSelectable();
     method public boolean isVisibleToUser();
+    method public void makeQueryableFromAppProcess(@NonNull android.view.View);
     method @Deprecated public static android.view.accessibility.AccessibilityNodeInfo obtain(android.view.View);
     method @Deprecated public static android.view.accessibility.AccessibilityNodeInfo obtain(android.view.View, int);
     method @Deprecated public static android.view.accessibility.AccessibilityNodeInfo obtain();
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 610cb94..e890005 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -172,6 +172,7 @@
     method @NonNull public static android.media.BluetoothProfileConnectionInfo createA2dpSinkInfo(int);
     method @NonNull public static android.media.BluetoothProfileConnectionInfo createHearingAidInfo(boolean);
     method @NonNull public static android.media.BluetoothProfileConnectionInfo createLeAudioInfo(boolean, boolean);
+    method @NonNull public static android.media.BluetoothProfileConnectionInfo createLeAudioOutputInfo(boolean, int);
     method public int describeContents();
     method public int getProfile();
     method public int getVolume();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 9a3c8a0..fefdfd8 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -131,6 +131,7 @@
     method public static void resumeAppSwitches() throws android.os.RemoteException;
     method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public void scheduleApplicationInfoChanged(java.util.List<java.lang.String>, int);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public void setStopUserOnSwitch(int);
+    method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean startUserInBackgroundOnSecondaryDisplay(int, int);
     method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public boolean stopUser(int, boolean);
     method @RequiresPermission(android.Manifest.permission.CHANGE_CONFIGURATION) public boolean updateMccMncConfiguration(@NonNull String, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.DUMP) public void waitForBroadcastIdle();
@@ -1875,6 +1876,7 @@
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.content.pm.UserInfo> getUsers(boolean, boolean, boolean);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public boolean hasBaseUserRestriction(@NonNull String, @NonNull android.os.UserHandle);
     method public static boolean isSplitSystemUser();
+    method public static boolean isUsersOnSecondaryDisplaysEnabled();
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo preCreateUser(@NonNull String) throws android.os.UserManager.UserOperationException;
   }
 
@@ -3331,11 +3333,12 @@
     ctor public TaskFragmentOrganizer(@NonNull java.util.concurrent.Executor);
     method @NonNull public java.util.concurrent.Executor getExecutor();
     method @NonNull public android.window.TaskFragmentOrganizerToken getOrganizerToken();
-    method public void onTaskFragmentAppeared(@NonNull android.window.TaskFragmentInfo);
-    method public void onTaskFragmentError(@NonNull android.os.IBinder, @Nullable android.window.TaskFragmentInfo, int, @NonNull Throwable);
-    method public void onTaskFragmentInfoChanged(@NonNull android.window.TaskFragmentInfo);
-    method public void onTaskFragmentParentInfoChanged(@NonNull android.os.IBinder, @NonNull android.content.res.Configuration);
-    method public void onTaskFragmentVanished(@NonNull android.window.TaskFragmentInfo);
+    method public void onActivityReparentedToTask(@NonNull android.window.WindowContainerTransaction, int, @NonNull android.content.Intent, @NonNull android.os.IBinder);
+    method public void onTaskFragmentAppeared(@NonNull android.window.WindowContainerTransaction, @NonNull android.window.TaskFragmentInfo);
+    method public void onTaskFragmentError(@NonNull android.window.WindowContainerTransaction, @NonNull android.os.IBinder, @Nullable android.window.TaskFragmentInfo, int, @NonNull Throwable);
+    method public void onTaskFragmentInfoChanged(@NonNull android.window.WindowContainerTransaction, @NonNull android.window.TaskFragmentInfo);
+    method public void onTaskFragmentParentInfoChanged(@NonNull android.window.WindowContainerTransaction, int, @NonNull android.content.res.Configuration);
+    method public void onTaskFragmentVanished(@NonNull android.window.WindowContainerTransaction, @NonNull android.window.TaskFragmentInfo);
     method @CallSuper public void registerOrganizer();
     method @CallSuper public void unregisterOrganizer();
   }
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 449729e..182b0a3 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -4336,13 +4336,42 @@
     @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
             android.Manifest.permission.CREATE_USERS})
     public boolean switchUser(@NonNull UserHandle user) {
-        if (user == null) {
-            throw new IllegalArgumentException("UserHandle cannot be null.");
-        }
+        Preconditions.checkNotNull(user, "UserHandle cannot be null.");
+
         return switchUser(user.getIdentifier());
     }
 
     /**
+     * Starts the given user in background and associate the user with the given display.
+     *
+     * <p>This method will allow the user to launch activities on that display, and it's typically
+     * used only on automotive builds when the vehicle has multiple displays (you can verify if it's
+     * supported by calling {@link UserManager#isBackgroundUsersOnSecondaryDisplaysSupported()}).
+     *
+     * @return whether the user was started.
+     *
+     * @throws UnsupportedOperationException if the device does not support background users on
+     * secondary displays.
+     * @throws IllegalArgumentException if the display does not exist.
+     * @throws IllegalStateException if the user cannot be started on that display (for example, if
+     * there's already a user using that display or if the user is already associated with other
+     * display).
+     *
+     * @hide
+     */
+    @TestApi
+    @RequiresPermission(anyOf = {android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.CREATE_USERS})
+    public boolean startUserInBackgroundOnSecondaryDisplay(@UserIdInt int userId,
+            int displayId) {
+        try {
+            return getService().startUserInBackgroundOnSecondaryDisplay(userId, displayId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Gets the message that is shown when a user is switched from.
      *
      * @hide
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 9210958..419b8e1 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -47,6 +47,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiFunction;
 
 /**
  * Activity manager local system service interface.
@@ -623,6 +624,11 @@
      * broadcast my be sent to; any app Ids < {@link android.os.Process#FIRST_APPLICATION_UID} are
      * automatically allowlisted.
      *
+     * @param filterExtrasForReceiver A function to filter intent extras for the given receiver by
+     * using the rules of package visibility. Returns extras with legitimate package info that the
+     * receiver is able to access, or {@code null} if none of the packages is visible to the
+     * receiver.
+     *
      * @see com.android.server.am.ActivityManagerService#broadcastIntentWithFeature(
      *      IApplicationThread, String, Intent, String, IIntentReceiver, int, String, Bundle,
      *      String[], int, Bundle, boolean, boolean, int)
@@ -630,7 +636,9 @@
     public abstract int broadcastIntent(Intent intent,
             IIntentReceiver resultTo,
             String[] requiredPermissions, boolean serialized,
-            int userId, int[] appIdAllowList, @Nullable Bundle bOptions);
+            int userId, int[] appIdAllowList,
+            @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
+            @Nullable Bundle bOptions);
 
     /**
      * Add uid to the ActivityManagerService PendingStartActivityUids list.
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 6b3dc82..b383d7d 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -107,7 +107,6 @@
 import android.net.Proxy;
 import android.net.TrafficStats;
 import android.net.Uri;
-import android.net.wifi.WifiFrameworkInitializer;
 import android.os.AsyncTask;
 import android.os.Binder;
 import android.os.BluetoothServiceManager;
@@ -7901,8 +7900,6 @@
         BluetoothFrameworkInitializer.setBluetoothServiceManager(new BluetoothServiceManager());
         BluetoothFrameworkInitializer.setBinderCallsStatsInitializer(context -> {
             BinderCallsStats.startForBluetooth(context); });
-        WifiFrameworkInitializer.setBinderCallsStatsInitializer(context -> {
-            BinderCallsStats.startForWifi(context); });
     }
 
     private void purgePendingResources() {
diff --git a/core/java/android/app/AppOpInfo.java b/core/java/android/app/AppOpInfo.java
new file mode 100644
index 0000000..979c910
--- /dev/null
+++ b/core/java/android/app/AppOpInfo.java
@@ -0,0 +1,198 @@
+/*
+ * 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.app;
+
+import static android.app.AppOpsManager.OP_NONE;
+
+import android.annotation.NonNull;
+
+import java.util.Objects;
+
+/**
+ * Information about a particular app op.
+ */
+class AppOpInfo {
+    /**
+     * A unique constant identifying this app op.
+     */
+    public final int code;
+
+    /**
+     * This maps each operation to the operation that serves as the
+     * switch to determine whether it is allowed.  Generally this is
+     * a 1:1 mapping, but for some things (like location) that have
+     * multiple low-level operations being tracked that should be
+     * presented to the user as one switch then this can be used to
+     * make them all controlled by the same single operation.
+     */
+    public final int switchCode;
+
+    /**
+     * This maps each operation to the public string constant for it.
+     */
+    public final String name;
+
+    /**
+     * This provides a simple name for each operation to be used
+     * in debug output.
+     */
+    public final String simpleName;
+
+    /**
+     * This optionally maps a permission to an operation.  If there
+     * is no permission associated with an operation, it is null.
+     */
+    public final String permission;
+
+    /**
+     * Specifies whether an Op should be restricted by a user restriction.
+     * Each Op should be filled with a restriction string from UserManager or
+     * null to specify it is not affected by any user restriction.
+     */
+    public final String restriction;
+
+    /**
+     * In which cases should an app be allowed to bypass the
+     * {@link AppOpsManager#setUserRestriction user restriction} for a certain app-op.
+     */
+    public final AppOpsManager.RestrictionBypass allowSystemRestrictionBypass;
+
+    /**
+     * This specifies the default mode for each operation.
+     */
+    public final int defaultMode;
+
+    /**
+     * This specifies whether each option is allowed to be reset
+     * when resetting all app preferences.  Disable reset for
+     * app ops that are under strong control of some part of the
+     * system (such as OP_WRITE_SMS, which should be allowed only
+     * for whichever app is selected as the current SMS app).
+     */
+    public final boolean disableReset;
+
+    /**
+     * This specifies whether each option is only allowed to be read
+     * by apps with manage appops permission.
+     */
+    public final boolean restrictRead;
+
+    AppOpInfo(int code,
+            int switchCode,
+            @NonNull String name,
+            @NonNull String simpleName,
+            String permission,
+            String restriction,
+            AppOpsManager.RestrictionBypass allowSystemRestrictionBypass,
+            int defaultMode,
+            boolean disableReset,
+            boolean restrictRead) {
+        if (code < OP_NONE) throw new IllegalArgumentException();
+        if (switchCode < OP_NONE) throw new IllegalArgumentException();
+        Objects.requireNonNull(name);
+        Objects.requireNonNull(simpleName);
+        this.code = code;
+        this.switchCode = switchCode;
+        this.name = name;
+        this.simpleName = simpleName;
+        this.permission = permission;
+        this.restriction = restriction;
+        this.allowSystemRestrictionBypass = allowSystemRestrictionBypass;
+        this.defaultMode = defaultMode;
+        this.disableReset = disableReset;
+        this.restrictRead = restrictRead;
+    }
+
+    static class Builder {
+        private int mCode;
+        private int mSwitchCode;
+        private String mName;
+        private String mSimpleName;
+        private String mPermission = null;
+        private String mRestriction = null;
+        private AppOpsManager.RestrictionBypass mAllowSystemRestrictionBypass = null;
+        private int mDefaultMode = AppOpsManager.MODE_DEFAULT;
+        private boolean mDisableReset = false;
+        private boolean mRestrictRead = false;
+
+        Builder(int code, @NonNull String name, @NonNull String simpleName) {
+            if (code < OP_NONE) throw new IllegalArgumentException();
+            Objects.requireNonNull(name);
+            Objects.requireNonNull(simpleName);
+            this.mCode = code;
+            this.mSwitchCode = code;
+            this.mName = name;
+            this.mSimpleName = simpleName;
+        }
+
+        public Builder setCode(int value) {
+            this.mCode = value;
+            return this;
+        }
+
+        public Builder setSwitchCode(int value) {
+            this.mSwitchCode = value;
+            return this;
+        }
+
+        public Builder setName(String value) {
+            this.mName = value;
+            return this;
+        }
+
+        public Builder setSimpleName(String value) {
+            this.mSimpleName = value;
+            return this;
+        }
+
+        public Builder setPermission(String value) {
+            this.mPermission = value;
+            return this;
+        }
+
+        public Builder setRestriction(String value) {
+            this.mRestriction = value;
+            return this;
+        }
+
+        public Builder setAllowSystemRestrictionBypass(
+                AppOpsManager.RestrictionBypass value) {
+            this.mAllowSystemRestrictionBypass = value;
+            return this;
+        }
+
+        public Builder setDefaultMode(int value) {
+            this.mDefaultMode = value;
+            return this;
+        }
+
+        public Builder setDisableReset(boolean value) {
+            this.mDisableReset = value;
+            return this;
+        }
+
+        public Builder setRestrictRead(boolean value) {
+            this.mRestrictRead = value;
+            return this;
+        }
+
+        public AppOpInfo build() {
+            return new AppOpInfo(mCode, mSwitchCode, mName, mSimpleName, mPermission, mRestriction,
+                mAllowSystemRestrictionBypass, mDefaultMode, mDisableReset, mRestrictRead);
+        }
+    }
+}
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 9072b50..d6b90a2 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -867,8 +867,7 @@
     // when adding one of these:
     //  - increment _NUM_OP
     //  - define an OPSTR_* constant (marked as @SystemApi)
-    //  - add rows to sOpToSwitch, sOpToString, sOpNames, sOpPerms, sOpDefaultMode, sOpDisableReset,
-    //      sOpRestrictions, sOpAllowSystemRestrictionBypass
+    //  - add row to sAppOpInfos
     //  - add descriptive strings to Settings/res/values/arrays.xml
     //  - add the op to the appropriate template in AppOpsState.OpsTemplate (settings app)
 
@@ -1907,1165 +1906,382 @@
             OP_SCHEDULE_EXACT_ALARM,
             OP_MANAGE_MEDIA,
             OP_TURN_SCREEN_ON,
+            OP_GET_USAGE_STATS,
     };
 
-    /**
-     * This maps each operation to the operation that serves as the
-     * switch to determine whether it is allowed.  Generally this is
-     * a 1:1 mapping, but for some things (like location) that have
-     * multiple low-level operations being tracked that should be
-     * presented to the user as one switch then this can be used to
-     * make them all controlled by the same single operation.
-     */
-    private static int[] sOpToSwitch = new int[] {
-            OP_COARSE_LOCATION,                 // COARSE_LOCATION
-            OP_FINE_LOCATION,                   // FINE_LOCATION
-            OP_COARSE_LOCATION,                 // GPS
-            OP_VIBRATE,                         // VIBRATE
-            OP_READ_CONTACTS,                   // READ_CONTACTS
-            OP_WRITE_CONTACTS,                  // WRITE_CONTACTS
-            OP_READ_CALL_LOG,                   // READ_CALL_LOG
-            OP_WRITE_CALL_LOG,                  // WRITE_CALL_LOG
-            OP_READ_CALENDAR,                   // READ_CALENDAR
-            OP_WRITE_CALENDAR,                  // WRITE_CALENDAR
-            OP_COARSE_LOCATION,                 // WIFI_SCAN
-            OP_POST_NOTIFICATION,               // POST_NOTIFICATION
-            OP_COARSE_LOCATION,                 // NEIGHBORING_CELLS
-            OP_CALL_PHONE,                      // CALL_PHONE
-            OP_READ_SMS,                        // READ_SMS
-            OP_WRITE_SMS,                       // WRITE_SMS
-            OP_RECEIVE_SMS,                     // RECEIVE_SMS
-            OP_RECEIVE_SMS,                     // RECEIVE_EMERGECY_SMS
-            OP_RECEIVE_MMS,                     // RECEIVE_MMS
-            OP_RECEIVE_WAP_PUSH,                // RECEIVE_WAP_PUSH
-            OP_SEND_SMS,                        // SEND_SMS
-            OP_READ_SMS,                        // READ_ICC_SMS
-            OP_WRITE_SMS,                       // WRITE_ICC_SMS
-            OP_WRITE_SETTINGS,                  // WRITE_SETTINGS
-            OP_SYSTEM_ALERT_WINDOW,             // SYSTEM_ALERT_WINDOW
-            OP_ACCESS_NOTIFICATIONS,            // ACCESS_NOTIFICATIONS
-            OP_CAMERA,                          // CAMERA
-            OP_RECORD_AUDIO,                    // RECORD_AUDIO
-            OP_PLAY_AUDIO,                      // PLAY_AUDIO
-            OP_READ_CLIPBOARD,                  // READ_CLIPBOARD
-            OP_WRITE_CLIPBOARD,                 // WRITE_CLIPBOARD
-            OP_TAKE_MEDIA_BUTTONS,              // TAKE_MEDIA_BUTTONS
-            OP_TAKE_AUDIO_FOCUS,                // TAKE_AUDIO_FOCUS
-            OP_AUDIO_MASTER_VOLUME,             // AUDIO_MASTER_VOLUME
-            OP_AUDIO_VOICE_VOLUME,              // AUDIO_VOICE_VOLUME
-            OP_AUDIO_RING_VOLUME,               // AUDIO_RING_VOLUME
-            OP_AUDIO_MEDIA_VOLUME,              // AUDIO_MEDIA_VOLUME
-            OP_AUDIO_ALARM_VOLUME,              // AUDIO_ALARM_VOLUME
-            OP_AUDIO_NOTIFICATION_VOLUME,       // AUDIO_NOTIFICATION_VOLUME
-            OP_AUDIO_BLUETOOTH_VOLUME,          // AUDIO_BLUETOOTH_VOLUME
-            OP_WAKE_LOCK,                       // WAKE_LOCK
-            OP_COARSE_LOCATION,                 // MONITOR_LOCATION
-            OP_COARSE_LOCATION,                 // MONITOR_HIGH_POWER_LOCATION
-            OP_GET_USAGE_STATS,                 // GET_USAGE_STATS
-            OP_MUTE_MICROPHONE,                 // MUTE_MICROPHONE
-            OP_TOAST_WINDOW,                    // TOAST_WINDOW
-            OP_PROJECT_MEDIA,                   // PROJECT_MEDIA
-            OP_ACTIVATE_VPN,                    // ACTIVATE_VPN
-            OP_WRITE_WALLPAPER,                 // WRITE_WALLPAPER
-            OP_ASSIST_STRUCTURE,                // ASSIST_STRUCTURE
-            OP_ASSIST_SCREENSHOT,               // ASSIST_SCREENSHOT
-            OP_READ_PHONE_STATE,                // READ_PHONE_STATE
-            OP_ADD_VOICEMAIL,                   // ADD_VOICEMAIL
-            OP_USE_SIP,                         // USE_SIP
-            OP_PROCESS_OUTGOING_CALLS,          // PROCESS_OUTGOING_CALLS
-            OP_USE_FINGERPRINT,                 // USE_FINGERPRINT
-            OP_BODY_SENSORS,                    // BODY_SENSORS
-            OP_READ_CELL_BROADCASTS,            // READ_CELL_BROADCASTS
-            OP_MOCK_LOCATION,                   // MOCK_LOCATION
-            OP_READ_EXTERNAL_STORAGE,           // READ_EXTERNAL_STORAGE
-            OP_WRITE_EXTERNAL_STORAGE,          // WRITE_EXTERNAL_STORAGE
-            OP_TURN_SCREEN_ON,                  // TURN_SCREEN_ON
-            OP_GET_ACCOUNTS,                    // GET_ACCOUNTS
-            OP_RUN_IN_BACKGROUND,               // RUN_IN_BACKGROUND
-            OP_AUDIO_ACCESSIBILITY_VOLUME,      // AUDIO_ACCESSIBILITY_VOLUME
-            OP_READ_PHONE_NUMBERS,              // READ_PHONE_NUMBERS
-            OP_REQUEST_INSTALL_PACKAGES,        // REQUEST_INSTALL_PACKAGES
-            OP_PICTURE_IN_PICTURE,              // ENTER_PICTURE_IN_PICTURE_ON_HIDE
-            OP_INSTANT_APP_START_FOREGROUND,    // INSTANT_APP_START_FOREGROUND
-            OP_ANSWER_PHONE_CALLS,              // ANSWER_PHONE_CALLS
-            OP_RUN_ANY_IN_BACKGROUND,           // OP_RUN_ANY_IN_BACKGROUND
-            OP_CHANGE_WIFI_STATE,               // OP_CHANGE_WIFI_STATE
-            OP_REQUEST_DELETE_PACKAGES,         // OP_REQUEST_DELETE_PACKAGES
-            OP_BIND_ACCESSIBILITY_SERVICE,      // OP_BIND_ACCESSIBILITY_SERVICE
-            OP_ACCEPT_HANDOVER,                 // ACCEPT_HANDOVER
-            OP_MANAGE_IPSEC_TUNNELS,            // MANAGE_IPSEC_HANDOVERS
-            OP_START_FOREGROUND,                // START_FOREGROUND
-            OP_BLUETOOTH_SCAN,                  // BLUETOOTH_SCAN
-            OP_USE_BIOMETRIC,                   // BIOMETRIC
-            OP_ACTIVITY_RECOGNITION,            // ACTIVITY_RECOGNITION
-            OP_SMS_FINANCIAL_TRANSACTIONS,      // SMS_FINANCIAL_TRANSACTIONS
-            OP_READ_MEDIA_AUDIO,                // READ_MEDIA_AUDIO
-            OP_WRITE_MEDIA_AUDIO,               // WRITE_MEDIA_AUDIO
-            OP_READ_MEDIA_VIDEO,                // READ_MEDIA_VIDEO
-            OP_WRITE_MEDIA_VIDEO,               // WRITE_MEDIA_VIDEO
-            OP_READ_MEDIA_IMAGES,               // READ_MEDIA_IMAGES
-            OP_WRITE_MEDIA_IMAGES,              // WRITE_MEDIA_IMAGES
-            OP_LEGACY_STORAGE,                  // LEGACY_STORAGE
-            OP_ACCESS_ACCESSIBILITY,            // ACCESS_ACCESSIBILITY
-            OP_READ_DEVICE_IDENTIFIERS,         // READ_DEVICE_IDENTIFIERS
-            OP_ACCESS_MEDIA_LOCATION,           // ACCESS_MEDIA_LOCATION
-            OP_QUERY_ALL_PACKAGES,              // QUERY_ALL_PACKAGES
-            OP_MANAGE_EXTERNAL_STORAGE,         // MANAGE_EXTERNAL_STORAGE
-            OP_INTERACT_ACROSS_PROFILES,        //INTERACT_ACROSS_PROFILES
-            OP_ACTIVATE_PLATFORM_VPN,           // ACTIVATE_PLATFORM_VPN
-            OP_LOADER_USAGE_STATS,              // LOADER_USAGE_STATS
-            OP_DEPRECATED_1,                    // deprecated
-            OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, //AUTO_REVOKE_PERMISSIONS_IF_UNUSED
-            OP_AUTO_REVOKE_MANAGED_BY_INSTALLER, //OP_AUTO_REVOKE_MANAGED_BY_INSTALLER
-            OP_NO_ISOLATED_STORAGE,             // NO_ISOLATED_STORAGE
-            OP_PHONE_CALL_MICROPHONE,           // OP_PHONE_CALL_MICROPHONE
-            OP_PHONE_CALL_CAMERA,               // OP_PHONE_CALL_CAMERA
-            OP_RECORD_AUDIO_HOTWORD,            // RECORD_AUDIO_HOTWORD
-            OP_MANAGE_ONGOING_CALLS,            // MANAGE_ONGOING_CALLS
-            OP_MANAGE_CREDENTIALS,              // MANAGE_CREDENTIALS
-            OP_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
-            OP_RECORD_AUDIO_OUTPUT,             // RECORD_AUDIO_OUTPUT
-            OP_SCHEDULE_EXACT_ALARM,            // SCHEDULE_EXACT_ALARM
-            OP_FINE_LOCATION,                   // OP_FINE_LOCATION_SOURCE
-            OP_COARSE_LOCATION,                 // OP_COARSE_LOCATION_SOURCE
-            OP_MANAGE_MEDIA,                    // MANAGE_MEDIA
-            OP_BLUETOOTH_CONNECT,               // OP_BLUETOOTH_CONNECT
-            OP_UWB_RANGING,                     // OP_UWB_RANGING
-            OP_ACTIVITY_RECOGNITION,            // OP_ACTIVITY_RECOGNITION_SOURCE
-            OP_BLUETOOTH_ADVERTISE,             // OP_BLUETOOTH_ADVERTISE
-            OP_RECORD_INCOMING_PHONE_AUDIO,     // OP_RECORD_INCOMING_PHONE_AUDIO
-            OP_NEARBY_WIFI_DEVICES,             // OP_NEARBY_WIFI_DEVICES
-            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
-    };
-
-    /**
-     * This maps each operation to the public string constant for it.
-     */
-    private static String[] sOpToString = new String[]{
-            OPSTR_COARSE_LOCATION,
-            OPSTR_FINE_LOCATION,
-            OPSTR_GPS,
-            OPSTR_VIBRATE,
-            OPSTR_READ_CONTACTS,
-            OPSTR_WRITE_CONTACTS,
-            OPSTR_READ_CALL_LOG,
-            OPSTR_WRITE_CALL_LOG,
-            OPSTR_READ_CALENDAR,
-            OPSTR_WRITE_CALENDAR,
-            OPSTR_WIFI_SCAN,
-            OPSTR_POST_NOTIFICATION,
-            OPSTR_NEIGHBORING_CELLS,
-            OPSTR_CALL_PHONE,
-            OPSTR_READ_SMS,
-            OPSTR_WRITE_SMS,
-            OPSTR_RECEIVE_SMS,
-            OPSTR_RECEIVE_EMERGENCY_BROADCAST,
-            OPSTR_RECEIVE_MMS,
-            OPSTR_RECEIVE_WAP_PUSH,
-            OPSTR_SEND_SMS,
-            OPSTR_READ_ICC_SMS,
-            OPSTR_WRITE_ICC_SMS,
-            OPSTR_WRITE_SETTINGS,
-            OPSTR_SYSTEM_ALERT_WINDOW,
-            OPSTR_ACCESS_NOTIFICATIONS,
-            OPSTR_CAMERA,
-            OPSTR_RECORD_AUDIO,
-            OPSTR_PLAY_AUDIO,
-            OPSTR_READ_CLIPBOARD,
-            OPSTR_WRITE_CLIPBOARD,
-            OPSTR_TAKE_MEDIA_BUTTONS,
-            OPSTR_TAKE_AUDIO_FOCUS,
-            OPSTR_AUDIO_MASTER_VOLUME,
-            OPSTR_AUDIO_VOICE_VOLUME,
-            OPSTR_AUDIO_RING_VOLUME,
-            OPSTR_AUDIO_MEDIA_VOLUME,
-            OPSTR_AUDIO_ALARM_VOLUME,
-            OPSTR_AUDIO_NOTIFICATION_VOLUME,
-            OPSTR_AUDIO_BLUETOOTH_VOLUME,
-            OPSTR_WAKE_LOCK,
-            OPSTR_MONITOR_LOCATION,
-            OPSTR_MONITOR_HIGH_POWER_LOCATION,
-            OPSTR_GET_USAGE_STATS,
-            OPSTR_MUTE_MICROPHONE,
-            OPSTR_TOAST_WINDOW,
-            OPSTR_PROJECT_MEDIA,
-            OPSTR_ACTIVATE_VPN,
-            OPSTR_WRITE_WALLPAPER,
-            OPSTR_ASSIST_STRUCTURE,
-            OPSTR_ASSIST_SCREENSHOT,
-            OPSTR_READ_PHONE_STATE,
-            OPSTR_ADD_VOICEMAIL,
-            OPSTR_USE_SIP,
-            OPSTR_PROCESS_OUTGOING_CALLS,
-            OPSTR_USE_FINGERPRINT,
-            OPSTR_BODY_SENSORS,
-            OPSTR_READ_CELL_BROADCASTS,
-            OPSTR_MOCK_LOCATION,
-            OPSTR_READ_EXTERNAL_STORAGE,
-            OPSTR_WRITE_EXTERNAL_STORAGE,
-            OPSTR_TURN_SCREEN_ON,
-            OPSTR_GET_ACCOUNTS,
-            OPSTR_RUN_IN_BACKGROUND,
-            OPSTR_AUDIO_ACCESSIBILITY_VOLUME,
-            OPSTR_READ_PHONE_NUMBERS,
-            OPSTR_REQUEST_INSTALL_PACKAGES,
-            OPSTR_PICTURE_IN_PICTURE,
-            OPSTR_INSTANT_APP_START_FOREGROUND,
-            OPSTR_ANSWER_PHONE_CALLS,
-            OPSTR_RUN_ANY_IN_BACKGROUND,
-            OPSTR_CHANGE_WIFI_STATE,
-            OPSTR_REQUEST_DELETE_PACKAGES,
-            OPSTR_BIND_ACCESSIBILITY_SERVICE,
-            OPSTR_ACCEPT_HANDOVER,
-            OPSTR_MANAGE_IPSEC_TUNNELS,
-            OPSTR_START_FOREGROUND,
-            OPSTR_BLUETOOTH_SCAN,
-            OPSTR_USE_BIOMETRIC,
-            OPSTR_ACTIVITY_RECOGNITION,
-            OPSTR_SMS_FINANCIAL_TRANSACTIONS,
-            OPSTR_READ_MEDIA_AUDIO,
-            OPSTR_WRITE_MEDIA_AUDIO,
-            OPSTR_READ_MEDIA_VIDEO,
-            OPSTR_WRITE_MEDIA_VIDEO,
-            OPSTR_READ_MEDIA_IMAGES,
-            OPSTR_WRITE_MEDIA_IMAGES,
-            OPSTR_LEGACY_STORAGE,
-            OPSTR_ACCESS_ACCESSIBILITY,
-            OPSTR_READ_DEVICE_IDENTIFIERS,
-            OPSTR_ACCESS_MEDIA_LOCATION,
-            OPSTR_QUERY_ALL_PACKAGES,
-            OPSTR_MANAGE_EXTERNAL_STORAGE,
-            OPSTR_INTERACT_ACROSS_PROFILES,
-            OPSTR_ACTIVATE_PLATFORM_VPN,
-            OPSTR_LOADER_USAGE_STATS,
-            "", // deprecated
-            OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
-            OPSTR_AUTO_REVOKE_MANAGED_BY_INSTALLER,
-            OPSTR_NO_ISOLATED_STORAGE,
-            OPSTR_PHONE_CALL_MICROPHONE,
-            OPSTR_PHONE_CALL_CAMERA,
-            OPSTR_RECORD_AUDIO_HOTWORD,
-            OPSTR_MANAGE_ONGOING_CALLS,
-            OPSTR_MANAGE_CREDENTIALS,
-            OPSTR_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER,
-            OPSTR_RECORD_AUDIO_OUTPUT,
-            OPSTR_SCHEDULE_EXACT_ALARM,
-            OPSTR_FINE_LOCATION_SOURCE,
-            OPSTR_COARSE_LOCATION_SOURCE,
-            OPSTR_MANAGE_MEDIA,
-            OPSTR_BLUETOOTH_CONNECT,
-            OPSTR_UWB_RANGING,
-            OPSTR_ACTIVITY_RECOGNITION_SOURCE,
-            OPSTR_BLUETOOTH_ADVERTISE,
-            OPSTR_RECORD_INCOMING_PHONE_AUDIO,
-            OPSTR_NEARBY_WIFI_DEVICES,
-            OPSTR_ESTABLISH_VPN_SERVICE,
-            OPSTR_ESTABLISH_VPN_MANAGER,
-            OPSTR_ACCESS_RESTRICTED_SETTINGS,
-            OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO,
-    };
-
-    /**
-     * This provides a simple name for each operation to be used
-     * in debug output.
-     */
-    private static String[] sOpNames = new String[] {
-            "COARSE_LOCATION",
-            "FINE_LOCATION",
-            "GPS",
-            "VIBRATE",
-            "READ_CONTACTS",
-            "WRITE_CONTACTS",
-            "READ_CALL_LOG",
-            "WRITE_CALL_LOG",
-            "READ_CALENDAR",
-            "WRITE_CALENDAR",
-            "WIFI_SCAN",
-            "POST_NOTIFICATION",
-            "NEIGHBORING_CELLS",
-            "CALL_PHONE",
-            "READ_SMS",
-            "WRITE_SMS",
-            "RECEIVE_SMS",
-            "RECEIVE_EMERGECY_SMS",
-            "RECEIVE_MMS",
-            "RECEIVE_WAP_PUSH",
-            "SEND_SMS",
-            "READ_ICC_SMS",
-            "WRITE_ICC_SMS",
-            "WRITE_SETTINGS",
-            "SYSTEM_ALERT_WINDOW",
-            "ACCESS_NOTIFICATIONS",
-            "CAMERA",
-            "RECORD_AUDIO",
-            "PLAY_AUDIO",
-            "READ_CLIPBOARD",
-            "WRITE_CLIPBOARD",
-            "TAKE_MEDIA_BUTTONS",
-            "TAKE_AUDIO_FOCUS",
-            "AUDIO_MASTER_VOLUME",
-            "AUDIO_VOICE_VOLUME",
-            "AUDIO_RING_VOLUME",
-            "AUDIO_MEDIA_VOLUME",
-            "AUDIO_ALARM_VOLUME",
-            "AUDIO_NOTIFICATION_VOLUME",
-            "AUDIO_BLUETOOTH_VOLUME",
-            "WAKE_LOCK",
-            "MONITOR_LOCATION",
-            "MONITOR_HIGH_POWER_LOCATION",
-            "GET_USAGE_STATS",
-            "MUTE_MICROPHONE",
-            "TOAST_WINDOW",
-            "PROJECT_MEDIA",
-            "ACTIVATE_VPN",
-            "WRITE_WALLPAPER",
-            "ASSIST_STRUCTURE",
-            "ASSIST_SCREENSHOT",
-            "READ_PHONE_STATE",
-            "ADD_VOICEMAIL",
-            "USE_SIP",
-            "PROCESS_OUTGOING_CALLS",
-            "USE_FINGERPRINT",
-            "BODY_SENSORS",
-            "READ_CELL_BROADCASTS",
-            "MOCK_LOCATION",
-            "READ_EXTERNAL_STORAGE",
-            "WRITE_EXTERNAL_STORAGE",
-            "TURN_ON_SCREEN",
-            "GET_ACCOUNTS",
-            "RUN_IN_BACKGROUND",
-            "AUDIO_ACCESSIBILITY_VOLUME",
-            "READ_PHONE_NUMBERS",
-            "REQUEST_INSTALL_PACKAGES",
-            "PICTURE_IN_PICTURE",
-            "INSTANT_APP_START_FOREGROUND",
-            "ANSWER_PHONE_CALLS",
-            "RUN_ANY_IN_BACKGROUND",
-            "CHANGE_WIFI_STATE",
-            "REQUEST_DELETE_PACKAGES",
-            "BIND_ACCESSIBILITY_SERVICE",
-            "ACCEPT_HANDOVER",
-            "MANAGE_IPSEC_TUNNELS",
-            "START_FOREGROUND",
-            "BLUETOOTH_SCAN",
-            "USE_BIOMETRIC",
-            "ACTIVITY_RECOGNITION",
-            "SMS_FINANCIAL_TRANSACTIONS",
-            "READ_MEDIA_AUDIO",
-            "WRITE_MEDIA_AUDIO",
-            "READ_MEDIA_VIDEO",
-            "WRITE_MEDIA_VIDEO",
-            "READ_MEDIA_IMAGES",
-            "WRITE_MEDIA_IMAGES",
-            "LEGACY_STORAGE",
-            "ACCESS_ACCESSIBILITY",
-            "READ_DEVICE_IDENTIFIERS",
-            "ACCESS_MEDIA_LOCATION",
-            "QUERY_ALL_PACKAGES",
-            "MANAGE_EXTERNAL_STORAGE",
-            "INTERACT_ACROSS_PROFILES",
-            "ACTIVATE_PLATFORM_VPN",
-            "LOADER_USAGE_STATS",
-            "deprecated",
-            "AUTO_REVOKE_PERMISSIONS_IF_UNUSED",
-            "AUTO_REVOKE_MANAGED_BY_INSTALLER",
-            "NO_ISOLATED_STORAGE",
-            "PHONE_CALL_MICROPHONE",
-            "PHONE_CALL_CAMERA",
-            "RECORD_AUDIO_HOTWORD",
-            "MANAGE_ONGOING_CALLS",
-            "MANAGE_CREDENTIALS",
-            "USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER",
-            "RECORD_AUDIO_OUTPUT",
-            "SCHEDULE_EXACT_ALARM",
-            "FINE_LOCATION_SOURCE",
-            "COARSE_LOCATION_SOURCE",
-            "MANAGE_MEDIA",
-            "BLUETOOTH_CONNECT",
-            "UWB_RANGING",
-            "ACTIVITY_RECOGNITION_SOURCE",
-            "BLUETOOTH_ADVERTISE",
-            "RECORD_INCOMING_PHONE_AUDIO",
-            "NEARBY_WIFI_DEVICES",
-            "ESTABLISH_VPN_SERVICE",
-            "ESTABLISH_VPN_MANAGER",
-            "ACCESS_RESTRICTED_SETTINGS",
-            "RECEIVE_SOUNDTRIGGER_AUDIO",
-    };
-
-    /**
-     * This optionally maps a permission to an operation.  If there
-     * is no permission associated with an operation, it is null.
-     */
-    @UnsupportedAppUsage
-    private static String[] sOpPerms = new String[] {
-            android.Manifest.permission.ACCESS_COARSE_LOCATION,
-            android.Manifest.permission.ACCESS_FINE_LOCATION,
-            null,
-            android.Manifest.permission.VIBRATE,
-            android.Manifest.permission.READ_CONTACTS,
-            android.Manifest.permission.WRITE_CONTACTS,
-            android.Manifest.permission.READ_CALL_LOG,
-            android.Manifest.permission.WRITE_CALL_LOG,
-            android.Manifest.permission.READ_CALENDAR,
-            android.Manifest.permission.WRITE_CALENDAR,
-            android.Manifest.permission.ACCESS_WIFI_STATE,
-            android.Manifest.permission.POST_NOTIFICATIONS,
-            null, // neighboring cells shares the coarse location perm
-            android.Manifest.permission.CALL_PHONE,
-            android.Manifest.permission.READ_SMS,
-            null, // no permission required for writing sms
-            android.Manifest.permission.RECEIVE_SMS,
-            android.Manifest.permission.RECEIVE_EMERGENCY_BROADCAST,
-            android.Manifest.permission.RECEIVE_MMS,
-            android.Manifest.permission.RECEIVE_WAP_PUSH,
-            android.Manifest.permission.SEND_SMS,
-            android.Manifest.permission.READ_SMS,
-            null, // no permission required for writing icc sms
-            android.Manifest.permission.WRITE_SETTINGS,
-            android.Manifest.permission.SYSTEM_ALERT_WINDOW,
-            android.Manifest.permission.ACCESS_NOTIFICATIONS,
-            android.Manifest.permission.CAMERA,
-            android.Manifest.permission.RECORD_AUDIO,
-            null, // no permission for playing audio
-            null, // no permission for reading clipboard
-            null, // no permission for writing clipboard
-            null, // no permission for taking media buttons
-            null, // no permission for taking audio focus
-            null, // no permission for changing global volume
-            null, // no permission for changing voice volume
-            null, // no permission for changing ring volume
-            null, // no permission for changing media volume
-            null, // no permission for changing alarm volume
-            null, // no permission for changing notification volume
-            null, // no permission for changing bluetooth volume
-            android.Manifest.permission.WAKE_LOCK,
-            null, // no permission for generic location monitoring
-            null, // no permission for high power location monitoring
-            android.Manifest.permission.PACKAGE_USAGE_STATS,
-            null, // no permission for muting/unmuting microphone
-            null, // no permission for displaying toasts
-            null, // no permission for projecting media
-            null, // no permission for activating vpn
-            null, // no permission for supporting wallpaper
-            null, // no permission for receiving assist structure
-            null, // no permission for receiving assist screenshot
-            Manifest.permission.READ_PHONE_STATE,
-            Manifest.permission.ADD_VOICEMAIL,
-            Manifest.permission.USE_SIP,
-            Manifest.permission.PROCESS_OUTGOING_CALLS,
-            Manifest.permission.USE_FINGERPRINT,
-            Manifest.permission.BODY_SENSORS,
-            Manifest.permission.READ_CELL_BROADCASTS,
-            null,
-            Manifest.permission.READ_EXTERNAL_STORAGE,
-            Manifest.permission.WRITE_EXTERNAL_STORAGE,
-            Manifest.permission.TURN_SCREEN_ON,
-            Manifest.permission.GET_ACCOUNTS,
-            null, // no permission for running in background
-            null, // no permission for changing accessibility volume
-            Manifest.permission.READ_PHONE_NUMBERS,
-            Manifest.permission.REQUEST_INSTALL_PACKAGES,
-            null, // no permission for entering picture-in-picture on hide
-            Manifest.permission.INSTANT_APP_FOREGROUND_SERVICE,
-            Manifest.permission.ANSWER_PHONE_CALLS,
-            null, // no permission for OP_RUN_ANY_IN_BACKGROUND
-            Manifest.permission.CHANGE_WIFI_STATE,
-            Manifest.permission.REQUEST_DELETE_PACKAGES,
-            Manifest.permission.BIND_ACCESSIBILITY_SERVICE,
-            Manifest.permission.ACCEPT_HANDOVER,
-            Manifest.permission.MANAGE_IPSEC_TUNNELS,
-            Manifest.permission.FOREGROUND_SERVICE,
-            Manifest.permission.BLUETOOTH_SCAN,
-            Manifest.permission.USE_BIOMETRIC,
-            Manifest.permission.ACTIVITY_RECOGNITION,
-            Manifest.permission.SMS_FINANCIAL_TRANSACTIONS,
-            Manifest.permission.READ_MEDIA_AUDIO,
-            null, // no permission for OP_WRITE_MEDIA_AUDIO
-            Manifest.permission.READ_MEDIA_VIDEO,
-            null, // no permission for OP_WRITE_MEDIA_VIDEO
-            Manifest.permission.READ_MEDIA_IMAGES,
-            null, // no permission for OP_WRITE_MEDIA_IMAGES
-            null, // no permission for OP_LEGACY_STORAGE
-            null, // no permission for OP_ACCESS_ACCESSIBILITY
-            null, // no direct permission for OP_READ_DEVICE_IDENTIFIERS
-            Manifest.permission.ACCESS_MEDIA_LOCATION,
-            null, // no permission for OP_QUERY_ALL_PACKAGES
-            Manifest.permission.MANAGE_EXTERNAL_STORAGE,
-            android.Manifest.permission.INTERACT_ACROSS_PROFILES,
-            null, // no permission for OP_ACTIVATE_PLATFORM_VPN
-            android.Manifest.permission.LOADER_USAGE_STATS,
-            null, // deprecated operation
-            null, // no permission for OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED
-            null, // no permission for OP_AUTO_REVOKE_MANAGED_BY_INSTALLER
-            null, // no permission for OP_NO_ISOLATED_STORAGE
-            null, // no permission for OP_PHONE_CALL_MICROPHONE
-            null, // no permission for OP_PHONE_CALL_CAMERA
-            null, // no permission for OP_RECORD_AUDIO_HOTWORD
-            Manifest.permission.MANAGE_ONGOING_CALLS,
-            null, // no permission for OP_MANAGE_CREDENTIALS
-            Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER,
-            null, // no permission for OP_RECORD_AUDIO_OUTPUT
-            Manifest.permission.SCHEDULE_EXACT_ALARM,
-            null, // no permission for OP_ACCESS_FINE_LOCATION_SOURCE,
-            null, // no permission for OP_ACCESS_COARSE_LOCATION_SOURCE,
-            Manifest.permission.MANAGE_MEDIA,
-            Manifest.permission.BLUETOOTH_CONNECT,
-            Manifest.permission.UWB_RANGING,
-            null, // no permission for OP_ACTIVITY_RECOGNITION_SOURCE,
-            Manifest.permission.BLUETOOTH_ADVERTISE,
-            null, // no permission for OP_RECORD_INCOMING_PHONE_AUDIO,
-            Manifest.permission.NEARBY_WIFI_DEVICES,
-            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
-    };
-
-    /**
-     * Specifies whether an Op should be restricted by a user restriction.
-     * Each Op should be filled with a restriction string from UserManager or
-     * null to specify it is not affected by any user restriction.
-     */
-    private static String[] sOpRestrictions = new String[] {
-            UserManager.DISALLOW_SHARE_LOCATION, //COARSE_LOCATION
-            UserManager.DISALLOW_SHARE_LOCATION, //FINE_LOCATION
-            UserManager.DISALLOW_SHARE_LOCATION, //GPS
-            null, //VIBRATE
-            null, //READ_CONTACTS
-            null, //WRITE_CONTACTS
-            UserManager.DISALLOW_OUTGOING_CALLS, //READ_CALL_LOG
-            UserManager.DISALLOW_OUTGOING_CALLS, //WRITE_CALL_LOG
-            null, //READ_CALENDAR
-            null, //WRITE_CALENDAR
-            UserManager.DISALLOW_SHARE_LOCATION, //WIFI_SCAN
-            null, //POST_NOTIFICATION
-            null, //NEIGHBORING_CELLS
-            null, //CALL_PHONE
-            UserManager.DISALLOW_SMS, //READ_SMS
-            UserManager.DISALLOW_SMS, //WRITE_SMS
-            UserManager.DISALLOW_SMS, //RECEIVE_SMS
-            null, //RECEIVE_EMERGENCY_SMS
-            UserManager.DISALLOW_SMS, //RECEIVE_MMS
-            null, //RECEIVE_WAP_PUSH
-            UserManager.DISALLOW_SMS, //SEND_SMS
-            UserManager.DISALLOW_SMS, //READ_ICC_SMS
-            UserManager.DISALLOW_SMS, //WRITE_ICC_SMS
-            null, //WRITE_SETTINGS
-            UserManager.DISALLOW_CREATE_WINDOWS, //SYSTEM_ALERT_WINDOW
-            null, //ACCESS_NOTIFICATIONS
-            UserManager.DISALLOW_CAMERA, //CAMERA
-            UserManager.DISALLOW_RECORD_AUDIO, //RECORD_AUDIO
-            null, //PLAY_AUDIO
-            null, //READ_CLIPBOARD
-            null, //WRITE_CLIPBOARD
-            null, //TAKE_MEDIA_BUTTONS
-            null, //TAKE_AUDIO_FOCUS
-            UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_MASTER_VOLUME
-            UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_VOICE_VOLUME
-            UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_RING_VOLUME
-            UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_MEDIA_VOLUME
-            UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_ALARM_VOLUME
-            UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_NOTIFICATION_VOLUME
-            UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_BLUETOOTH_VOLUME
-            null, //WAKE_LOCK
-            UserManager.DISALLOW_SHARE_LOCATION, //MONITOR_LOCATION
-            UserManager.DISALLOW_SHARE_LOCATION, //MONITOR_HIGH_POWER_LOCATION
-            null, //GET_USAGE_STATS
-            UserManager.DISALLOW_UNMUTE_MICROPHONE, // MUTE_MICROPHONE
-            UserManager.DISALLOW_CREATE_WINDOWS, // TOAST_WINDOW
-            null, //PROJECT_MEDIA
-            null, // ACTIVATE_VPN
-            UserManager.DISALLOW_WALLPAPER, // WRITE_WALLPAPER
-            null, // ASSIST_STRUCTURE
-            null, // ASSIST_SCREENSHOT
-            null, // READ_PHONE_STATE
-            null, // ADD_VOICEMAIL
-            null, // USE_SIP
-            null, // PROCESS_OUTGOING_CALLS
-            null, // USE_FINGERPRINT
-            null, // BODY_SENSORS
-            null, // READ_CELL_BROADCASTS
-            null, // MOCK_LOCATION
-            null, // READ_EXTERNAL_STORAGE
-            null, // WRITE_EXTERNAL_STORAGE
-            null, // TURN_SCREEN_ON
-            null, // GET_ACCOUNTS
-            null, // RUN_IN_BACKGROUND
-            UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_ACCESSIBILITY_VOLUME
-            null, // READ_PHONE_NUMBERS
-            null, // REQUEST_INSTALL_PACKAGES
-            null, // ENTER_PICTURE_IN_PICTURE_ON_HIDE
-            null, // INSTANT_APP_START_FOREGROUND
-            null, // ANSWER_PHONE_CALLS
-            null, // OP_RUN_ANY_IN_BACKGROUND
-            null, // OP_CHANGE_WIFI_STATE
-            null, // REQUEST_DELETE_PACKAGES
-            null, // OP_BIND_ACCESSIBILITY_SERVICE
-            null, // ACCEPT_HANDOVER
-            null, // MANAGE_IPSEC_TUNNELS
-            null, // START_FOREGROUND
-            null, // maybe should be UserManager.DISALLOW_SHARE_LOCATION, //BLUETOOTH_SCAN
-            null, // USE_BIOMETRIC
-            null, // ACTIVITY_RECOGNITION
-            UserManager.DISALLOW_SMS, // SMS_FINANCIAL_TRANSACTIONS
-            null, // READ_MEDIA_AUDIO
-            null, // WRITE_MEDIA_AUDIO
-            null, // READ_MEDIA_VIDEO
-            null, // WRITE_MEDIA_VIDEO
-            null, // READ_MEDIA_IMAGES
-            null, // WRITE_MEDIA_IMAGES
-            null, // LEGACY_STORAGE
-            null, // ACCESS_ACCESSIBILITY
-            null, // READ_DEVICE_IDENTIFIERS
-            null, // ACCESS_MEDIA_LOCATION
-            null, // QUERY_ALL_PACKAGES
-            null, // MANAGE_EXTERNAL_STORAGE
-            null, // INTERACT_ACROSS_PROFILES
-            null, // ACTIVATE_PLATFORM_VPN
-            null, // LOADER_USAGE_STATS
-            null, // deprecated operation
-            null, // AUTO_REVOKE_PERMISSIONS_IF_UNUSED
-            null, // AUTO_REVOKE_MANAGED_BY_INSTALLER
-            null, // NO_ISOLATED_STORAGE
-            null, // PHONE_CALL_MICROPHONE
-            null, // PHONE_CALL_MICROPHONE
-            null, // RECORD_AUDIO_HOTWORD
-            null, // MANAGE_ONGOING_CALLS
-            null, // MANAGE_CREDENTIALS
-            null, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
-            null, // RECORD_AUDIO_OUTPUT
-            null, // SCHEDULE_EXACT_ALARM
-            null, // ACCESS_FINE_LOCATION_SOURCE
-            null, // ACCESS_COARSE_LOCATION_SOURCE
-            null, // MANAGE_MEDIA
-            null, // BLUETOOTH_CONNECT
-            null, // UWB_RANGING
-            null, // ACTIVITY_RECOGNITION_SOURCE
-            null, // BLUETOOTH_ADVERTISE
-            null, // RECORD_INCOMING_PHONE_AUDIO
-            null, // NEARBY_WIFI_DEVICES
-            null, // ESTABLISH_VPN_SERVICE
-            null, // ESTABLISH_VPN_MANAGER
-            null, // ACCESS_RESTRICTED_SETTINGS
-            null, // RECEIVE_SOUNDTRIGGER_AUDIO
-    };
-
-    /**
-     * In which cases should an app be allowed to bypass the {@link #setUserRestriction user
-     * restriction} for a certain app-op.
-     */
-    private static RestrictionBypass[] sOpAllowSystemRestrictionBypass = new RestrictionBypass[] {
-            new RestrictionBypass(true, false, false), //COARSE_LOCATION
-            new RestrictionBypass(true, false, false), //FINE_LOCATION
-            null, //GPS
-            null, //VIBRATE
-            null, //READ_CONTACTS
-            null, //WRITE_CONTACTS
-            null, //READ_CALL_LOG
-            null, //WRITE_CALL_LOG
-            null, //READ_CALENDAR
-            null, //WRITE_CALENDAR
-            new RestrictionBypass(false, true, false), //WIFI_SCAN
-            null, //POST_NOTIFICATION
-            null, //NEIGHBORING_CELLS
-            null, //CALL_PHONE
-            null, //READ_SMS
-            null, //WRITE_SMS
-            null, //RECEIVE_SMS
-            null, //RECEIVE_EMERGECY_SMS
-            null, //RECEIVE_MMS
-            null, //RECEIVE_WAP_PUSH
-            null, //SEND_SMS
-            null, //READ_ICC_SMS
-            null, //WRITE_ICC_SMS
-            null, //WRITE_SETTINGS
-            new RestrictionBypass(false, true, false), //SYSTEM_ALERT_WINDOW
-            null, //ACCESS_NOTIFICATIONS
-            null, //CAMERA
-            new RestrictionBypass(false, false, true), //RECORD_AUDIO
-            null, //PLAY_AUDIO
-            null, //READ_CLIPBOARD
-            null, //WRITE_CLIPBOARD
-            null, //TAKE_MEDIA_BUTTONS
-            null, //TAKE_AUDIO_FOCUS
-            null, //AUDIO_MASTER_VOLUME
-            null, //AUDIO_VOICE_VOLUME
-            null, //AUDIO_RING_VOLUME
-            null, //AUDIO_MEDIA_VOLUME
-            null, //AUDIO_ALARM_VOLUME
-            null, //AUDIO_NOTIFICATION_VOLUME
-            null, //AUDIO_BLUETOOTH_VOLUME
-            null, //WAKE_LOCK
-            null, //MONITOR_LOCATION
-            null, //MONITOR_HIGH_POWER_LOCATION
-            null, //GET_USAGE_STATS
-            null, //MUTE_MICROPHONE
-            new RestrictionBypass(false, true, false), //TOAST_WINDOW
-            null, //PROJECT_MEDIA
-            null, //ACTIVATE_VPN
-            null, //WALLPAPER
-            null, //ASSIST_STRUCTURE
-            null, //ASSIST_SCREENSHOT
-            null, //READ_PHONE_STATE
-            null, //ADD_VOICEMAIL
-            null, // USE_SIP
-            null, // PROCESS_OUTGOING_CALLS
-            null, // USE_FINGERPRINT
-            null, // BODY_SENSORS
-            null, // READ_CELL_BROADCASTS
-            null, // MOCK_LOCATION
-            null, // READ_EXTERNAL_STORAGE
-            null, // WRITE_EXTERNAL_STORAGE
-            null, // TURN_SCREEN_ON
-            null, // GET_ACCOUNTS
-            null, // RUN_IN_BACKGROUND
-            null, // AUDIO_ACCESSIBILITY_VOLUME
-            null, // READ_PHONE_NUMBERS
-            null, // REQUEST_INSTALL_PACKAGES
-            null, // ENTER_PICTURE_IN_PICTURE_ON_HIDE
-            null, // INSTANT_APP_START_FOREGROUND
-            null, // ANSWER_PHONE_CALLS
-            null, // OP_RUN_ANY_IN_BACKGROUND
-            null, // OP_CHANGE_WIFI_STATE
-            null, // OP_REQUEST_DELETE_PACKAGES
-            null, // OP_BIND_ACCESSIBILITY_SERVICE
-            null, // ACCEPT_HANDOVER
-            null, // MANAGE_IPSEC_HANDOVERS
-            null, // START_FOREGROUND
-            new RestrictionBypass(false, true, false), // BLUETOOTH_SCAN
-            null, // USE_BIOMETRIC
-            null, // ACTIVITY_RECOGNITION
-            null, // SMS_FINANCIAL_TRANSACTIONS
-            null, // READ_MEDIA_AUDIO
-            null, // WRITE_MEDIA_AUDIO
-            null, // READ_MEDIA_VIDEO
-            null, // WRITE_MEDIA_VIDEO
-            null, // READ_MEDIA_IMAGES
-            null, // WRITE_MEDIA_IMAGES
-            null, // LEGACY_STORAGE
-            null, // ACCESS_ACCESSIBILITY
-            null, // READ_DEVICE_IDENTIFIERS
-            null, // ACCESS_MEDIA_LOCATION
-            null, // QUERY_ALL_PACKAGES
-            null, // MANAGE_EXTERNAL_STORAGE
-            null, // INTERACT_ACROSS_PROFILES
-            null, // ACTIVATE_PLATFORM_VPN
-            null, // LOADER_USAGE_STATS
-            null, // deprecated operation
-            null, // AUTO_REVOKE_PERMISSIONS_IF_UNUSED
-            null, // AUTO_REVOKE_MANAGED_BY_INSTALLER
-            null, // NO_ISOLATED_STORAGE
-            null, // PHONE_CALL_MICROPHONE
-            null, // PHONE_CALL_CAMERA
-            null, // RECORD_AUDIO_HOTWORD
-            null, // MANAGE_ONGOING_CALLS
-            null, // MANAGE_CREDENTIALS
-            null, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
-            null, // RECORD_AUDIO_OUTPUT
-            null, // SCHEDULE_EXACT_ALARM
-            null, // ACCESS_FINE_LOCATION_SOURCE
-            null, // ACCESS_COARSE_LOCATION_SOURCE
-            null, // MANAGE_MEDIA
-            null, // BLUETOOTH_CONNECT
-            null, // UWB_RANGING
-            null, // ACTIVITY_RECOGNITION_SOURCE
-            null, // BLUETOOTH_ADVERTISE
-            null, // RECORD_INCOMING_PHONE_AUDIO
-            null, // NEARBY_WIFI_DEVICES
-            null, // ESTABLISH_VPN_SERVICE
-            null, // ESTABLISH_VPN_MANAGER
-            null, // ACCESS_RESTRICTED_SETTINGS
-            null, // RECEIVE_SOUNDTRIGGER_AUDIO
-    };
-
-    /**
-     * This specifies the default mode for each operation.
-     */
-    private static int[] sOpDefaultMode = new int[] {
-            AppOpsManager.MODE_ALLOWED, // COARSE_LOCATION
-            AppOpsManager.MODE_ALLOWED, // FINE_LOCATION
-            AppOpsManager.MODE_ALLOWED, // GPS
-            AppOpsManager.MODE_ALLOWED, // VIBRATE
-            AppOpsManager.MODE_ALLOWED, // READ_CONTACTS
-            AppOpsManager.MODE_ALLOWED, // WRITE_CONTACTS
-            AppOpsManager.MODE_ALLOWED, // READ_CALL_LOG
-            AppOpsManager.MODE_ALLOWED, // WRITE_CALL_LOG
-            AppOpsManager.MODE_ALLOWED, // READ_CALENDAR
-            AppOpsManager.MODE_ALLOWED, // WRITE_CALENDAR
-            AppOpsManager.MODE_ALLOWED, // WIFI_SCAN
-            AppOpsManager.MODE_ALLOWED, // POST_NOTIFICATION
-            AppOpsManager.MODE_ALLOWED, // NEIGHBORING_CELLS
-            AppOpsManager.MODE_ALLOWED, // CALL_PHONE
-            AppOpsManager.MODE_ALLOWED, // READ_SMS
-            AppOpsManager.MODE_IGNORED, // WRITE_SMS
-            AppOpsManager.MODE_ALLOWED, // RECEIVE_SMS
-            AppOpsManager.MODE_ALLOWED, // RECEIVE_EMERGENCY_BROADCAST
-            AppOpsManager.MODE_ALLOWED, // RECEIVE_MMS
-            AppOpsManager.MODE_ALLOWED, // RECEIVE_WAP_PUSH
-            AppOpsManager.MODE_ALLOWED, // SEND_SMS
-            AppOpsManager.MODE_ALLOWED, // READ_ICC_SMS
-            AppOpsManager.MODE_ALLOWED, // WRITE_ICC_SMS
-            AppOpsManager.MODE_DEFAULT, // WRITE_SETTINGS
-            getSystemAlertWindowDefault(), // SYSTEM_ALERT_WINDOW
-            AppOpsManager.MODE_ALLOWED, // ACCESS_NOTIFICATIONS
-            AppOpsManager.MODE_ALLOWED, // CAMERA
-            AppOpsManager.MODE_ALLOWED, // RECORD_AUDIO
-            AppOpsManager.MODE_ALLOWED, // PLAY_AUDIO
-            AppOpsManager.MODE_ALLOWED, // READ_CLIPBOARD
-            AppOpsManager.MODE_ALLOWED, // WRITE_CLIPBOARD
-            AppOpsManager.MODE_ALLOWED, // TAKE_MEDIA_BUTTONS
-            AppOpsManager.MODE_ALLOWED, // TAKE_AUDIO_FOCUS
-            AppOpsManager.MODE_ALLOWED, // AUDIO_MASTER_VOLUME
-            AppOpsManager.MODE_ALLOWED, // AUDIO_VOICE_VOLUME
-            AppOpsManager.MODE_ALLOWED, // AUDIO_RING_VOLUME
-            AppOpsManager.MODE_ALLOWED, // AUDIO_MEDIA_VOLUME
-            AppOpsManager.MODE_ALLOWED, // AUDIO_ALARM_VOLUME
-            AppOpsManager.MODE_ALLOWED, // AUDIO_NOTIFICATION_VOLUME
-            AppOpsManager.MODE_ALLOWED, // AUDIO_BLUETOOTH_VOLUME
-            AppOpsManager.MODE_ALLOWED, // WAKE_LOCK
-            AppOpsManager.MODE_ALLOWED, // MONITOR_LOCATION
-            AppOpsManager.MODE_ALLOWED, // MONITOR_HIGH_POWER_LOCATION
-            AppOpsManager.MODE_DEFAULT, // GET_USAGE_STATS
-            AppOpsManager.MODE_ALLOWED, // MUTE_MICROPHONE
-            AppOpsManager.MODE_ALLOWED, // TOAST_WINDOW
-            AppOpsManager.MODE_IGNORED, // PROJECT_MEDIA
-            AppOpsManager.MODE_IGNORED, // ACTIVATE_VPN
-            AppOpsManager.MODE_ALLOWED, // WRITE_WALLPAPER
-            AppOpsManager.MODE_ALLOWED, // ASSIST_STRUCTURE
-            AppOpsManager.MODE_ALLOWED, // ASSIST_SCREENSHOT
-            AppOpsManager.MODE_ALLOWED, // READ_PHONE_STATE
-            AppOpsManager.MODE_ALLOWED, // ADD_VOICEMAIL
-            AppOpsManager.MODE_ALLOWED, // USE_SIP
-            AppOpsManager.MODE_ALLOWED, // PROCESS_OUTGOING_CALLS
-            AppOpsManager.MODE_ALLOWED, // USE_FINGERPRINT
-            AppOpsManager.MODE_ALLOWED, // BODY_SENSORS
-            AppOpsManager.MODE_ALLOWED, // READ_CELL_BROADCASTS
-            AppOpsManager.MODE_ERRORED, // MOCK_LOCATION
-            AppOpsManager.MODE_ALLOWED, // READ_EXTERNAL_STORAGE
-            AppOpsManager.MODE_ALLOWED, // WRITE_EXTERNAL_STORAGE
-            AppOpsManager.MODE_ERRORED, // TURN_SCREEN_ON
-            AppOpsManager.MODE_ALLOWED, // GET_ACCOUNTS
-            AppOpsManager.MODE_ALLOWED, // RUN_IN_BACKGROUND
-            AppOpsManager.MODE_ALLOWED, // AUDIO_ACCESSIBILITY_VOLUME
-            AppOpsManager.MODE_ALLOWED, // READ_PHONE_NUMBERS
-            AppOpsManager.MODE_DEFAULT, // REQUEST_INSTALL_PACKAGES
-            AppOpsManager.MODE_ALLOWED, // PICTURE_IN_PICTURE
-            AppOpsManager.MODE_DEFAULT, // INSTANT_APP_START_FOREGROUND
-            AppOpsManager.MODE_ALLOWED, // ANSWER_PHONE_CALLS
-            AppOpsManager.MODE_ALLOWED, // RUN_ANY_IN_BACKGROUND
-            AppOpsManager.MODE_ALLOWED, // CHANGE_WIFI_STATE
-            AppOpsManager.MODE_ALLOWED, // REQUEST_DELETE_PACKAGES
-            AppOpsManager.MODE_ALLOWED, // BIND_ACCESSIBILITY_SERVICE
-            AppOpsManager.MODE_ALLOWED, // ACCEPT_HANDOVER
-            AppOpsManager.MODE_ERRORED, // MANAGE_IPSEC_TUNNELS
-            AppOpsManager.MODE_ALLOWED, // START_FOREGROUND
-            AppOpsManager.MODE_ALLOWED, // BLUETOOTH_SCAN
-            AppOpsManager.MODE_ALLOWED, // USE_BIOMETRIC
-            AppOpsManager.MODE_ALLOWED, // ACTIVITY_RECOGNITION
-            AppOpsManager.MODE_DEFAULT, // SMS_FINANCIAL_TRANSACTIONS
-            AppOpsManager.MODE_ALLOWED, // READ_MEDIA_AUDIO
-            AppOpsManager.MODE_ERRORED, // WRITE_MEDIA_AUDIO
-            AppOpsManager.MODE_ALLOWED, // READ_MEDIA_VIDEO
-            AppOpsManager.MODE_ERRORED, // WRITE_MEDIA_VIDEO
-            AppOpsManager.MODE_ALLOWED, // READ_MEDIA_IMAGES
-            AppOpsManager.MODE_ERRORED, // WRITE_MEDIA_IMAGES
-            AppOpsManager.MODE_DEFAULT, // LEGACY_STORAGE
-            AppOpsManager.MODE_ALLOWED, // ACCESS_ACCESSIBILITY
-            AppOpsManager.MODE_ERRORED, // READ_DEVICE_IDENTIFIERS
-            AppOpsManager.MODE_ALLOWED, // ALLOW_MEDIA_LOCATION
-            AppOpsManager.MODE_DEFAULT, // QUERY_ALL_PACKAGES
-            AppOpsManager.MODE_DEFAULT, // MANAGE_EXTERNAL_STORAGE
-            AppOpsManager.MODE_DEFAULT, // INTERACT_ACROSS_PROFILES
-            AppOpsManager.MODE_IGNORED, // ACTIVATE_PLATFORM_VPN
-            AppOpsManager.MODE_DEFAULT, // LOADER_USAGE_STATS
-            AppOpsManager.MODE_IGNORED, // deprecated operation
-            AppOpsManager.MODE_DEFAULT, // OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED
-            AppOpsManager.MODE_ALLOWED, // OP_AUTO_REVOKE_MANAGED_BY_INSTALLER
-            AppOpsManager.MODE_ERRORED, // OP_NO_ISOLATED_STORAGE
-            AppOpsManager.MODE_ALLOWED, // PHONE_CALL_MICROPHONE
-            AppOpsManager.MODE_ALLOWED, // PHONE_CALL_CAMERA
-            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
-            AppOpsManager.MODE_ALLOWED, // RECORD_AUDIO_OUTPUT
-            AppOpsManager.MODE_DEFAULT, // SCHEDULE_EXACT_ALARM
-            AppOpsManager.MODE_ALLOWED, // ACCESS_FINE_LOCATION_SOURCE
-            AppOpsManager.MODE_ALLOWED, // ACCESS_COARSE_LOCATION_SOURCE
-            AppOpsManager.MODE_DEFAULT, // MANAGE_MEDIA
-            AppOpsManager.MODE_ALLOWED, // BLUETOOTH_CONNECT
-            AppOpsManager.MODE_ALLOWED, // UWB_RANGING
-            AppOpsManager.MODE_ALLOWED, // ACTIVITY_RECOGNITION_SOURCE
-            AppOpsManager.MODE_ALLOWED, // BLUETOOTH_ADVERTISE
-            AppOpsManager.MODE_ALLOWED, // RECORD_INCOMING_PHONE_AUDIO
-            AppOpsManager.MODE_ALLOWED, // NEARBY_WIFI_DEVICES
-            AppOpsManager.MODE_ALLOWED, // ESTABLISH_VPN_SERVICE
-            AppOpsManager.MODE_ALLOWED, // ESTABLISH_VPN_MANAGER
-            AppOpsManager.MODE_ALLOWED, // ACCESS_RESTRICTED_SETTINGS,
-            AppOpsManager.MODE_ALLOWED, // RECEIVE_SOUNDTRIGGER_AUDIO
-    };
-
-    /**
-     * This specifies whether each option is allowed to be reset
-     * when resetting all app preferences.  Disable reset for
-     * app ops that are under strong control of some part of the
-     * system (such as OP_WRITE_SMS, which should be allowed only
-     * for whichever app is selected as the current SMS app).
-     */
-    private static boolean[] sOpDisableReset = new boolean[] {
-            false, // COARSE_LOCATION
-            false, // FINE_LOCATION
-            false, // GPS
-            false, // VIBRATE
-            false, // READ_CONTACTS
-            false, // WRITE_CONTACTS
-            false, // READ_CALL_LOG
-            false, // WRITE_CALL_LOG
-            false, // READ_CALENDAR
-            false, // WRITE_CALENDAR
-            false, // WIFI_SCAN
-            false, // POST_NOTIFICATION
-            false, // NEIGHBORING_CELLS
-            false, // CALL_PHONE
-            true, // READ_SMS
-            true, // WRITE_SMS
-            true, // RECEIVE_SMS
-            false, // RECEIVE_EMERGENCY_BROADCAST
-            false, // RECEIVE_MMS
-            true, // RECEIVE_WAP_PUSH
-            true, // SEND_SMS
-            false, // READ_ICC_SMS
-            false, // WRITE_ICC_SMS
-            false, // WRITE_SETTINGS
-            false, // SYSTEM_ALERT_WINDOW
-            false, // ACCESS_NOTIFICATIONS
-            false, // CAMERA
-            false, // RECORD_AUDIO
-            false, // PLAY_AUDIO
-            false, // READ_CLIPBOARD
-            false, // WRITE_CLIPBOARD
-            false, // TAKE_MEDIA_BUTTONS
-            false, // TAKE_AUDIO_FOCUS
-            false, // AUDIO_MASTER_VOLUME
-            false, // AUDIO_VOICE_VOLUME
-            false, // AUDIO_RING_VOLUME
-            false, // AUDIO_MEDIA_VOLUME
-            false, // AUDIO_ALARM_VOLUME
-            false, // AUDIO_NOTIFICATION_VOLUME
-            false, // AUDIO_BLUETOOTH_VOLUME
-            false, // WAKE_LOCK
-            false, // MONITOR_LOCATION
-            false, // MONITOR_HIGH_POWER_LOCATION
-            false, // GET_USAGE_STATS
-            false, // MUTE_MICROPHONE
-            false, // TOAST_WINDOW
-            false, // PROJECT_MEDIA
-            false, // ACTIVATE_VPN
-            false, // WRITE_WALLPAPER
-            false, // ASSIST_STRUCTURE
-            false, // ASSIST_SCREENSHOT
-            false, // READ_PHONE_STATE
-            false, // ADD_VOICEMAIL
-            false, // USE_SIP
-            false, // PROCESS_OUTGOING_CALLS
-            false, // USE_FINGERPRINT
-            false, // BODY_SENSORS
-            true, // READ_CELL_BROADCASTS
-            false, // MOCK_LOCATION
-            false, // READ_EXTERNAL_STORAGE
-            false, // WRITE_EXTERNAL_STORAGE
-            false, // TURN_SCREEN_ON
-            false, // GET_ACCOUNTS
-            false, // RUN_IN_BACKGROUND
-            false, // AUDIO_ACCESSIBILITY_VOLUME
-            false, // READ_PHONE_NUMBERS
-            false, // REQUEST_INSTALL_PACKAGES
-            false, // PICTURE_IN_PICTURE
-            false, // INSTANT_APP_START_FOREGROUND
-            false, // ANSWER_PHONE_CALLS
-            false, // RUN_ANY_IN_BACKGROUND
-            false, // CHANGE_WIFI_STATE
-            false, // REQUEST_DELETE_PACKAGES
-            false, // BIND_ACCESSIBILITY_SERVICE
-            false, // ACCEPT_HANDOVER
-            false, // MANAGE_IPSEC_TUNNELS
-            false, // START_FOREGROUND
-            false, // BLUETOOTH_SCAN
-            false, // USE_BIOMETRIC
-            false, // ACTIVITY_RECOGNITION
-            false, // SMS_FINANCIAL_TRANSACTIONS
-            false, // READ_MEDIA_AUDIO
-            false, // WRITE_MEDIA_AUDIO
-            false, // READ_MEDIA_VIDEO
-            true,  // WRITE_MEDIA_VIDEO
-            false, // READ_MEDIA_IMAGES
-            true,  // WRITE_MEDIA_IMAGES
-            true,  // LEGACY_STORAGE
-            false, // ACCESS_ACCESSIBILITY
-            false, // READ_DEVICE_IDENTIFIERS
-            false, // ACCESS_MEDIA_LOCATION
-            false, // QUERY_ALL_PACKAGES
-            false, // MANAGE_EXTERNAL_STORAGE
-            false, // INTERACT_ACROSS_PROFILES
-            false, // ACTIVATE_PLATFORM_VPN
-            false, // LOADER_USAGE_STATS
-            false, // deprecated operation
-            false, // AUTO_REVOKE_PERMISSIONS_IF_UNUSED
-            false, // AUTO_REVOKE_MANAGED_BY_INSTALLER
-            true, // NO_ISOLATED_STORAGE
-            false, // PHONE_CALL_MICROPHONE
-            false, // PHONE_CALL_CAMERA
-            false, // RECORD_AUDIO_HOTWORD
-            true, // MANAGE_ONGOING_CALLS
-            false, // MANAGE_CREDENTIALS
-            true, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
-            false, // RECORD_AUDIO_OUTPUT
-            false, // SCHEDULE_EXACT_ALARM
-            false, // ACCESS_FINE_LOCATION_SOURCE
-            false, // ACCESS_COARSE_LOCATION_SOURCE
-            false, // MANAGE_MEDIA
-            false, // BLUETOOTH_CONNECT
-            false, // UWB_RANGING
-            false, // ACTIVITY_RECOGNITION_SOURCE
-            false, // BLUETOOTH_ADVERTISE
-            false, // RECORD_INCOMING_PHONE_AUDIO
-            false, // NEARBY_WIFI_DEVICES
-            false, // OP_ESTABLISH_VPN_SERVICE
-            false, // OP_ESTABLISH_VPN_MANAGER
-            true, // ACCESS_RESTRICTED_SETTINGS
-            false, // RECEIVE_SOUNDTRIGGER_AUDIO
-    };
-
-    /**
-     * This specifies whether each option is only allowed to be read
-     * by apps with manage appops permission.
-     */
-    private static boolean[] sOpRestrictRead = new boolean[] {
-            false, // COARSE_LOCATION
-            false, // FINE_LOCATION
-            false, // GPS
-            false, // VIBRATE
-            false, // READ_CONTACTS
-            false, // WRITE_CONTACTS
-            false, // READ_CALL_LOG
-            false, // WRITE_CALL_LOG
-            false, // READ_CALENDAR
-            false, // WRITE_CALENDAR
-            false, // WIFI_SCAN
-            false, // POST_NOTIFICATION
-            false, // NEIGHBORING_CELLS
-            false, // CALL_PHONE
-            false, // READ_SMS
-            false, // WRITE_SMS
-            false, // RECEIVE_SMS
-            false, // RECEIVE_EMERGENCY_BROADCAST
-            false, // RECEIVE_MMS
-            false, // RECEIVE_WAP_PUSH
-            false, // SEND_SMS
-            false, // READ_ICC_SMS
-            false, // WRITE_ICC_SMS
-            false, // WRITE_SETTINGS
-            false, // SYSTEM_ALERT_WINDOW
-            false, // ACCESS_NOTIFICATIONS
-            false, // CAMERA
-            false, // RECORD_AUDIO
-            false, // PLAY_AUDIO
-            false, // READ_CLIPBOARD
-            false, // WRITE_CLIPBOARD
-            false, // TAKE_MEDIA_BUTTONS
-            false, // TAKE_AUDIO_FOCUS
-            false, // AUDIO_MASTER_VOLUME
-            false, // AUDIO_VOICE_VOLUME
-            false, // AUDIO_RING_VOLUME
-            false, // AUDIO_MEDIA_VOLUME
-            false, // AUDIO_ALARM_VOLUME
-            false, // AUDIO_NOTIFICATION_VOLUME
-            false, // AUDIO_BLUETOOTH_VOLUME
-            false, // WAKE_LOCK
-            false, // MONITOR_LOCATION
-            false, // MONITOR_HIGH_POWER_LOCATION
-            false, // GET_USAGE_STATS
-            false, // MUTE_MICROPHONE
-            false, // TOAST_WINDOW
-            false, // PROJECT_MEDIA
-            false, // ACTIVATE_VPN
-            false, // WRITE_WALLPAPER
-            false, // ASSIST_STRUCTURE
-            false, // ASSIST_SCREENSHOT
-            false, // READ_PHONE_STATE
-            false, // ADD_VOICEMAIL
-            false, // USE_SIP
-            false, // PROCESS_OUTGOING_CALLS
-            false, // USE_FINGERPRINT
-            false, // BODY_SENSORS
-            false, // READ_CELL_BROADCASTS
-            false, // MOCK_LOCATION
-            false, // READ_EXTERNAL_STORAGE
-            false, // WRITE_EXTERNAL_STORAGE
-            false, // TURN_SCREEN_ON
-            false, // GET_ACCOUNTS
-            false, // RUN_IN_BACKGROUND
-            false, // AUDIO_ACCESSIBILITY_VOLUME
-            false, // READ_PHONE_NUMBERS
-            false, // REQUEST_INSTALL_PACKAGES
-            false, // PICTURE_IN_PICTURE
-            false, // INSTANT_APP_START_FOREGROUND
-            false, // ANSWER_PHONE_CALLS
-            false, // RUN_ANY_IN_BACKGROUND
-            false, // CHANGE_WIFI_STATE
-            false, // REQUEST_DELETE_PACKAGES
-            false, // BIND_ACCESSIBILITY_SERVICE
-            false, // ACCEPT_HANDOVER
-            false, // MANAGE_IPSEC_TUNNELS
-            false, // START_FOREGROUND
-            false, // BLUETOOTH_SCAN
-            false, // USE_BIOMETRIC
-            false, // ACTIVITY_RECOGNITION
-            false, // SMS_FINANCIAL_TRANSACTIONS
-            false, // READ_MEDIA_AUDIO
-            false, // WRITE_MEDIA_AUDIO
-            false, // READ_MEDIA_VIDEO
-            false,  // WRITE_MEDIA_VIDEO
-            false, // READ_MEDIA_IMAGES
-            false,  // WRITE_MEDIA_IMAGES
-            false,  // LEGACY_STORAGE
-            false, // ACCESS_ACCESSIBILITY
-            false, // READ_DEVICE_IDENTIFIERS
-            false, // ACCESS_MEDIA_LOCATION
-            false, // QUERY_ALL_PACKAGES
-            false, // MANAGE_EXTERNAL_STORAGE
-            false, // INTERACT_ACROSS_PROFILES
-            false, // ACTIVATE_PLATFORM_VPN
-            false, // LOADER_USAGE_STATS
-            false, // deprecated operation
-            false, // AUTO_REVOKE_PERMISSIONS_IF_UNUSED
-            false, // AUTO_REVOKE_MANAGED_BY_INSTALLER
-            false, // NO_ISOLATED_STORAGE
-            false, // PHONE_CALL_MICROPHONE
-            false, // PHONE_CALL_CAMERA
-            false, // RECORD_AUDIO_HOTWORD
-            false, // MANAGE_ONGOING_CALLS
-            false, // MANAGE_CREDENTIALS
-            false, // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER
-            false, // RECORD_AUDIO_OUTPUT
-            false, // SCHEDULE_EXACT_ALARM
-            false, // ACCESS_FINE_LOCATION_SOURCE
-            false, // ACCESS_COARSE_LOCATION_SOURCE
-            false, // MANAGE_MEDIA
-            false, // BLUETOOTH_CONNECT
-            false, // UWB_RANGING
-            false, // ACTIVITY_RECOGNITION_SOURCE
-            false, // BLUETOOTH_ADVERTISE
-            false, // RECORD_INCOMING_PHONE_AUDIO
-            false, // NEARBY_WIFI_DEVICES
-            false, // OP_ESTABLISH_VPN_SERVICE
-            false, // OP_ESTABLISH_VPN_MANAGER
-            true, // ACCESS_RESTRICTED_SETTINGS
-            false, // RECEIVE_SOUNDTRIGGER_AUDIO
+    static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{
+        new AppOpInfo.Builder(OP_COARSE_LOCATION, OPSTR_COARSE_LOCATION, "COARSE_LOCATION")
+            .setPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION)
+            .setRestriction(UserManager.DISALLOW_SHARE_LOCATION)
+            .setAllowSystemRestrictionBypass(new RestrictionBypass(true, false, false))
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_FINE_LOCATION, OPSTR_FINE_LOCATION, "FINE_LOCATION")
+            .setPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+            .setRestriction(UserManager.DISALLOW_SHARE_LOCATION)
+            .setAllowSystemRestrictionBypass(new RestrictionBypass(true, false, false))
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_GPS, OPSTR_GPS, "GPS")
+            .setSwitchCode(OP_COARSE_LOCATION)
+            .setRestriction(UserManager.DISALLOW_SHARE_LOCATION)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_VIBRATE, OPSTR_VIBRATE, "VIBRATE")
+            .setSwitchCode(OP_VIBRATE).setPermission(android.Manifest.permission.VIBRATE)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_READ_CONTACTS, OPSTR_READ_CONTACTS, "READ_CONTACTS")
+            .setPermission(android.Manifest.permission.READ_CONTACTS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WRITE_CONTACTS, OPSTR_WRITE_CONTACTS, "WRITE_CONTACTS")
+            .setPermission(android.Manifest.permission.WRITE_CONTACTS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_READ_CALL_LOG, OPSTR_READ_CALL_LOG, "READ_CALL_LOG")
+            .setPermission(android.Manifest.permission.READ_CALL_LOG)
+            .setRestriction(UserManager.DISALLOW_OUTGOING_CALLS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WRITE_CALL_LOG, OPSTR_WRITE_CALL_LOG, "WRITE_CALL_LOG")
+            .setPermission(android.Manifest.permission.WRITE_CALL_LOG)
+            .setRestriction(UserManager.DISALLOW_OUTGOING_CALLS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_READ_CALENDAR, OPSTR_READ_CALENDAR, "READ_CALENDAR")
+            .setPermission(android.Manifest.permission.READ_CALENDAR)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WRITE_CALENDAR, OPSTR_WRITE_CALENDAR, "WRITE_CALENDAR")
+            .setPermission(android.Manifest.permission.WRITE_CALENDAR)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WIFI_SCAN, OPSTR_WIFI_SCAN, "WIFI_SCAN")
+            .setSwitchCode(OP_COARSE_LOCATION)
+            .setPermission(android.Manifest.permission.ACCESS_WIFI_STATE)
+            .setRestriction(UserManager.DISALLOW_SHARE_LOCATION)
+            .setAllowSystemRestrictionBypass(new RestrictionBypass(false, true, false))
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_POST_NOTIFICATION, OPSTR_POST_NOTIFICATION, "POST_NOTIFICATION")
+            .setPermission(android.Manifest.permission.POST_NOTIFICATIONS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_NEIGHBORING_CELLS, OPSTR_NEIGHBORING_CELLS, "NEIGHBORING_CELLS")
+            .setSwitchCode(OP_COARSE_LOCATION).setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_CALL_PHONE, OPSTR_CALL_PHONE, "CALL_PHONE")
+            .setSwitchCode(OP_CALL_PHONE).setPermission(android.Manifest.permission.CALL_PHONE)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_READ_SMS, OPSTR_READ_SMS, "READ_SMS")
+            .setPermission(android.Manifest.permission.READ_SMS)
+            .setRestriction(UserManager.DISALLOW_SMS).setDefaultMode(AppOpsManager.MODE_ALLOWED)
+            .setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_WRITE_SMS, OPSTR_WRITE_SMS, "WRITE_SMS")
+            .setRestriction(UserManager.DISALLOW_SMS)
+            .setDefaultMode(AppOpsManager.MODE_IGNORED).setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_RECEIVE_SMS, OPSTR_RECEIVE_SMS, "RECEIVE_SMS")
+            .setPermission(android.Manifest.permission.RECEIVE_SMS)
+            .setRestriction(UserManager.DISALLOW_SMS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_RECEIVE_EMERGECY_SMS, OPSTR_RECEIVE_EMERGENCY_BROADCAST,
+                "RECEIVE_EMERGENCY_BROADCAST").setSwitchCode(OP_RECEIVE_SMS)
+            .setPermission(android.Manifest.permission.RECEIVE_EMERGENCY_BROADCAST)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_RECEIVE_MMS, OPSTR_RECEIVE_MMS, "RECEIVE_MMS")
+            .setPermission(android.Manifest.permission.RECEIVE_MMS)
+            .setRestriction(UserManager.DISALLOW_SMS).setDefaultMode(AppOpsManager.MODE_ALLOWED)
+            .build(),
+        new AppOpInfo.Builder(OP_RECEIVE_WAP_PUSH, OPSTR_RECEIVE_WAP_PUSH, "RECEIVE_WAP_PUSH")
+            .setPermission(android.Manifest.permission.RECEIVE_WAP_PUSH)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_SEND_SMS, OPSTR_SEND_SMS, "SEND_SMS")
+            .setPermission(android.Manifest.permission.SEND_SMS)
+            .setRestriction(UserManager.DISALLOW_SMS).setDefaultMode(AppOpsManager.MODE_ALLOWED)
+            .setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_READ_ICC_SMS, OPSTR_READ_ICC_SMS, "READ_ICC_SMS")
+            .setSwitchCode(OP_READ_SMS).setPermission(android.Manifest.permission.READ_SMS)
+            .setRestriction(UserManager.DISALLOW_SMS).setDefaultMode(AppOpsManager.MODE_ALLOWED)
+            .build(),
+        new AppOpInfo.Builder(OP_WRITE_ICC_SMS, OPSTR_WRITE_ICC_SMS, "WRITE_ICC_SMS")
+            .setSwitchCode(OP_WRITE_SMS).setRestriction(UserManager.DISALLOW_SMS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WRITE_SETTINGS, OPSTR_WRITE_SETTINGS, "WRITE_SETTINGS")
+            .setPermission(android.Manifest.permission.WRITE_SETTINGS).build(),
+        new AppOpInfo.Builder(OP_SYSTEM_ALERT_WINDOW, OPSTR_SYSTEM_ALERT_WINDOW,
+                "SYSTEM_ALERT_WINDOW")
+            .setPermission(android.Manifest.permission.SYSTEM_ALERT_WINDOW)
+            .setRestriction(UserManager.DISALLOW_CREATE_WINDOWS)
+            .setAllowSystemRestrictionBypass(new RestrictionBypass(false, true, false))
+            .setDefaultMode(getSystemAlertWindowDefault()).build(),
+        new AppOpInfo.Builder(OP_ACCESS_NOTIFICATIONS, OPSTR_ACCESS_NOTIFICATIONS,
+                "ACCESS_NOTIFICATIONS")
+            .setPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_CAMERA, OPSTR_CAMERA, "CAMERA")
+            .setPermission(android.Manifest.permission.CAMERA)
+            .setRestriction(UserManager.DISALLOW_CAMERA)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_RECORD_AUDIO, OPSTR_RECORD_AUDIO, "RECORD_AUDIO")
+            .setPermission(android.Manifest.permission.RECORD_AUDIO)
+            .setRestriction(UserManager.DISALLOW_RECORD_AUDIO)
+            .setAllowSystemRestrictionBypass(new RestrictionBypass(false, false, true))
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_PLAY_AUDIO, OPSTR_PLAY_AUDIO, "PLAY_AUDIO")
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_READ_CLIPBOARD, OPSTR_READ_CLIPBOARD, "READ_CLIPBOARD")
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WRITE_CLIPBOARD, OPSTR_WRITE_CLIPBOARD, "WRITE_CLIPBOARD")
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_TAKE_MEDIA_BUTTONS, OPSTR_TAKE_MEDIA_BUTTONS, "TAKE_MEDIA_BUTTONS")
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED)
+            .build(),
+        new AppOpInfo.Builder(OP_TAKE_AUDIO_FOCUS, OPSTR_TAKE_AUDIO_FOCUS, "TAKE_AUDIO_FOCUS")
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_AUDIO_MASTER_VOLUME, OPSTR_AUDIO_MASTER_VOLUME,
+                "AUDIO_MASTER_VOLUME").setSwitchCode(OP_AUDIO_MASTER_VOLUME)
+            .setRestriction(UserManager.DISALLOW_ADJUST_VOLUME)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_AUDIO_VOICE_VOLUME, OPSTR_AUDIO_VOICE_VOLUME, "AUDIO_VOICE_VOLUME")
+            .setRestriction(UserManager.DISALLOW_ADJUST_VOLUME)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_AUDIO_RING_VOLUME, OPSTR_AUDIO_RING_VOLUME, "AUDIO_RING_VOLUME")
+            .setRestriction(UserManager.DISALLOW_ADJUST_VOLUME)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_AUDIO_MEDIA_VOLUME, OPSTR_AUDIO_MEDIA_VOLUME, "AUDIO_MEDIA_VOLUME")
+            .setRestriction(UserManager.DISALLOW_ADJUST_VOLUME)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_AUDIO_ALARM_VOLUME, OPSTR_AUDIO_ALARM_VOLUME, "AUDIO_ALARM_VOLUME")
+            .setRestriction(UserManager.DISALLOW_ADJUST_VOLUME)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_AUDIO_NOTIFICATION_VOLUME, OPSTR_AUDIO_NOTIFICATION_VOLUME,
+                "AUDIO_NOTIFICATION_VOLUME").setSwitchCode(OP_AUDIO_NOTIFICATION_VOLUME)
+            .setRestriction(UserManager.DISALLOW_ADJUST_VOLUME)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_AUDIO_BLUETOOTH_VOLUME, OPSTR_AUDIO_BLUETOOTH_VOLUME,
+                "AUDIO_BLUETOOTH_VOLUME").setRestriction(UserManager.DISALLOW_ADJUST_VOLUME)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WAKE_LOCK, OPSTR_WAKE_LOCK, "WAKE_LOCK")
+            .setPermission(android.Manifest.permission.WAKE_LOCK)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_MONITOR_LOCATION, OPSTR_MONITOR_LOCATION, "MONITOR_LOCATION")
+            .setSwitchCode(OP_COARSE_LOCATION)
+            .setRestriction(UserManager.DISALLOW_SHARE_LOCATION)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_MONITOR_HIGH_POWER_LOCATION, OPSTR_MONITOR_HIGH_POWER_LOCATION,
+                "MONITOR_HIGH_POWER_LOCATION").setSwitchCode(OP_COARSE_LOCATION)
+            .setRestriction(UserManager.DISALLOW_SHARE_LOCATION)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_GET_USAGE_STATS, OPSTR_GET_USAGE_STATS, "GET_USAGE_STATS")
+            .setPermission(android.Manifest.permission.PACKAGE_USAGE_STATS).build(),
+        new AppOpInfo.Builder(OP_MUTE_MICROPHONE, OPSTR_MUTE_MICROPHONE, "MUTE_MICROPHONE")
+            .setRestriction(UserManager.DISALLOW_UNMUTE_MICROPHONE)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_TOAST_WINDOW, OPSTR_TOAST_WINDOW, "TOAST_WINDOW")
+            .setRestriction(UserManager.DISALLOW_CREATE_WINDOWS)
+            .setAllowSystemRestrictionBypass(new RestrictionBypass(false, true, false))
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_PROJECT_MEDIA, OPSTR_PROJECT_MEDIA, "PROJECT_MEDIA")
+            .setDefaultMode(AppOpsManager.MODE_IGNORED).build(),
+        new AppOpInfo.Builder(OP_ACTIVATE_VPN, OPSTR_ACTIVATE_VPN, "ACTIVATE_VPN")
+            .setDefaultMode(AppOpsManager.MODE_IGNORED).build(),
+        new AppOpInfo.Builder(OP_WRITE_WALLPAPER, OPSTR_WRITE_WALLPAPER, "WRITE_WALLPAPER")
+            .setRestriction(UserManager.DISALLOW_WALLPAPER)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_ASSIST_STRUCTURE, OPSTR_ASSIST_STRUCTURE, "ASSIST_STRUCTURE")
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_ASSIST_SCREENSHOT, OPSTR_ASSIST_SCREENSHOT, "ASSIST_SCREENSHOT")
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED)
+            .build(),
+        new AppOpInfo.Builder(OP_READ_PHONE_STATE, OPSTR_READ_PHONE_STATE, "READ_PHONE_STATE")
+            .setPermission(Manifest.permission.READ_PHONE_STATE)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_ADD_VOICEMAIL, OPSTR_ADD_VOICEMAIL, "ADD_VOICEMAIL")
+            .setPermission(Manifest.permission.ADD_VOICEMAIL)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_USE_SIP, OPSTR_USE_SIP, "USE_SIP")
+            .setPermission(Manifest.permission.USE_SIP)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_PROCESS_OUTGOING_CALLS, OPSTR_PROCESS_OUTGOING_CALLS,
+                "PROCESS_OUTGOING_CALLS").setSwitchCode(OP_PROCESS_OUTGOING_CALLS)
+            .setPermission(Manifest.permission.PROCESS_OUTGOING_CALLS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_USE_FINGERPRINT, OPSTR_USE_FINGERPRINT, "USE_FINGERPRINT")
+            .setPermission(Manifest.permission.USE_FINGERPRINT)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_BODY_SENSORS, OPSTR_BODY_SENSORS, "BODY_SENSORS")
+            .setPermission(Manifest.permission.BODY_SENSORS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_READ_CELL_BROADCASTS, OPSTR_READ_CELL_BROADCASTS,
+                "READ_CELL_BROADCASTS").setPermission(Manifest.permission.READ_CELL_BROADCASTS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_MOCK_LOCATION, OPSTR_MOCK_LOCATION, "MOCK_LOCATION")
+            .setDefaultMode(AppOpsManager.MODE_ERRORED).build(),
+        new AppOpInfo.Builder(OP_READ_EXTERNAL_STORAGE, OPSTR_READ_EXTERNAL_STORAGE,
+                "READ_EXTERNAL_STORAGE").setPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WRITE_EXTERNAL_STORAGE, OPSTR_WRITE_EXTERNAL_STORAGE,
+                "WRITE_EXTERNAL_STORAGE").setPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_TURN_SCREEN_ON, OPSTR_TURN_SCREEN_ON, "TURN_SCREEN_ON")
+            .setPermission(Manifest.permission.TURN_SCREEN_ON)
+            .setDefaultMode(AppOpsManager.MODE_ERRORED).build(),
+        new AppOpInfo.Builder(OP_GET_ACCOUNTS, OPSTR_GET_ACCOUNTS, "GET_ACCOUNTS")
+            .setPermission(Manifest.permission.GET_ACCOUNTS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_RUN_IN_BACKGROUND, OPSTR_RUN_IN_BACKGROUND, "RUN_IN_BACKGROUND")
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED)
+            .build(),
+        new AppOpInfo.Builder(OP_AUDIO_ACCESSIBILITY_VOLUME, OPSTR_AUDIO_ACCESSIBILITY_VOLUME,
+                "AUDIO_ACCESSIBILITY_VOLUME")
+            .setRestriction(UserManager.DISALLOW_ADJUST_VOLUME)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_READ_PHONE_NUMBERS, OPSTR_READ_PHONE_NUMBERS, "READ_PHONE_NUMBERS")
+            .setPermission(Manifest.permission.READ_PHONE_NUMBERS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_REQUEST_INSTALL_PACKAGES, OPSTR_REQUEST_INSTALL_PACKAGES,
+                "REQUEST_INSTALL_PACKAGES").setSwitchCode(OP_REQUEST_INSTALL_PACKAGES)
+            .setPermission(Manifest.permission.REQUEST_INSTALL_PACKAGES).build(),
+        new AppOpInfo.Builder(OP_PICTURE_IN_PICTURE, OPSTR_PICTURE_IN_PICTURE, "PICTURE_IN_PICTURE")
+            .setSwitchCode(OP_PICTURE_IN_PICTURE).setDefaultMode(AppOpsManager.MODE_ALLOWED)
+            .build(),
+        new AppOpInfo.Builder(OP_INSTANT_APP_START_FOREGROUND, OPSTR_INSTANT_APP_START_FOREGROUND,
+                "INSTANT_APP_START_FOREGROUND")
+            .setPermission(Manifest.permission.INSTANT_APP_FOREGROUND_SERVICE).build(),
+        new AppOpInfo.Builder(OP_ANSWER_PHONE_CALLS, OPSTR_ANSWER_PHONE_CALLS, "ANSWER_PHONE_CALLS")
+            .setSwitchCode(OP_ANSWER_PHONE_CALLS)
+            .setPermission(Manifest.permission.ANSWER_PHONE_CALLS)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_RUN_ANY_IN_BACKGROUND, OPSTR_RUN_ANY_IN_BACKGROUND,
+                "RUN_ANY_IN_BACKGROUND")
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_CHANGE_WIFI_STATE, OPSTR_CHANGE_WIFI_STATE, "CHANGE_WIFI_STATE")
+            .setSwitchCode(OP_CHANGE_WIFI_STATE)
+            .setPermission(Manifest.permission.CHANGE_WIFI_STATE)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_REQUEST_DELETE_PACKAGES, OPSTR_REQUEST_DELETE_PACKAGES,
+                "REQUEST_DELETE_PACKAGES")
+            .setPermission(Manifest.permission.REQUEST_DELETE_PACKAGES)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_BIND_ACCESSIBILITY_SERVICE, OPSTR_BIND_ACCESSIBILITY_SERVICE,
+                "BIND_ACCESSIBILITY_SERVICE")
+            .setPermission(Manifest.permission.BIND_ACCESSIBILITY_SERVICE)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_ACCEPT_HANDOVER, OPSTR_ACCEPT_HANDOVER, "ACCEPT_HANDOVER")
+            .setSwitchCode(OP_ACCEPT_HANDOVER)
+            .setPermission(Manifest.permission.ACCEPT_HANDOVER)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_MANAGE_IPSEC_TUNNELS, OPSTR_MANAGE_IPSEC_TUNNELS,
+                "MANAGE_IPSEC_TUNNELS")
+            .setPermission(Manifest.permission.MANAGE_IPSEC_TUNNELS)
+            .setDefaultMode(AppOpsManager.MODE_ERRORED).build(),
+        new AppOpInfo.Builder(OP_START_FOREGROUND, OPSTR_START_FOREGROUND, "START_FOREGROUND")
+            .setPermission(Manifest.permission.FOREGROUND_SERVICE)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_BLUETOOTH_SCAN, OPSTR_BLUETOOTH_SCAN, "BLUETOOTH_SCAN")
+            .setPermission(Manifest.permission.BLUETOOTH_SCAN)
+            .setAllowSystemRestrictionBypass(new RestrictionBypass(false, true, false))
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_USE_BIOMETRIC, OPSTR_USE_BIOMETRIC, "USE_BIOMETRIC")
+            .setPermission(Manifest.permission.USE_BIOMETRIC)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_ACTIVITY_RECOGNITION, OPSTR_ACTIVITY_RECOGNITION,
+                "ACTIVITY_RECOGNITION")
+            .setPermission(Manifest.permission.ACTIVITY_RECOGNITION)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_SMS_FINANCIAL_TRANSACTIONS, OPSTR_SMS_FINANCIAL_TRANSACTIONS,
+                "SMS_FINANCIAL_TRANSACTIONS")
+            .setPermission(Manifest.permission.SMS_FINANCIAL_TRANSACTIONS)
+            .setRestriction(UserManager.DISALLOW_SMS).build(),
+        new AppOpInfo.Builder(OP_READ_MEDIA_AUDIO, OPSTR_READ_MEDIA_AUDIO, "READ_MEDIA_AUDIO")
+            .setPermission(Manifest.permission.READ_MEDIA_AUDIO)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WRITE_MEDIA_AUDIO, OPSTR_WRITE_MEDIA_AUDIO, "WRITE_MEDIA_AUDIO")
+            .setDefaultMode(AppOpsManager.MODE_ERRORED).build(),
+        new AppOpInfo.Builder(OP_READ_MEDIA_VIDEO, OPSTR_READ_MEDIA_VIDEO, "READ_MEDIA_VIDEO")
+            .setPermission(Manifest.permission.READ_MEDIA_VIDEO)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WRITE_MEDIA_VIDEO, OPSTR_WRITE_MEDIA_VIDEO, "WRITE_MEDIA_VIDEO")
+            .setDefaultMode(AppOpsManager.MODE_ERRORED).setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_READ_MEDIA_IMAGES, OPSTR_READ_MEDIA_IMAGES, "READ_MEDIA_IMAGES")
+            .setPermission(Manifest.permission.READ_MEDIA_IMAGES)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WRITE_MEDIA_IMAGES, OPSTR_WRITE_MEDIA_IMAGES, "WRITE_MEDIA_IMAGES")
+            .setDefaultMode(AppOpsManager.MODE_ERRORED).setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_LEGACY_STORAGE, OPSTR_LEGACY_STORAGE, "LEGACY_STORAGE")
+            .setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_ACCESS_ACCESSIBILITY, OPSTR_ACCESS_ACCESSIBILITY,
+                "ACCESS_ACCESSIBILITY").setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_READ_DEVICE_IDENTIFIERS, OPSTR_READ_DEVICE_IDENTIFIERS,
+                "READ_DEVICE_IDENTIFIERS").setDefaultMode(AppOpsManager.MODE_ERRORED).build(),
+        new AppOpInfo.Builder(OP_ACCESS_MEDIA_LOCATION, OPSTR_ACCESS_MEDIA_LOCATION,
+                "ACCESS_MEDIA_LOCATION").setPermission(Manifest.permission.ACCESS_MEDIA_LOCATION)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_QUERY_ALL_PACKAGES, OPSTR_QUERY_ALL_PACKAGES, "QUERY_ALL_PACKAGES")
+            .build(),
+        new AppOpInfo.Builder(OP_MANAGE_EXTERNAL_STORAGE, OPSTR_MANAGE_EXTERNAL_STORAGE,
+                "MANAGE_EXTERNAL_STORAGE")
+            .setPermission(Manifest.permission.MANAGE_EXTERNAL_STORAGE).build(),
+        new AppOpInfo.Builder(OP_INTERACT_ACROSS_PROFILES, OPSTR_INTERACT_ACROSS_PROFILES,
+                "INTERACT_ACROSS_PROFILES")
+            .setPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES).build(),
+        new AppOpInfo.Builder(OP_ACTIVATE_PLATFORM_VPN, OPSTR_ACTIVATE_PLATFORM_VPN,
+                "ACTIVATE_PLATFORM_VPN").setDefaultMode(AppOpsManager.MODE_IGNORED).build(),
+        new AppOpInfo.Builder(OP_LOADER_USAGE_STATS, OPSTR_LOADER_USAGE_STATS, "LOADER_USAGE_STATS")
+            .setPermission(android.Manifest.permission.LOADER_USAGE_STATS).build(),
+        new AppOpInfo.Builder(OP_NONE, "", "").setDefaultMode(AppOpsManager.MODE_IGNORED).build(),
+        new AppOpInfo.Builder(OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED,
+                OPSTR_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, "AUTO_REVOKE_PERMISSIONS_IF_UNUSED")
+            .build(),
+        new AppOpInfo.Builder(OP_AUTO_REVOKE_MANAGED_BY_INSTALLER,
+                OPSTR_AUTO_REVOKE_MANAGED_BY_INSTALLER, "AUTO_REVOKE_MANAGED_BY_INSTALLER")
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_NO_ISOLATED_STORAGE, OPSTR_NO_ISOLATED_STORAGE,
+                "NO_ISOLATED_STORAGE").setDefaultMode(AppOpsManager.MODE_ERRORED)
+            .setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_PHONE_CALL_MICROPHONE, OPSTR_PHONE_CALL_MICROPHONE,
+                "PHONE_CALL_MICROPHONE").setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_PHONE_CALL_CAMERA, OPSTR_PHONE_CALL_CAMERA, "PHONE_CALL_CAMERA")
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_RECORD_AUDIO_HOTWORD, OPSTR_RECORD_AUDIO_HOTWORD,
+                "RECORD_AUDIO_HOTWORD").setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_MANAGE_ONGOING_CALLS, OPSTR_MANAGE_ONGOING_CALLS,
+                "MANAGE_ONGOING_CALLS").setPermission(Manifest.permission.MANAGE_ONGOING_CALLS)
+            .setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_MANAGE_CREDENTIALS, OPSTR_MANAGE_CREDENTIALS, "MANAGE_CREDENTIALS")
+            .build(),
+        new AppOpInfo.Builder(OP_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER,
+                OPSTR_USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER, "USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER")
+            .setPermission(Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER)
+            .setDisableReset(true).build(),
+        new AppOpInfo.Builder(OP_RECORD_AUDIO_OUTPUT, OPSTR_RECORD_AUDIO_OUTPUT,
+                "RECORD_AUDIO_OUTPUT").setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_SCHEDULE_EXACT_ALARM, OPSTR_SCHEDULE_EXACT_ALARM,
+                "SCHEDULE_EXACT_ALARM").setPermission(Manifest.permission.SCHEDULE_EXACT_ALARM)
+            .build(),
+        new AppOpInfo.Builder(OP_FINE_LOCATION_SOURCE, OPSTR_FINE_LOCATION_SOURCE,
+                "FINE_LOCATION_SOURCE").setSwitchCode(OP_FINE_LOCATION)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_COARSE_LOCATION_SOURCE, OPSTR_COARSE_LOCATION_SOURCE,
+                "COARSE_LOCATION_SOURCE").setSwitchCode(OP_COARSE_LOCATION)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_MANAGE_MEDIA, OPSTR_MANAGE_MEDIA, "MANAGE_MEDIA")
+            .setPermission(Manifest.permission.MANAGE_MEDIA).build(),
+        new AppOpInfo.Builder(OP_BLUETOOTH_CONNECT, OPSTR_BLUETOOTH_CONNECT, "BLUETOOTH_CONNECT")
+            .setPermission(Manifest.permission.BLUETOOTH_CONNECT)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_UWB_RANGING, OPSTR_UWB_RANGING, "UWB_RANGING")
+            .setPermission(Manifest.permission.UWB_RANGING)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_ACTIVITY_RECOGNITION_SOURCE, OPSTR_ACTIVITY_RECOGNITION_SOURCE,
+                "ACTIVITY_RECOGNITION_SOURCE")
+            .setSwitchCode(OP_ACTIVITY_RECOGNITION).setDefaultMode(AppOpsManager.MODE_ALLOWED)
+            .build(),
+        new AppOpInfo.Builder(OP_BLUETOOTH_ADVERTISE, OPSTR_BLUETOOTH_ADVERTISE,
+                "BLUETOOTH_ADVERTISE").setPermission(Manifest.permission.BLUETOOTH_ADVERTISE)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_RECORD_INCOMING_PHONE_AUDIO, OPSTR_RECORD_INCOMING_PHONE_AUDIO,
+                "RECORD_INCOMING_PHONE_AUDIO").setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_NEARBY_WIFI_DEVICES, OPSTR_NEARBY_WIFI_DEVICES,
+                "NEARBY_WIFI_DEVICES").setPermission(Manifest.permission.NEARBY_WIFI_DEVICES)
+            .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_ESTABLISH_VPN_SERVICE, OPSTR_ESTABLISH_VPN_SERVICE,
+                "ESTABLISH_VPN_SERVICE").setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_ESTABLISH_VPN_MANAGER, OPSTR_ESTABLISH_VPN_MANAGER,
+                "ESTABLISH_VPN_MANAGER").setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_ACCESS_RESTRICTED_SETTINGS, OPSTR_ACCESS_RESTRICTED_SETTINGS,
+                "ACCESS_RESTRICTED_SETTINGS").setDefaultMode(AppOpsManager.MODE_ALLOWED)
+            .setDisableReset(true).setRestrictRead(true).build(),
+        new AppOpInfo.Builder(OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO,
+                "RECEIVE_SOUNDTRIGGER_AUDIO").setDefaultMode(AppOpsManager.MODE_ALLOWED).build()
     };
 
     /**
@@ -3101,46 +2317,18 @@
     private static final @ShouldCollectNoteOp byte[] sAppOpsToNote = new byte[_NUM_OP];
 
     static {
-        if (sOpToSwitch.length != _NUM_OP) {
-            throw new IllegalStateException("sOpToSwitch length " + sOpToSwitch.length
+        if (sAppOpInfos.length != _NUM_OP) {
+            throw new IllegalStateException("mAppOpInfos length " + sAppOpInfos.length
                     + " should be " + _NUM_OP);
         }
-        if (sOpToString.length != _NUM_OP) {
-            throw new IllegalStateException("sOpToString length " + sOpToString.length
-                    + " should be " + _NUM_OP);
-        }
-        if (sOpNames.length != _NUM_OP) {
-            throw new IllegalStateException("sOpNames length " + sOpNames.length
-                    + " should be " + _NUM_OP);
-        }
-        if (sOpPerms.length != _NUM_OP) {
-            throw new IllegalStateException("sOpPerms length " + sOpPerms.length
-                    + " should be " + _NUM_OP);
-        }
-        if (sOpDefaultMode.length != _NUM_OP) {
-            throw new IllegalStateException("sOpDefaultMode length " + sOpDefaultMode.length
-                    + " should be " + _NUM_OP);
-        }
-        if (sOpDisableReset.length != _NUM_OP) {
-            throw new IllegalStateException("sOpDisableReset length " + sOpDisableReset.length
-                    + " should be " + _NUM_OP);
-        }
-        if (sOpRestrictions.length != _NUM_OP) {
-            throw new IllegalStateException("sOpRestrictions length " + sOpRestrictions.length
-                    + " should be " + _NUM_OP);
-        }
-        if (sOpAllowSystemRestrictionBypass.length != _NUM_OP) {
-            throw new IllegalStateException("sOpAllowSYstemRestrictionsBypass length "
-                    + sOpRestrictions.length + " should be " + _NUM_OP);
-        }
         for (int i=0; i<_NUM_OP; i++) {
-            if (sOpToString[i] != null) {
-                sOpStrToOp.put(sOpToString[i], i);
+            if (sAppOpInfos[i].name != null) {
+                sOpStrToOp.put(sAppOpInfos[i].name, i);
             }
         }
         for (int op : RUNTIME_AND_APPOP_PERMISSIONS_OPS) {
-            if (sOpPerms[op] != null) {
-                sPermToOp.put(sOpPerms[op], op);
+            if (sAppOpInfos[op].permission != null) {
+                sPermToOp.put(sAppOpInfos[op].permission, op);
             }
         }
 
@@ -3170,7 +2358,7 @@
      */
     @UnsupportedAppUsage
     public static int opToSwitch(int op) {
-        return sOpToSwitch[op];
+        return sAppOpInfos[op].switchCode;
     }
 
     /**
@@ -3180,7 +2368,7 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static String opToName(int op) {
         if (op == OP_NONE) return "NONE";
-        return op < sOpNames.length ? sOpNames[op] : ("Unknown(" + op + ")");
+        return op < sAppOpInfos.length ? sAppOpInfos[op].simpleName : ("Unknown(" + op + ")");
     }
 
     /**
@@ -3189,15 +2377,15 @@
      * @hide
      */
     public static @NonNull String opToPublicName(int op) {
-        return sOpToString[op];
+        return sAppOpInfos[op].name;
     }
 
     /**
      * @hide
      */
     public static int strDebugOpToOp(String op) {
-        for (int i=0; i<sOpNames.length; i++) {
-            if (sOpNames[i].equals(op)) {
+        for (int i = 0; i < sAppOpInfos.length; i++) {
+            if (sAppOpInfos[i].simpleName.equals(op)) {
                 return i;
             }
         }
@@ -3211,7 +2399,7 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     @TestApi
     public static String opToPermission(int op) {
-        return sOpPerms[op];
+        return sAppOpInfos[op].permission;
     }
 
     /**
@@ -3232,7 +2420,7 @@
      * @hide
      */
     public static String opToRestriction(int op) {
-        return sOpRestrictions[op];
+        return sAppOpInfos[op].restriction;
     }
 
     /**
@@ -3254,7 +2442,7 @@
      * @hide
      */
     public static RestrictionBypass opAllowSystemBypassRestriction(int op) {
-        return sOpAllowSystemRestrictionBypass[op];
+        return sAppOpInfos[op].allowSystemRestrictionBypass;
     }
 
     /**
@@ -3262,7 +2450,7 @@
      * @hide
      */
     public static @Mode int opToDefaultMode(int op) {
-        return sOpDefaultMode[op];
+        return sAppOpInfos[op].defaultMode;
     }
 
     /**
@@ -3295,7 +2483,7 @@
      * @hide
      */
     public static boolean opRestrictsRead(int op) {
-        return sOpRestrictRead[op];
+        return sAppOpInfos[op].restrictRead;
     }
 
     /**
@@ -3303,7 +2491,7 @@
      * @hide
      */
     public static boolean opAllowsReset(int op) {
-        return !sOpDisableReset[op];
+        return !sAppOpInfos[op].disableReset;
     }
 
     /**
@@ -4436,7 +3624,7 @@
          * @return This entry's op string name, such as {@link #OPSTR_COARSE_LOCATION}.
          */
         public @NonNull String getOpStr() {
-            return sOpToString[mOp];
+            return sAppOpInfos[mOp].name;
         }
 
         /**
@@ -6553,7 +5741,7 @@
             if (mHistoricalOps == null) {
                 mHistoricalOps = new ArrayMap<>();
             }
-            final String opStr = sOpToString[opCode];
+            final String opStr = sAppOpInfos[opCode].name;
             HistoricalOp op = mHistoricalOps.get(opStr);
             if (op == null) {
                 op = new HistoricalOp(opCode);
@@ -6898,7 +6086,7 @@
          * @return The op name.
          */
         public @NonNull String getOpName() {
-            return sOpToString[mOp];
+            return sAppOpInfos[mOp].name;
         }
 
         /** @hide */
@@ -7912,7 +7100,7 @@
         if (opCode == null) {
             return null;
         }
-        return sOpToString[opCode];
+        return sAppOpInfos[opCode].name;
     }
 
     /**
@@ -8007,8 +7195,8 @@
                         if (callback instanceof OnOpChangedInternalListener) {
                             ((OnOpChangedInternalListener)callback).onOpChanged(op, packageName);
                         }
-                        if (sOpToString[op] != null) {
-                            callback.onOpChanged(sOpToString[op], packageName);
+                        if (sAppOpInfos[op].name != null) {
+                            callback.onOpChanged(sAppOpInfos[op].name, packageName);
                         }
                     }
                 };
@@ -8096,8 +7284,8 @@
                             ((OnOpActiveChangedInternalListener) callback).onOpActiveChanged(op,
                                     uid, packageName, active);
                         }
-                        if (sOpToString[op] != null) {
-                            callback.onOpActiveChanged(sOpToString[op], uid, packageName,
+                        if (sAppOpInfos[op].name != null) {
+                            callback.onOpActiveChanged(sAppOpInfos[op].name, uid, packageName,
                                     attributionTag, active, attributionFlags, attributionChainId);
                         }
                     });
@@ -8273,7 +7461,8 @@
     }
 
     private String buildSecurityExceptionMsg(int op, int uid, String packageName) {
-        return packageName + " from uid " + uid + " not allowed to perform " + sOpNames[op];
+        return packageName + " from uid " + uid + " not allowed to perform " +
+            sAppOpInfos[op].simpleName;
     }
 
     /**
@@ -8633,7 +7822,7 @@
                     + attributionSource.getUid() + " or calling package "
                     + attributionSource.getNextPackageName() + " from uid "
                     + attributionSource.getNextUid() + " not allowed to perform "
-                    + sOpNames[op]);
+                    + sAppOpInfos[op].simpleName);
         }
         return mode;
     }
@@ -10084,10 +9273,13 @@
      */
     @SystemApi
     public static String[] getOpStrs() {
-        return Arrays.copyOf(sOpToString, sOpToString.length);
+        String[] opStrs = new String[sAppOpInfos.length];
+        for(int i = 0; i < sAppOpInfos.length; i++) {
+            opStrs[i] = sAppOpInfos[i].name;
+        }
+        return opStrs;
     }
 
-
     /**
      * @return number of App ops
      * @hide
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index f0e1448..aa5fa5b 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -528,6 +528,28 @@
         return mIsAlarmBroadcast;
     }
 
+    /**
+     * Did this broadcast originate from a push message from the server?
+     *
+     * @return true if this broadcast is a push message, false otherwise.
+     * @hide
+     */
+    public boolean isPushMessagingBroadcast() {
+        return mTemporaryAppAllowlistReasonCode == PowerExemptionManager.REASON_PUSH_MESSAGING;
+    }
+
+    /**
+     * Did this broadcast originate from a push message from the server which was over the allowed
+     * quota?
+     *
+     * @return true if this broadcast is a push message over quota, false otherwise.
+     * @hide
+     */
+    public boolean isPushMessagingOverQuotaBroadcast() {
+        return mTemporaryAppAllowlistReasonCode
+                == PowerExemptionManager.REASON_PUSH_MESSAGING_OVER_QUOTA;
+    }
+
     /** {@hide} */
     public long getRequireCompatChangeId() {
         return mRequireCompatChangeId;
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 8367441..bd999fc 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -760,4 +760,15 @@
      * </p>
      */
     int getBackgroundRestrictionExemptionReason(int uid);
+
+    // Start (?) of T transactions
+    /**
+     * Similar to {@link #startUserInBackgroundWithListener(int userId, IProgressListener unlockProgressListener),
+     * but setting the user as the visible user of that display (i.e., allowing the user and its
+     * running profiles to launch activities on that display).
+     *
+     * <p>Typically used only by automotive builds when the vehicle has multiple displays.
+     */
+    boolean startUserInBackgroundOnSecondaryDisplay(int userid, int displayId);
+
 }
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 0e89b25..b9ad595 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -124,8 +124,13 @@
     /**
      * The maximum length for text fields in a NotificationChannel. Fields will be truncated at this
      * limit.
+     * @hide
      */
-    private static final int MAX_TEXT_LENGTH = 1000;
+    public static final int MAX_TEXT_LENGTH = 1000;
+    /**
+     * @hide
+     */
+    public static final int MAX_VIBRATION_LENGTH = 1000;
 
     private static final String TAG_CHANNEL = "channel";
     private static final String ATT_NAME = "name";
@@ -283,17 +288,17 @@
      */
     protected NotificationChannel(Parcel in) {
         if (in.readByte() != 0) {
-            mId = in.readString();
+            mId = getTrimmedString(in.readString());
         } else {
             mId = null;
         }
         if (in.readByte() != 0) {
-            mName = in.readString();
+            mName = getTrimmedString(in.readString());
         } else {
             mName = null;
         }
         if (in.readByte() != 0) {
-            mDesc = in.readString();
+            mDesc = getTrimmedString(in.readString());
         } else {
             mDesc = null;
         }
@@ -302,18 +307,22 @@
         mLockscreenVisibility = in.readInt();
         if (in.readByte() != 0) {
             mSound = Uri.CREATOR.createFromParcel(in);
+            mSound = Uri.parse(getTrimmedString(mSound.toString()));
         } else {
             mSound = null;
         }
         mLights = in.readByte() != 0;
         mVibration = in.createLongArray();
+        if (mVibration != null && mVibration.length > MAX_VIBRATION_LENGTH) {
+            mVibration = Arrays.copyOf(mVibration, MAX_VIBRATION_LENGTH);
+        }
         mUserLockedFields = in.readInt();
         mFgServiceShown = in.readByte() != 0;
         mVibrationEnabled = in.readByte() != 0;
         mShowBadge = in.readByte() != 0;
         mDeleted = in.readByte() != 0;
         if (in.readByte() != 0) {
-            mGroup = in.readString();
+            mGroup = getTrimmedString(in.readString());
         } else {
             mGroup = null;
         }
@@ -322,8 +331,8 @@
         mBlockableSystem = in.readBoolean();
         mAllowBubbles = in.readInt();
         mOriginalImportance = in.readInt();
-        mParentId = in.readString();
-        mConversationId = in.readString();
+        mParentId = getTrimmedString(in.readString());
+        mConversationId = getTrimmedString(in.readString());
         mDemoted = in.readBoolean();
         mImportantConvo = in.readBoolean();
         mDeletedTime = in.readLong();
diff --git a/core/java/android/app/NotificationChannelGroup.java b/core/java/android/app/NotificationChannelGroup.java
index f97415c..807bd57 100644
--- a/core/java/android/app/NotificationChannelGroup.java
+++ b/core/java/android/app/NotificationChannelGroup.java
@@ -20,6 +20,7 @@
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Intent;
+import android.content.pm.ParceledListSlice;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
@@ -43,8 +44,9 @@
     /**
      * The maximum length for text fields in a NotificationChannelGroup. Fields will be truncated at
      * this limit.
+     * @hide
      */
-    private static final int MAX_TEXT_LENGTH = 1000;
+    public static final int MAX_TEXT_LENGTH = 1000;
 
     private static final String TAG_GROUP = "channelGroup";
     private static final String ATT_NAME = "name";
@@ -66,7 +68,7 @@
     private CharSequence mName;
     private String mDescription;
     private boolean mBlocked;
-    private List<NotificationChannel> mChannels = new ArrayList<>();
+    private ParceledListSlice<NotificationChannel> mChannels;
     // Bitwise representation of fields that have been changed by the user
     private int mUserLockedFields;
 
@@ -90,17 +92,19 @@
      */
     protected NotificationChannelGroup(Parcel in) {
         if (in.readByte() != 0) {
-            mId = in.readString();
+            mId = getTrimmedString(in.readString());
         } else {
             mId = null;
         }
         mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+        mName = getTrimmedString(mName.toString());
         if (in.readByte() != 0) {
-            mDescription = in.readString();
+            mDescription = getTrimmedString(in.readString());
         } else {
             mDescription = null;
         }
-        in.readParcelableList(mChannels, NotificationChannel.class.getClassLoader(), android.app.NotificationChannel.class);
+        mChannels = in.readParcelable(
+                NotificationChannelGroup.class.getClassLoader(), ParceledListSlice.class);
         mBlocked = in.readBoolean();
         mUserLockedFields = in.readInt();
     }
@@ -120,14 +124,14 @@
         } else {
             dest.writeByte((byte) 0);
         }
-        TextUtils.writeToParcel(mName, dest, flags);
+        TextUtils.writeToParcel(mName.toString(), dest, flags);
         if (mDescription != null) {
             dest.writeByte((byte) 1);
             dest.writeString(mDescription);
         } else {
             dest.writeByte((byte) 0);
         }
-        dest.writeParcelableList(mChannels, flags);
+        dest.writeParcelable(mChannels, flags);
         dest.writeBoolean(mBlocked);
         dest.writeInt(mUserLockedFields);
     }
@@ -157,7 +161,7 @@
      * Returns the list of channels that belong to this group
      */
     public List<NotificationChannel> getChannels() {
-        return mChannels;
+        return mChannels == null ? new ArrayList<>() : mChannels.getList();
     }
 
     /**
@@ -191,15 +195,8 @@
     /**
      * @hide
      */
-    public void addChannel(NotificationChannel channel) {
-        mChannels.add(channel);
-    }
-
-    /**
-     * @hide
-     */
     public void setChannels(List<NotificationChannel> channels) {
-        mChannels = channels;
+        mChannels = new ParceledListSlice<>(channels);
     }
 
     /**
@@ -334,7 +331,7 @@
         proto.write(NotificationChannelGroupProto.NAME, mName.toString());
         proto.write(NotificationChannelGroupProto.DESCRIPTION, mDescription);
         proto.write(NotificationChannelGroupProto.IS_BLOCKED, mBlocked);
-        for (NotificationChannel channel : mChannels) {
+        for (NotificationChannel channel : mChannels.getList()) {
             channel.dumpDebug(proto, NotificationChannelGroupProto.CHANNELS);
         }
         proto.end(token);
diff --git a/core/java/android/content/pm/OWNERS b/core/java/android/content/pm/OWNERS
index 087e61d..f9d3222 100644
--- a/core/java/android/content/pm/OWNERS
+++ b/core/java/android/content/pm/OWNERS
@@ -11,3 +11,4 @@
 per-file AppSearchPerson.java = file:/core/java/android/content/pm/SHORTCUT_OWNERS
 per-file *Launcher* = file:/core/java/android/content/pm/LAUNCHER_OWNERS
 per-file UserInfo* = file:/MULTIUSER_OWNERS
+per-file *UserProperties* = file:/MULTIUSER_OWNERS
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt b/core/java/android/content/pm/UserProperties.aidl
similarity index 74%
copy from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
copy to core/java/android/content/pm/UserProperties.aidl
index 8dde897..4d37067 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
+++ b/core/java/android/content/pm/UserProperties.aidl
@@ -11,13 +11,9 @@
  * 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.
+ * limitations under the License
  */
 
-package com.android.settingslib.spaprivileged.framework.app
+package android.content.pm;
 
-import android.content.pm.ApplicationInfo
-
-interface AppRecord {
-    val app: ApplicationInfo
-}
+parcelable UserProperties;
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
new file mode 100644
index 0000000..1a82e4d
--- /dev/null
+++ b/core/java/android/content/pm/UserProperties.java
@@ -0,0 +1,356 @@
+/*
+ * 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.content.pm;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Slog;
+import android.util.TypedXmlPullParser;
+import android.util.TypedXmlSerializer;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Class holding the properties of a user that derive mostly from its user type.
+ */
+public final class UserProperties implements Parcelable {
+    private static final String LOG_TAG = UserProperties.class.getSimpleName();
+
+    // Attribute strings for reading/writing properties to/from XML.
+    private static final String ATTR_SHOW_IN_LAUNCHER = "showInLauncher";
+    private static final String ATTR_START_WITH_PARENT = "startWithParent";
+
+    /** Index values of each property (to indicate whether they are present in this object). */
+    @IntDef(prefix = "INDEX_", value = {
+            INDEX_SHOW_IN_LAUNCHER,
+            INDEX_START_WITH_PARENT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface PropertyIndex {
+    }
+    private static final int INDEX_SHOW_IN_LAUNCHER = 0;
+    private static final int INDEX_START_WITH_PARENT = 1;
+    /** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
+    private long mPropertiesPresent = 0;
+
+
+    /**
+     * Possible values for whether or how to show this user in the Launcher.
+     * @hide
+     */
+    @IntDef(prefix = "SHOW_IN_LAUNCHER_", value = {
+            SHOW_IN_LAUNCHER_WITH_PARENT,
+            SHOW_IN_LAUNCHER_SEPARATE,
+            SHOW_IN_LAUNCHER_NO,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ShowInLauncher {
+    }
+    /**
+     * Suggests that the launcher should show this user's apps in the main tab.
+     * That is, either this user is a full user, so its apps should be presented accordingly, or, if
+     * this user is a profile, then its apps should be shown alongside its parent's apps.
+     */
+    public static final int SHOW_IN_LAUNCHER_WITH_PARENT = 0;
+    /**
+     * Suggests that the launcher should show this user's apps, but separately from the apps of this
+     * user's parent.
+     */
+    public static final int SHOW_IN_LAUNCHER_SEPARATE = 1;
+    /**
+     * Suggests that the launcher should not show this user.
+     */
+    public static final int SHOW_IN_LAUNCHER_NO = 2;
+
+    /**
+     * Reference to the default user properties for this user's user type.
+     * <li>If non-null, then any absent property will use the default property from here instead.
+     * <li>If null, then any absent property indicates that the caller lacks permission to see it,
+     *          so attempting to get that property will trigger a SecurityException.
+     */
+    private final @Nullable UserProperties mDefaultProperties;
+
+    /**
+     * Creates a UserProperties (intended for the SystemServer) that stores a reference to the given
+     * default properties, which it uses for any property not subsequently set.
+     * @hide
+     */
+    public UserProperties(@NonNull UserProperties defaultProperties) {
+        mDefaultProperties = defaultProperties;
+        mPropertiesPresent = 0;
+    }
+
+    /**
+     * Copies the given UserProperties, excluding any information that doesn't satisfy the specified
+     * permissions.
+     * Can only be used on the original version (one that won't throw on permission errors).
+     * Note that, internally, this does not perform an exact copy.
+     * @hide
+     */
+    public UserProperties(UserProperties orig,
+            boolean exposeAllFields,
+            boolean hasManagePermission,
+            boolean hasQueryPermission) {
+
+        if (orig.mDefaultProperties == null) {
+            throw new IllegalArgumentException("Attempting to copy a non-original UserProperties.");
+        }
+
+        this.mDefaultProperties = null;
+
+        // NOTE: Copy each property using getters to ensure default values are copied if needed.
+        if (exposeAllFields) {
+            setStartWithParent(orig.getStartWithParent());
+        }
+        if (hasManagePermission) {
+            // Add any items that require this permission.
+        }
+        if (hasQueryPermission) {
+            // Add any items that require this permission.
+        }
+        // Add any items that require no permissions at all.
+        setShowInLauncher(orig.getShowInLauncher());
+    }
+
+    /**
+     * Indicates that the given property is being stored explicitly in this object.
+     * If false, it means that either
+     * <li>the default property for the user type should be used instead (for SystemServer callers)
+     * <li>the caller lacks permission to see this property (for all other callers)
+     */
+    private boolean isPresent(@PropertyIndex long index) {
+        return (mPropertiesPresent & (1L << index)) != 0;
+    }
+
+    /** Indicates that the given property is henceforth being explicitly stored in this object. */
+    private void setPresent(@PropertyIndex long index) {
+        mPropertiesPresent |= (1L << index);
+    }
+
+    /** @hide Returns the internal mPropertiesPresent value. Only for testing purposes. */
+    @VisibleForTesting
+    public long getPropertiesPresent() {
+        return mPropertiesPresent;
+    }
+
+    /**
+     * Returns whether, and how, a user should be shown in the Launcher.
+     * This is generally inapplicable for non-profile users.
+     *
+     * Possible return values include
+     *    {@link #SHOW_IN_LAUNCHER_WITH_PARENT}},
+     *    {@link #SHOW_IN_LAUNCHER_SEPARATE},
+     *    and {@link #SHOW_IN_LAUNCHER_NO}.
+     *
+     * @return whether, and how, a profile should be shown in the Launcher.
+     */
+    public @ShowInLauncher int getShowInLauncher() {
+        if (isPresent(INDEX_SHOW_IN_LAUNCHER)) return mShowInLauncher;
+        if (mDefaultProperties != null) return mDefaultProperties.mShowInLauncher;
+        throw new SecurityException("You don't have permission to query showInLauncher");
+    }
+    /** @hide */
+    public void setShowInLauncher(@ShowInLauncher int val) {
+        this.mShowInLauncher = val;
+        setPresent(INDEX_SHOW_IN_LAUNCHER);
+    }
+    private @ShowInLauncher int mShowInLauncher;
+
+    /**
+     * Returns whether a profile should be started when its parent starts (unless in quiet mode).
+     * This only applies for users that have parents (i.e. for profiles).
+     * @hide
+     */
+    public boolean getStartWithParent() {
+        if (isPresent(INDEX_START_WITH_PARENT)) return mStartWithParent;
+        if (mDefaultProperties != null) return mDefaultProperties.mStartWithParent;
+        throw new SecurityException("You don't have permission to query startWithParent");
+    }
+    /** @hide */
+    public void setStartWithParent(boolean val) {
+        this.mStartWithParent = val;
+        setPresent(INDEX_START_WITH_PARENT);
+    }
+    private boolean mStartWithParent;
+
+    @Override
+    public String toString() {
+        // Please print in increasing order of PropertyIndex.
+        return "UserProperties{"
+                + "mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent)
+                + ", mShowInLauncher=" + getShowInLauncher()
+                + ", mStartWithParent=" + getStartWithParent()
+                + "}";
+    }
+
+    /**
+     * Print the UserProperties to the given PrintWriter.
+     * @hide
+     */
+    public void println(PrintWriter pw, String prefix) {
+        // Please print in increasing order of PropertyIndex.
+        pw.println(prefix + "UserProperties:");
+        pw.println(prefix + "    mPropertiesPresent=" + Long.toBinaryString(mPropertiesPresent));
+        pw.println(prefix + "    mShowInLauncher=" + getShowInLauncher());
+        pw.println(prefix + "    mStartWithParent=" + getStartWithParent());
+    }
+
+    /**
+     * Reads in a UserProperties from an xml file, for use by the SystemServer.
+     *
+     * The serializer should already be inside a tag from which to read the user properties.
+     *
+     * @param defaultUserPropertiesReference the default UserProperties to use for this user type.
+     * @see #writeToXml
+     * @hide
+     */
+    public UserProperties(
+            TypedXmlPullParser parser,
+            @NonNull UserProperties defaultUserPropertiesReference)
+            throws IOException, XmlPullParserException {
+
+        this(defaultUserPropertiesReference);
+        updateFromXml(parser);
+    }
+
+    /**
+     * Parses the given xml file and updates this UserProperties with its data.
+     * I.e., if a piece of data is present in the xml, it will overwrite whatever was
+     * previously stored in this UserProperties.
+     * @hide
+     */
+    public void updateFromXml(TypedXmlPullParser parser)
+            throws IOException, XmlPullParserException {
+
+        final int attributeCount = parser.getAttributeCount();
+        for (int i = 0; i < attributeCount; i++) {
+            final String attributeName = parser.getAttributeName(i);
+            switch(attributeName) {
+                case ATTR_SHOW_IN_LAUNCHER:
+                    setShowInLauncher(parser.getAttributeInt(i));
+                    break;
+                case ATTR_START_WITH_PARENT:
+                    setStartWithParent(parser.getAttributeBoolean(i));
+                    break;
+                default:
+                    Slog.w(LOG_TAG, "Skipping unknown property " + attributeName);
+            }
+        }
+    }
+
+    /**
+     * Writes the UserProperties, as used by the SystemServer, to the xml file.
+     *
+     * The serializer should already be inside a tag in which to write the user properties.
+     *
+     * @see  #UserProperties(TypedXmlPullParser, UserProperties)
+     * @hide
+     */
+    public void writeToXml(TypedXmlSerializer serializer)
+            throws IOException, XmlPullParserException {
+
+        if (isPresent(INDEX_SHOW_IN_LAUNCHER)) {
+            serializer.attributeInt(null, ATTR_SHOW_IN_LAUNCHER, mShowInLauncher);
+        }
+        if (isPresent(INDEX_START_WITH_PARENT)) {
+            serializer.attributeBoolean(null, ATTR_START_WITH_PARENT, mStartWithParent);
+        }
+    }
+
+    // For use only with an object that has already had any permission-lacking fields stripped out.
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int parcelableFlags) {
+        dest.writeLong(mPropertiesPresent);
+        dest.writeInt(mShowInLauncher);
+        dest.writeBoolean(mStartWithParent);
+    }
+
+    /**
+     * Reads a UserProperties object from the parcel.
+     * Not suitable for the canonical SystemServer version since it lacks mDefaultProperties.
+      */
+    private UserProperties(@NonNull Parcel source) {
+        mDefaultProperties = null;
+
+        mPropertiesPresent = source.readLong();
+        mShowInLauncher = source.readInt();
+        mStartWithParent = source.readBoolean();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<UserProperties> CREATOR
+            = new Parcelable.Creator<UserProperties>() {
+        public UserProperties createFromParcel(Parcel source) {
+            return new UserProperties(source);
+        }
+        public UserProperties[] newArray(int size) {
+            return new UserProperties[size];
+        }
+    };
+
+    /**
+     * Builder for the SystemServer's {@link UserProperties}; see that class for documentation.
+     * Intended for building default values (and so all properties are present in the built object).
+     * @hide
+     */
+    public static final class Builder {
+        // UserProperties fields and their default values.
+        private @ShowInLauncher int mShowInLauncher = SHOW_IN_LAUNCHER_WITH_PARENT;
+        private boolean mStartWithParent = false;
+
+        public Builder setShowInLauncher(@ShowInLauncher int showInLauncher) {
+            mShowInLauncher = showInLauncher;
+            return this;
+        }
+
+        public Builder setStartWithParent(boolean startWithParent) {
+            mStartWithParent = startWithParent;
+            return this;
+        }
+
+        /** Builds a UserProperties object with *all* values populated. */
+        public UserProperties build() {
+            return new UserProperties(
+                    mShowInLauncher,
+                    mStartWithParent);
+        }
+    } // end Builder
+
+    /** Creates a UserProperties with the given properties. Intended for building default values. */
+    private UserProperties(
+            @ShowInLauncher int showInLauncher,
+            boolean startWithParent) {
+
+        mDefaultProperties = null;
+        setShowInLauncher(showInLauncher);
+        setStartWithParent(startWithParent);
+    }
+}
diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java
index 1e4c9501..11892fe 100644
--- a/core/java/android/hardware/HardwareBuffer.java
+++ b/core/java/android/hardware/HardwareBuffer.java
@@ -188,9 +188,6 @@
     public static HardwareBuffer create(
             @IntRange(from = 1) int width, @IntRange(from = 1) int height,
             @Format int format, @IntRange(from = 1) int layers, @Usage long usage) {
-        if (!HardwareBuffer.isSupportedFormat(format)) {
-            throw new IllegalArgumentException("Invalid pixel format " + format);
-        }
         if (width <= 0) {
             throw new IllegalArgumentException("Invalid width " + width);
         }
@@ -226,9 +223,6 @@
      */
     public static boolean isSupported(@IntRange(from = 1) int width, @IntRange(from = 1) int height,
             @Format int format, @IntRange(from = 1) int layers, @Usage long usage) {
-        if (!HardwareBuffer.isSupportedFormat(format)) {
-            throw new IllegalArgumentException("Invalid pixel format " + format);
-        }
         if (width <= 0) {
             throw new IllegalArgumentException("Invalid width " + width);
         }
@@ -286,10 +280,7 @@
      * Returns the width of this buffer in pixels.
      */
     public int getWidth() {
-        if (isClosed()) {
-            throw new IllegalStateException("This HardwareBuffer has been closed and its width "
-                    + "cannot be obtained.");
-        }
+        checkClosed("width");
         return nGetWidth(mNativeObject);
     }
 
@@ -297,10 +288,7 @@
      * Returns the height of this buffer in pixels.
      */
     public int getHeight() {
-        if (isClosed()) {
-            throw new IllegalStateException("This HardwareBuffer has been closed and its height "
-                    + "cannot be obtained.");
-        }
+        checkClosed("height");
         return nGetHeight(mNativeObject);
     }
 
@@ -309,10 +297,7 @@
      */
     @Format
     public int getFormat() {
-        if (isClosed()) {
-            throw new IllegalStateException("This HardwareBuffer has been closed and its format "
-                    + "cannot be obtained.");
-        }
+        checkClosed("format");
         return nGetFormat(mNativeObject);
     }
 
@@ -320,10 +305,7 @@
      * Returns the number of layers in this buffer.
      */
     public int getLayers() {
-        if (isClosed()) {
-            throw new IllegalStateException("This HardwareBuffer has been closed and its layer "
-                    + "count cannot be obtained.");
-        }
+        checkClosed("layer count");
         return nGetLayers(mNativeObject);
     }
 
@@ -331,14 +313,27 @@
      * Returns the usage flags of the usage hints set on this buffer.
      */
     public long getUsage() {
-        if (isClosed()) {
-            throw new IllegalStateException("This HardwareBuffer has been closed and its usage "
-                    + "cannot be obtained.");
-        }
+        checkClosed("usage");
         return nGetUsage(mNativeObject);
     }
 
     /**
+     * Returns the system-wide unique id for this buffer
+     *
+     */
+    public long getId() {
+        checkClosed("id");
+        return nGetId(mNativeObject);
+    }
+
+    private void checkClosed(String name) {
+        if (isClosed()) {
+            throw new IllegalStateException("This HardwareBuffer has been closed and its "
+                    + name + " cannot be obtained.");
+        }
+    }
+
+    /**
      * Destroys this buffer immediately. Calling this method frees up any
      * underlying native resources. After calling this method, this buffer
      * must not be used in any way.
@@ -407,36 +402,6 @@
         }
     };
 
-    /**
-     * Validates whether a particular format is supported by HardwareBuffer.
-     *
-     * @param format The format to validate.
-     *
-     * @return True if <code>format</code> is a supported format. false otherwise.
-     * See {@link #create(int, int, int, int, long)}.
-     */
-    private static boolean isSupportedFormat(@Format int format) {
-        switch(format) {
-            case RGBA_8888:
-            case RGBA_FP16:
-            case RGBA_1010102:
-            case RGBX_8888:
-            case RGB_565:
-            case RGB_888:
-            case BLOB:
-            case YCBCR_420_888:
-            case D_16:
-            case D_24:
-            case DS_24UI8:
-            case D_FP32:
-            case DS_FP32UI8:
-            case S_UI8:
-            case YCBCR_P010:
-                return true;
-        }
-        return false;
-    }
-
     private static native long nCreateHardwareBuffer(int width, int height, int format, int layers,
             long usage);
     private static native long nCreateFromGraphicBuffer(GraphicBuffer graphicBuffer);
@@ -457,4 +422,6 @@
             long usage);
     @CriticalNative
     private static native long nEstimateSize(long nativeObject);
+    @CriticalNative
+    private static native long nGetId(long nativeObject);
 }
diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java
index 7092e43..7247ef7 100644
--- a/core/java/android/hardware/face/FaceManager.java
+++ b/core/java/android/hardware/face/FaceManager.java
@@ -29,6 +29,7 @@
 import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricFaceConstants;
+import android.hardware.biometrics.BiometricStateListener;
 import android.hardware.biometrics.CryptoObject;
 import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
 import android.os.Binder;
@@ -674,6 +675,45 @@
     }
 
     /**
+     * Forwards BiometricStateListener to FaceService.
+     *
+     * @param listener new BiometricStateListener being added
+     * @hide
+     */
+    public void registerBiometricStateListener(@NonNull BiometricStateListener listener) {
+        try {
+            mService.registerBiometricStateListener(listener);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Adds a callback that gets called when the service registers all of the face
+     * authenticators (HALs).
+     *
+     * If the face authenticators are already registered when the callback is added, the
+     * callback is invoked immediately.
+     *
+     * The callback is automatically removed after it's invoked.
+     *
+     * @hide
+     */
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    public void addAuthenticatorsRegisteredCallback(
+            IFaceAuthenticatorsRegisteredCallback callback) {
+        if (mService != null) {
+            try {
+                mService.addAuthenticatorsRegisteredCallback(callback);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        } else {
+            Slog.w(TAG, "addAuthenticatorsRegisteredCallback(): Service not connected!");
+        }
+    }
+
+    /**
      * @hide
      */
     @RequiresPermission(USE_BIOMETRIC_INTERNAL)
diff --git a/core/java/android/hardware/face/IFaceAuthenticatorsRegisteredCallback.aidl b/core/java/android/hardware/face/IFaceAuthenticatorsRegisteredCallback.aidl
new file mode 100644
index 0000000..78f978d2
--- /dev/null
+++ b/core/java/android/hardware/face/IFaceAuthenticatorsRegisteredCallback.aidl
@@ -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 android.hardware.face;
+
+import android.hardware.face.FaceSensorPropertiesInternal;
+import java.util.List;
+
+/**
+ * Callback to notify FaceManager that FaceService has registered all of the
+ * face authenticators (HALs).
+ * See {@link android.hardware.face.IFaceService#registerAuthenticators}.
+ *
+ * @hide
+ */
+oneway interface IFaceAuthenticatorsRegisteredCallback {
+    /**
+     * Notifies FaceManager that all of the face authenticators have been registered.
+     *
+     * @param sensors A consolidated list of sensor properties for all of the authenticators.
+     */
+    void onAllAuthenticatorsRegistered(in List<FaceSensorPropertiesInternal> sensors);
+}
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index 369248e..9b56f43 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -17,9 +17,11 @@
 
 import android.hardware.biometrics.IBiometricSensorReceiver;
 import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
+import android.hardware.biometrics.IBiometricStateListener;
 import android.hardware.biometrics.IInvalidationCallback;
 import android.hardware.biometrics.ITestSession;
 import android.hardware.biometrics.ITestSessionCallback;
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
 import android.hardware.face.IFaceServiceReceiver;
 import android.hardware.face.Face;
 import android.hardware.face.FaceSensorPropertiesInternal;
@@ -163,4 +165,11 @@
     // hidlSensors must be non-null and empty. See AuthService.java
     @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     void registerAuthenticators(in List<FaceSensorPropertiesInternal> hidlSensors);
+
+    // Adds a callback which gets called when the service registers all of the face
+    // authenticators. The callback is automatically removed after it's invoked.
+    void addAuthenticatorsRegisteredCallback(IFaceAuthenticatorsRegisteredCallback callback);
+
+    // Registers BiometricStateListener.
+    void registerBiometricStateListener(IBiometricStateListener listener);
 }
diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
index cc7ed18..1ba9a04 100644
--- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl
+++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl
@@ -202,8 +202,10 @@
     void setSidefpsController(in ISidefpsController controller);
 
     // Registers BiometricStateListener.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     void registerBiometricStateListener(IBiometricStateListener listener);
 
     // Sends a power button pressed event to all listeners.
+    @EnforcePermission("USE_BIOMETRIC_INTERNAL")
     oneway void onPowerPressed();
 }
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index a9d665c8..621eab5 100644
--- a/core/java/android/hardware/soundtrigger/SoundTrigger.java
+++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java
@@ -1963,7 +1963,6 @@
     }
 
     private static Object mServiceLock = new Object();
-    private static ISoundTriggerMiddlewareService mService;
 
     /**
      * Translate an exception thrown from interaction with the underlying service to an error code.
@@ -2217,20 +2216,12 @@
                     binder =
                             ServiceManager.getServiceOrThrow(
                                     Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE);
-                    binder.linkToDeath(() -> {
-                        synchronized (mServiceLock) {
-                            mService = null;
-                        }
-                    }, 0);
-                    mService = ISoundTriggerMiddlewareService.Stub.asInterface(binder);
-                    break;
+                    return ISoundTriggerMiddlewareService.Stub.asInterface(binder);
                 } catch (Exception e) {
                     Log.e(TAG, "Failed to bind to soundtrigger service", e);
                 }
             }
-            return  mService;
         }
-
     }
 
     /**
diff --git a/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java b/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java
index be57372..d6f191e 100644
--- a/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java
+++ b/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java
@@ -37,6 +37,7 @@
 import android.net.ipsec.ike.IkeSessionParams.IkeConfigRequest;
 import android.os.PersistableBundle;
 import android.util.ArraySet;
+import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.vcn.util.PersistableBundleUtils;
@@ -58,6 +59,8 @@
  */
 @VisibleForTesting(visibility = Visibility.PRIVATE)
 public final class IkeSessionParamsUtils {
+    private static final String TAG = IkeSessionParamsUtils.class.getSimpleName();
+
     private static final String SERVER_HOST_NAME_KEY = "SERVER_HOST_NAME_KEY";
     private static final String SA_PROPOSALS_KEY = "SA_PROPOSALS_KEY";
     private static final String LOCAL_ID_KEY = "LOCAL_ID_KEY";
@@ -72,6 +75,13 @@
     private static final String NATT_KEEPALIVE_DELAY_SEC_KEY = "NATT_KEEPALIVE_DELAY_SEC_KEY";
     private static final String IKE_OPTIONS_KEY = "IKE_OPTIONS_KEY";
 
+    // TODO: b/243181760 Use the IKE API when they are exposed
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static final int IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION = 6;
+
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static final int IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES = 7;
+
     private static final Set<Integer> IKE_OPTIONS = new ArraySet<>();
 
     static {
@@ -80,6 +90,26 @@
         IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_MOBIKE);
         IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_FORCE_PORT_4500);
         IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_INITIAL_CONTACT);
+        IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_REKEY_MOBILITY);
+        IKE_OPTIONS.add(IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION);
+        IKE_OPTIONS.add(IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES);
+    }
+
+    /**
+     * Check if an IKE option is supported in the IPsec module installed on the device
+     *
+     * <p>This method ensures caller to safely access options that are added between dessert
+     * releases.
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public static boolean isIkeOptionValid(int option) {
+        try {
+            new IkeSessionParams.Builder().addIkeOption(option);
+            return true;
+        } catch (IllegalArgumentException e) {
+            Log.d(TAG, "Option not supported; discarding: " + option);
+            return false;
+        }
     }
 
     /** Serializes an IkeSessionParams to a PersistableBundle. */
@@ -130,7 +160,7 @@
         // IKE_OPTION is defined in IKE module and added in the IkeSessionParams
         final List<Integer> enabledIkeOptions = new ArrayList<>();
         for (int option : IKE_OPTIONS) {
-            if (params.hasIkeOption(option)) {
+            if (isIkeOptionValid(option) && params.hasIkeOption(option)) {
                 enabledIkeOptions.add(option);
             }
         }
@@ -205,12 +235,16 @@
 
         // Clear IKE Options that are by default enabled
         for (int option : IKE_OPTIONS) {
-            builder.removeIkeOption(option);
+            if (isIkeOptionValid(option)) {
+                builder.removeIkeOption(option);
+            }
         }
 
         final int[] optionArray = in.getIntArray(IKE_OPTIONS_KEY);
         for (int option : optionArray) {
-            builder.addIkeOption(option);
+            if (isIkeOptionValid(option)) {
+                builder.addIkeOption(option);
+            }
         }
 
         return builder.build();
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index 09a52e4..9d05cec0 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -955,16 +955,7 @@
 
         public static final int NUM_WIFI_BATCHED_SCAN_BINS = 5;
 
-        /**
-         * Note that these must match the constants in android.os.PowerManager.
-         * Also, if the user activity types change, the BatteryStatsImpl.VERSION must
-         * also be bumped.
-         */
-        static final String[] USER_ACTIVITY_TYPES = {
-            "other", "button", "touch", "accessibility", "attention"
-        };
-
-        public static final int NUM_USER_ACTIVITY_TYPES = USER_ACTIVITY_TYPES.length;
+        public static final int NUM_USER_ACTIVITY_TYPES = PowerManager.USER_ACTIVITY_EVENT_MAX + 1;
 
         public abstract void noteUserActivityLocked(int type);
         public abstract boolean hasUserActivity();
@@ -6168,7 +6159,7 @@
                         }
                         sb.append(val);
                         sb.append(" ");
-                        sb.append(Uid.USER_ACTIVITY_TYPES[i]);
+                        sb.append(PowerManager.userActivityEventToString(i));
                     }
                 }
                 if (hasData) {
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index f69d6b0..fa6b118 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -23,6 +23,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.content.IntentSender;
 import android.content.RestrictionEntry;
 import android.graphics.Bitmap;
@@ -71,9 +72,11 @@
     boolean isUserOfType(int userId, in String userType);
     @UnsupportedAppUsage
     UserInfo getUserInfo(int userId);
+    UserProperties getUserPropertiesCopy(int userId);
     String getUserAccount(int userId);
     void setUserAccount(int userId, String accountName);
     long getUserCreationTime(int userId);
+    boolean isUserSwitcherEnabled(int mUserId);
     boolean isRestricted(int userId);
     boolean canHaveRestrictedProfile(int userId);
     int getUserSerialNumber(int userId);
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 01b75d1..a3a3e3f 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -348,6 +348,44 @@
     public static final int USER_ACTIVITY_EVENT_DEVICE_STATE = 6;
 
     /**
+     * @hide
+     */
+    public static final int USER_ACTIVITY_EVENT_MAX =  USER_ACTIVITY_EVENT_DEVICE_STATE;
+
+    /**
+     * @hide
+     */
+    @IntDef(prefix = { "USER_ACTIVITY_EVENT_" }, value = {
+            USER_ACTIVITY_EVENT_OTHER,
+            USER_ACTIVITY_EVENT_BUTTON,
+            USER_ACTIVITY_EVENT_TOUCH,
+            USER_ACTIVITY_EVENT_ACCESSIBILITY,
+            USER_ACTIVITY_EVENT_ATTENTION,
+            USER_ACTIVITY_EVENT_FACE_DOWN,
+            USER_ACTIVITY_EVENT_DEVICE_STATE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface UserActivityEvent{}
+
+    /**
+     *
+     * Convert the user activity event to a string for debugging purposes.
+     * @hide
+     */
+    public static String userActivityEventToString(@UserActivityEvent int userActivityEvent) {
+        switch (userActivityEvent) {
+            case USER_ACTIVITY_EVENT_OTHER: return "other";
+            case USER_ACTIVITY_EVENT_BUTTON: return "button";
+            case USER_ACTIVITY_EVENT_TOUCH: return "touch";
+            case USER_ACTIVITY_EVENT_ACCESSIBILITY: return "accessibility";
+            case USER_ACTIVITY_EVENT_ATTENTION: return "attention";
+            case USER_ACTIVITY_EVENT_FACE_DOWN: return "faceDown";
+            case USER_ACTIVITY_EVENT_DEVICE_STATE: return "deviceState";
+            default: return Integer.toString(userActivityEvent);
+        }
+    }
+
+    /**
      * User activity flag: If already dimmed, extend the dim timeout
      * but do not brighten.  This flag is useful for keeping the screen on
      * a little longer without causing a visible change such as when
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 16352b8..ca0af2c 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -51,6 +51,7 @@
 import android.content.IntentSender;
 import android.content.pm.UserInfo;
 import android.content.pm.UserInfo.UserInfoFlag;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -2786,7 +2787,7 @@
         return isUserRunning(user.getIdentifier());
     }
 
-    /** {@hide} */
+    /** @hide */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
     public boolean isUserRunning(@UserIdInt int userId) {
@@ -2841,6 +2842,7 @@
     /**
      * @hide
      */
+    @TestApi
     public static boolean isUsersOnSecondaryDisplaysEnabled() {
         return SystemProperties.getBoolean("fw.users_on_secondary_displays",
                 Resources.getSystem()
@@ -2968,7 +2970,7 @@
                 }
             };
 
-    /** {@hide} */
+    /** @hide */
     @UnsupportedAppUsage
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
@@ -2976,13 +2978,13 @@
         return mIsUserUnlockedCache.query(userId);
     }
 
-    /** {@hide} */
+    /** @hide */
     public void disableIsUserUnlockedCache() {
         mIsUserUnlockedCache.disableLocal();
         mIsUserUnlockingOrUnlockedCache.disableLocal();
     }
 
-    /** {@hide} */
+    /** @hide */
     public static final void invalidateIsUserUnlockedCache() {
         PropertyInvalidatedCache.invalidateCache(CACHE_KEY_IS_USER_UNLOCKED_PROPERTY);
     }
@@ -3012,7 +3014,7 @@
         return isUserUnlockingOrUnlocked(user.getIdentifier());
     }
 
-    /** {@hide} */
+    /** @hide */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
     public boolean isUserUnlockingOrUnlocked(@UserIdInt int userId) {
@@ -3083,6 +3085,30 @@
     }
 
     /**
+     * Returns a {@link UserProperties} object describing the properties of the given user.
+     *
+     * Note that the caller may not have permission to access all items; requesting any item for
+     * which permission is lacking will throw a {@link SecurityException}.
+     *
+     * <p> Requires
+     * {@code android.Manifest.permission#MANAGE_USERS},
+     * {@code android.Manifest.permission#QUERY_USERS}, or
+     * {@code android.Manifest.permission#INTERACT_ACROSS_USERS}
+     * permission, or else the caller must be in the same profile group as the caller.
+     *
+     * @param userHandle the user handle of the user whose information is being requested.
+     * @return a UserProperties object for a specific user.
+     * @throws IllegalArgumentException if {@code userHandle} doesn't correspond to an existing user
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.QUERY_USERS,
+            android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+    public @NonNull UserProperties getUserProperties(@NonNull UserHandle userHandle) {
+        return mUserPropertiesCache.query(userHandle.getIdentifier());
+    }
+
+    /**
      * @hide
      *
      * Returns who set a user restriction on a user.
@@ -5220,23 +5246,13 @@
     })
     @UserHandleAware
     public boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable) {
-        if (!supportsMultipleUsers()) {
-            return false;
-        }
-        if (hasUserRestrictionForUser(DISALLOW_USER_SWITCH, mUserId)) {
-            return false;
-        }
-        // If Demo Mode is on, don't show user switcher
-        if (isDeviceInDemoMode(mContext)) {
-            return false;
-        }
-        // Check the Settings.Global.USER_SWITCHER_ENABLED that the user can toggle on/off.
-        final boolean userSwitcherSettingOn = Settings.Global.getInt(mContext.getContentResolver(),
-                Settings.Global.USER_SWITCHER_ENABLED,
-                Resources.getSystem().getBoolean(R.bool.config_showUserSwitcherByDefault) ? 1 : 0)
-                != 0;
-        if (!userSwitcherSettingOn) {
-            return false;
+
+        try {
+            if (!mService.isUserSwitcherEnabled(mUserId)) {
+                return false;
+            }
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
         }
 
         // The feature is enabled. But is it worth showing?
@@ -5480,11 +5496,37 @@
                 }
             };
 
-    /** {@hide} */
+    /** @hide */
     public static final void invalidateStaticUserProperties() {
         PropertyInvalidatedCache.invalidateCache(CACHE_KEY_STATIC_USER_PROPERTIES);
     }
 
+    /* Cache key for UserProperties object. */
+    private static final String CACHE_KEY_USER_PROPERTIES = "cache_key.user_properties";
+
+    // TODO: It would be better to somehow have this as static, so that it can work cross-context.
+    private final PropertyInvalidatedCache<Integer, UserProperties> mUserPropertiesCache =
+            new PropertyInvalidatedCache<Integer, UserProperties>(16, CACHE_KEY_USER_PROPERTIES) {
+                @Override
+                public UserProperties recompute(Integer userId) {
+                    try {
+                        // If the userId doesn't exist, this will throw rather than cache garbage.
+                        return mService.getUserPropertiesCopy(userId);
+                    } catch (RemoteException re) {
+                        throw re.rethrowFromSystemServer();
+                    }
+                }
+                @Override
+                public boolean bypass(Integer query) {
+                    return query < 0;
+                }
+            };
+
+    /** @hide */
+    public static final void invalidateUserPropertiesCache() {
+        PropertyInvalidatedCache.invalidateCache(CACHE_KEY_USER_PROPERTIES);
+    }
+
     /**
      * @hide
      * User that enforces a restriction.
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index baf3eeca..8a50e79 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10661,6 +10661,13 @@
         public static final String MEDIA_CONTROLS_RESUME = "qs_media_resumption";
 
         /**
+         * Whether to enable media controls on lock screen.
+         * When enabled, media controls will appear on lock screen.
+         * @hide
+         */
+        public static final String MEDIA_CONTROLS_LOCK_SCREEN = "media_controls_lock_screen";
+
+        /**
          * Controls whether contextual suggestions can be shown in the media controls.
          * @hide
          */
@@ -10841,6 +10848,23 @@
                 "accessibility_software_cursor_enabled";
 
         /**
+         * Software Cursor settings that specifies whether trigger hints are enabled.
+         *
+         * @hide
+         */
+        public static final String ACCESSIBILITY_SOFTWARE_CURSOR_TRIGGER_HINTS_ENABLED =
+                "accessibility_software_cursor_trigger_hints_enabled";
+
+        /**
+         * Software Cursor settings that specifies whether triggers are shifted when the keyboard
+         * is shown.
+         *
+         * @hide
+         */
+        public static final String ACCESSIBILITY_SOFTWARE_CURSOR_KEYBOARD_SHIFT_ENABLED =
+                "accessibility_software_cursor_keyboard_shift_enabled";
+
+        /**
          * Whether the Adaptive connectivity option is enabled.
          *
          * @hide
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index a2cb1d5..4a72a62 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -314,6 +314,9 @@
             (int) (startValue.right + fraction * (endValue.right - startValue.right)),
             (int) (startValue.bottom + fraction * (endValue.bottom - startValue.bottom)));
 
+    /** Logging listener. */
+    private WindowInsetsAnimationControlListener mLoggingListener;
+
     /**
      * The default implementation of listener, to be used by InsetsController and InsetsPolicy to
      * animate insets.
@@ -330,6 +333,7 @@
         private final long mDurationMs;
         private final boolean mDisable;
         private final int mFloatingImeBottomInset;
+        private final WindowInsetsAnimationControlListener mLoggingListener;
 
         private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
                 new ThreadLocal<AnimationHandler>() {
@@ -343,7 +347,7 @@
 
         public InternalAnimationControlListener(boolean show, boolean hasAnimationCallbacks,
                 @InsetsType int requestedTypes, @Behavior int behavior, boolean disable,
-                int floatingImeBottomInset) {
+                int floatingImeBottomInset, WindowInsetsAnimationControlListener loggingListener) {
             mShow = show;
             mHasAnimationCallbacks = hasAnimationCallbacks;
             mRequestedTypes = requestedTypes;
@@ -351,12 +355,16 @@
             mDurationMs = calculateDurationMs();
             mDisable = disable;
             mFloatingImeBottomInset = floatingImeBottomInset;
+            mLoggingListener = loggingListener;
         }
 
         @Override
         public void onReady(WindowInsetsAnimationController controller, int types) {
             mController = controller;
             if (DEBUG) Log.d(TAG, "default animation onReady types: " + types);
+            if (mLoggingListener != null) {
+                mLoggingListener.onReady(controller, types);
+            }
 
             if (mDisable) {
                 onAnimationFinish();
@@ -410,6 +418,9 @@
         public void onFinished(WindowInsetsAnimationController controller) {
             if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onFinished types:"
                     + Type.toString(mRequestedTypes));
+            if (mLoggingListener != null) {
+                mLoggingListener.onFinished(controller);
+            }
         }
 
         @Override
@@ -420,6 +431,9 @@
             }
             if (DEBUG) Log.d(TAG, "InternalAnimationControlListener onCancelled types:"
                     + mRequestedTypes);
+            if (mLoggingListener != null) {
+                mLoggingListener.onCancelled(controller);
+            }
         }
 
         protected Interpolator getInsetsInterpolator() {
@@ -1147,6 +1161,13 @@
         updateRequestedVisibilities();
     }
 
+    // TODO(b/242962223): Make this setter restrictive.
+    @Override
+    public void setSystemDrivenInsetsAnimationLoggingListener(
+            @Nullable WindowInsetsAnimationControlListener listener) {
+        mLoggingListener = listener;
+    }
+
     /**
      * @return Pair of (types ready to animate, IME ready to animate).
      */
@@ -1159,7 +1180,7 @@
             final InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i));
             boolean show = animationType == ANIMATION_TYPE_SHOW
                     || animationType == ANIMATION_TYPE_USER;
-            boolean canRun = false;
+            boolean canRun = true;
             if (show) {
                 // Show request
                 if (fromIme) {
@@ -1169,7 +1190,6 @@
                 }
                 switch(consumer.requestShow(fromIme)) {
                     case ShowResult.SHOW_IMMEDIATELY:
-                        canRun = true;
                         break;
                     case ShowResult.IME_SHOW_DELAYED:
                         imeReady = false;
@@ -1180,6 +1200,7 @@
                                 + fromIme);
                         // IME cannot be shown (since it didn't have focus), proceed
                         // with animation of other types.
+                        canRun = false;
                         break;
                 }
             } else {
@@ -1189,7 +1210,6 @@
                 if (!fromIme) {
                     consumer.notifyHidden();
                 }
-                canRun = true;
             }
             if (!canRun) {
                 if (WARN) Log.w(TAG, String.format(
@@ -1460,7 +1480,8 @@
         boolean hasAnimationCallbacks = mHost.hasAnimationCallbacks();
         final InternalAnimationControlListener listener = new InternalAnimationControlListener(
                 show, hasAnimationCallbacks, types, mHost.getSystemBarsBehavior(),
-                skipAnim || mAnimationsDisabled, mHost.dipToPx(FLOATING_IME_BOTTOM_INSET_DP));
+                skipAnim || mAnimationsDisabled, mHost.dipToPx(FLOATING_IME_BOTTOM_INSET_DP),
+                mLoggingListener);
 
         // We are about to playing the default animation (show/hide). Passing a null frame indicates
         // the controlled types should be animated regardless of the frame.
diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS
index 1e1c250..6d64022 100644
--- a/core/java/android/view/OWNERS
+++ b/core/java/android/view/OWNERS
@@ -81,8 +81,8 @@
 per-file DisplayCutout.aidl = file:/services/core/java/com/android/server/wm/OWNERS
 per-file DisplayCutout.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file IDisplay*.aidl = file:/services/core/java/com/android/server/wm/OWNERS
-per-file Inset*.java = file:/services/core/java/com/android/server/wm/OWNERS
-per-file Inset*.aidl = file:/services/core/java/com/android/server/wm/OWNERS
+per-file *Inset*.java = file:/services/core/java/com/android/server/wm/OWNERS
+per-file *Inset*.aidl = file:/services/core/java/com/android/server/wm/OWNERS
 per-file IPinnedStackListener.aidl = file:/services/core/java/com/android/server/wm/OWNERS
 per-file IRecents*.aidl = file:/services/core/java/com/android/server/wm/OWNERS
 per-file IRemote*.aidl = file:/services/core/java/com/android/server/wm/OWNERS
@@ -94,7 +94,6 @@
 per-file SurfaceControl*.aidl = file:/services/core/java/com/android/server/wm/OWNERS
 per-file SurfaceSession.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file SyncRtSurfaceTransactionApplier.java = file:/services/core/java/com/android/server/wm/OWNERS
-per-file ViewRootInsetsControllerHost.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file Window*.java = file:/services/core/java/com/android/server/wm/OWNERS
 per-file Window*.aidl = file:/services/core/java/com/android/server/wm/OWNERS
 per-file TransactionCommittedCallback.java = file:/services/core/java/com/android/server/wm/OWNERS
diff --git a/core/java/android/view/PendingInsetsController.java b/core/java/android/view/PendingInsetsController.java
index c61baf6..3fe9110 100644
--- a/core/java/android/view/PendingInsetsController.java
+++ b/core/java/android/view/PendingInsetsController.java
@@ -44,6 +44,7 @@
     private ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
             = new ArrayList<>();
     private int mCaptionInsetsHeight = 0;
+    private WindowInsetsAnimationControlListener mLoggingListener;
 
     @Override
     public void show(int types) {
@@ -176,6 +177,9 @@
             controller.addOnControllableInsetsChangedListener(
                     mControllableInsetsChangedListeners.get(i));
         }
+        if (mLoggingListener != null) {
+            controller.setSystemDrivenInsetsAnimationLoggingListener(mLoggingListener);
+        }
 
         // Reset all state so it doesn't get applied twice just in case
         mRequests.clear();
@@ -184,7 +188,7 @@
         mAppearance = 0;
         mAppearanceMask = 0;
         mAnimationsDisabled = false;
-
+        mLoggingListener = null;
         // After replaying, we forward everything directly to the replayed instance.
         mReplayedInsetsController = controller;
     }
@@ -198,6 +202,16 @@
     }
 
     @Override
+    public void setSystemDrivenInsetsAnimationLoggingListener(
+            @Nullable WindowInsetsAnimationControlListener listener) {
+        if (mReplayedInsetsController != null) {
+            mReplayedInsetsController.setSystemDrivenInsetsAnimationLoggingListener(listener);
+        } else {
+            mLoggingListener = listener;
+        }
+    }
+
+    @Override
     public void controlWindowInsetsAnimation(@InsetsType int types, long durationMillis,
             @Nullable Interpolator interpolator,
             CancellationSignal cancellationSignal,
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index 5721fa6..3acb053 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -28,8 +28,8 @@
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.util.Log;
-import android.window.WindowTokenClient;
 import android.view.accessibility.IAccessibilityEmbeddedConnection;
+import android.window.WindowTokenClient;
 
 import java.util.Objects;
 
@@ -271,14 +271,8 @@
     /** @hide */
     public SurfaceControlViewHost(@NonNull Context c, @NonNull Display d,
             @NonNull WindowlessWindowManager wwm) {
-        this(c, d, wwm, false /* useSfChoreographer */);
-    }
-
-    /** @hide */
-    public SurfaceControlViewHost(@NonNull Context c, @NonNull Display d,
-            @NonNull WindowlessWindowManager wwm, boolean useSfChoreographer) {
         mWm = wwm;
-        mViewRoot = new ViewRootImpl(c, d, mWm, new WindowlessWindowLayout(), useSfChoreographer);
+        mViewRoot = new ViewRootImpl(c, d, mWm, new WindowlessWindowLayout());
         addConfigCallback(c, d);
 
         WindowManagerGlobal.getInstance().addWindowlessRoot(mViewRoot);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 674f0a2..59bc061 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -173,6 +173,7 @@
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
 import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityInteractionClient;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
 import android.view.accessibility.AccessibilityManager.HighTextContrastChangeListener;
@@ -910,17 +911,11 @@
     private String mTag = TAG;
 
     public ViewRootImpl(Context context, Display display) {
-        this(context, display, WindowManagerGlobal.getWindowSession(), new WindowLayout(),
-                false /* useSfChoreographer */);
+        this(context, display, WindowManagerGlobal.getWindowSession(), new WindowLayout());
     }
 
     public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
             WindowLayout windowLayout) {
-        this(context, display, session, windowLayout, false /* useSfChoreographer */);
-    }
-
-    public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
-            WindowLayout windowLayout, boolean useSfChoreographer) {
         mContext = context;
         mWindowSession = session;
         mWindowLayout = windowLayout;
@@ -952,8 +947,7 @@
         mNoncompatDensity = context.getResources().getDisplayMetrics().noncompatDensityDpi;
         mFallbackEventHandler = new PhoneFallbackEventHandler(context);
         // TODO(b/222696368): remove getSfInstance usage and use vsyncId for transactions
-        mChoreographer = useSfChoreographer
-                ? Choreographer.getSfInstance() : Choreographer.getInstance();
+        mChoreographer = Choreographer.getInstance();
         mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
         mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this));
         mHandwritingInitiator = new HandwritingInitiator(
@@ -5325,6 +5319,7 @@
         }
 
         mAccessibilityInteractionConnectionManager.ensureNoConnection();
+        mAccessibilityInteractionConnectionManager.ensureNoDirectConnection();
         removeSendWindowContentChangedCallback();
 
         destroyHardwareRenderer();
@@ -9577,6 +9572,14 @@
         }
     }
 
+    /**
+     * Return the connection ID for the {@link AccessibilityInteractionController} of this instance.
+     * @see AccessibilityNodeInfo#makeQueryableFromAppProcess(View)
+     */
+    public int getDirectAccessibilityConnectionId() {
+        return mAccessibilityInteractionConnectionManager.ensureDirectConnection();
+    }
+
     @Override
     public boolean showContextMenuForChild(View originalView) {
         return false;
@@ -10452,6 +10455,8 @@
      */
     final class AccessibilityInteractionConnectionManager
             implements AccessibilityStateChangeListener {
+        private int mDirectConnectionId = AccessibilityNodeInfo.UNDEFINED_CONNECTION_ID;
+
         @Override
         public void onAccessibilityStateChanged(boolean enabled) {
             if (enabled) {
@@ -10495,6 +10500,21 @@
                 mAccessibilityManager.removeAccessibilityInteractionConnection(mWindow);
             }
         }
+
+        public int ensureDirectConnection() {
+            if (mDirectConnectionId == AccessibilityNodeInfo.UNDEFINED_CONNECTION_ID) {
+                mDirectConnectionId = AccessibilityInteractionClient.addDirectConnection(
+                        new AccessibilityInteractionConnection(ViewRootImpl.this));
+            }
+            return mDirectConnectionId;
+        }
+
+        public void ensureNoDirectConnection() {
+            if (mDirectConnectionId != AccessibilityNodeInfo.UNDEFINED_CONNECTION_ID) {
+                AccessibilityInteractionClient.removeConnection(mDirectConnectionId);
+                mDirectConnectionId = AccessibilityNodeInfo.UNDEFINED_CONNECTION_ID;
+            }
+        }
     }
 
     final class HighContrastTextManager implements HighTextContrastChangeListener {
diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java
index 227b9f4..63f9e13 100644
--- a/core/java/android/view/WindowInsetsController.java
+++ b/core/java/android/view/WindowInsetsController.java
@@ -201,6 +201,21 @@
             @NonNull WindowInsetsAnimationControlListener listener);
 
     /**
+     * Lets the application add non-controllable listener object that can be called back
+     * when animation is invoked by the system by host calling methods such as {@link #show} or
+     * {@link #hide}.
+     *
+     * The listener is supposed to be used for logging only, using the control or
+     * relying on the timing of the callback in any other way is not supported.
+     *
+     * @param listener The {@link WindowInsetsAnimationControlListener} that gets called when
+     *                 the animation is driven by the system and not the host
+     * @hide
+     */
+    void setSystemDrivenInsetsAnimationLoggingListener(
+            @Nullable WindowInsetsAnimationControlListener listener);
+
+    /**
      * Controls the appearance of system bars.
      * <p>
      * For example, the following statement adds {@link #APPEARANCE_LIGHT_STATUS_BARS}:
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index fe8d64f..a49caaf 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -1054,6 +1054,14 @@
         }
     }
 
+    /**
+     * Ensure scales are between 0 and 20.
+     * @hide
+     */
+    static float fixScale(float scale) {
+        return Math.max(Math.min(scale, 20), 0);
+    }
+
     public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
         /**
          * X position for this window.  With the default gravity it is ignored.
diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
index 6853278..227a8ef 100644
--- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java
+++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java
@@ -114,6 +114,10 @@
     private static final SparseArray<IAccessibilityServiceConnection> sConnectionCache =
             new SparseArray<>();
 
+    // Used to generate connection ids for direct app-process connections. Start sufficiently far
+    // enough from the connection ids generated by AccessibilityManagerService.
+    private static int sDirectConnectionIdCounter = 1 << 30;
+
     /** List of timestamps which indicate the latest time an a11y service receives a scroll event
         from a window, mapping from windowId -> timestamp. */
     private static final SparseLongArray sScrollingWindows = new SparseLongArray();
@@ -232,6 +236,12 @@
             return;
         }
         synchronized (sConnectionCache) {
+            IAccessibilityServiceConnection existingConnection = getConnection(connectionId);
+            if (existingConnection instanceof DirectAccessibilityConnection) {
+                throw new IllegalArgumentException(
+                        "Cannot add service connection with id " + connectionId
+                                + " which conflicts with existing direct connection.");
+            }
             sConnectionCache.put(connectionId, connection);
             if (!initializeCache) {
                 return;
@@ -242,6 +252,33 @@
     }
 
     /**
+     * Adds a new {@link DirectAccessibilityConnection} using the provided
+     * {@link IAccessibilityInteractionConnection} to create a direct connection between
+     * this client and the {@link android.view.ViewRootImpl} for queries inside the app process.
+     *
+     * <p>
+     * See {@link DirectAccessibilityConnection} for supported methods.
+     * </p>
+     *
+     * @param connection The ViewRootImpl's {@link IAccessibilityInteractionConnection}.
+     */
+    public static int addDirectConnection(IAccessibilityInteractionConnection connection) {
+        synchronized (sConnectionCache) {
+            int connectionId = sDirectConnectionIdCounter++;
+            if (getConnection(connectionId) != null) {
+                throw new IllegalArgumentException(
+                        "Cannot add direct connection with existing id " + connectionId);
+            }
+            DirectAccessibilityConnection directAccessibilityConnection =
+                    new DirectAccessibilityConnection(connection);
+            sConnectionCache.put(connectionId, directAccessibilityConnection);
+            // Do not use AccessibilityCache for this connection, since there is no corresponding
+            // AccessibilityService to handle cache invalidation events.
+            return connectionId;
+        }
+    }
+
+    /**
      * Gets a cached associated with the connection id if available.
      *
      */
diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
index 953f261..5d52750 100644
--- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java
@@ -58,6 +58,7 @@
 import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewRootImpl;
 import android.widget.TextView;
 
 import com.android.internal.R;
@@ -82,7 +83,9 @@
  * </p>
  * <p>
  * Once an accessibility node info is delivered to an accessibility service it is
- * made immutable and calling a state mutation method generates an error.
+ * made immutable and calling a state mutation method generates an error. See
+ * {@link #makeQueryableFromAppProcess(View)} if you would like to inspect the
+ * node tree from the app process for testing or debugging tools.
  * </p>
  * <p>
  * Please refer to {@link android.accessibilityservice.AccessibilityService} for
@@ -1156,8 +1159,8 @@
      * @param index The child index.
      * @return The child node.
      *
-     * @throws IllegalStateException If called outside of an AccessibilityService.
-     *
+     * @throws IllegalStateException If called outside of an {@link AccessibilityService} and before
+     *                               calling {@link #makeQueryableFromAppProcess(View)}.
      */
     public AccessibilityNodeInfo getChild(int index) {
         return getChild(index, FLAG_PREFETCH_DESCENDANTS_HYBRID);
@@ -1171,7 +1174,8 @@
      * @param prefetchingStrategy the prefetching strategy.
      * @return The child node.
      *
-     * @throws IllegalStateException If called outside of an AccessibilityService.
+     * @throws IllegalStateException If called outside of an {@link AccessibilityService} and before
+     *                               calling {@link #makeQueryableFromAppProcess(View)}.
      *
      * @see AccessibilityNodeInfo#getParent(int) for a description of prefetching.
      */
@@ -1893,6 +1897,9 @@
      * Gets the parent.
      *
      * @return The parent.
+     *
+     * @throws IllegalStateException If called outside of an {@link AccessibilityService} and before
+     *                               calling {@link #makeQueryableFromAppProcess(View)}.
      */
     public AccessibilityNodeInfo getParent() {
         enforceSealed();
@@ -1920,7 +1927,8 @@
      * @param prefetchingStrategy the prefetching strategy.
      * @return The parent.
      *
-     * @throws IllegalStateException If called outside of an AccessibilityService.
+     * @throws IllegalStateException If called outside of an {@link AccessibilityService} and before
+     *                               calling {@link #makeQueryableFromAppProcess(View)}.
      *
      * @see #FLAG_PREFETCH_ANCESTORS
      * @see #FLAG_PREFETCH_DESCENDANTS_BREADTH_FIRST
@@ -3642,6 +3650,47 @@
     }
 
     /**
+     * Connects this node to the View's root so that operations on this node can query the entire
+     * {@link AccessibilityNodeInfo} tree and perform accessibility actions on nodes.
+     *
+     * <p>
+     * This is intended for short-lived inspections from testing or debugging tools in the app
+     * process. After calling this method, all nodes linked to this node (children, ancestors, etc.)
+     * are also queryable. Operations on this node tree will only succeed as long as the associated
+     * view hierarchy remains attached to a window.
+     * </p>
+     *
+     * <p>
+     * Calling this method more than once on the same node is a no-op; if you wish to inspect a
+     * different view hierarchy then create a new node from any view in that hierarchy and call this
+     * method on that node.
+     * </p>
+     *
+     * <p>
+     * Testing or debugging tools should create this {@link AccessibilityNodeInfo} node using
+     * {@link View#createAccessibilityNodeInfo()} or {@link AccessibilityNodeProvider} and call this
+     * method, then navigate and interact with the node tree by calling methods on the node.
+     * </p>
+     *
+     * @param view The view that generated this node, or any view in the same view-root hierarchy.
+     * @throws IllegalStateException If called from an {@link AccessibilityService}, or if provided
+     *                               a {@link View} that is not attached to a window.
+     */
+    public void makeQueryableFromAppProcess(@NonNull View view) {
+        enforceNotSealed();
+        if (mConnectionId != UNDEFINED_CONNECTION_ID) {
+            return;
+        }
+
+        ViewRootImpl viewRootImpl = view.getViewRootImpl();
+        if (viewRootImpl == null) {
+            throw new IllegalStateException(
+                    "Cannot link a node to a view that is not attached to a window.");
+        }
+        setConnectionId(viewRootImpl.getDirectAccessibilityConnectionId());
+    }
+
+    /**
      * Sets if this instance is sealed.
      *
      * @param sealed Whether is sealed.
@@ -3665,15 +3714,21 @@
         return mSealed;
     }
 
+    private static boolean usingDirectConnection(int connectionId) {
+        return AccessibilityInteractionClient.getConnection(
+                connectionId) instanceof DirectAccessibilityConnection;
+    }
+
     /**
-     * Enforces that this instance is sealed.
+     * Enforces that this instance is sealed, unless using a {@link DirectAccessibilityConnection}
+     * which allows queries while the node is not sealed.
      *
      * @throws IllegalStateException If this instance is not sealed.
      *
      * @hide
      */
     protected void enforceSealed() {
-        if (!isSealed()) {
+        if (!usingDirectConnection(mConnectionId) && !isSealed()) {
             throw new IllegalStateException("Cannot perform this "
                     + "action on a not sealed instance.");
         }
@@ -4499,7 +4554,8 @@
 
     private static boolean canPerformRequestOverConnection(int connectionId,
             int windowId, long accessibilityNodeId) {
-        return ((windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID)
+        final boolean hasWindowId = windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
+        return ((usingDirectConnection(connectionId) || hasWindowId)
                 && (getAccessibilityViewId(accessibilityNodeId) != UNDEFINED_ITEM_ID)
                 && (connectionId != UNDEFINED_CONNECTION_ID));
     }
diff --git a/core/java/android/view/accessibility/DirectAccessibilityConnection.java b/core/java/android/view/accessibility/DirectAccessibilityConnection.java
new file mode 100644
index 0000000..71746ee
--- /dev/null
+++ b/core/java/android/view/accessibility/DirectAccessibilityConnection.java
@@ -0,0 +1,136 @@
+/*
+ * 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.view.accessibility;
+
+import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.graphics.Matrix;
+import android.graphics.Region;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.RemoteException;
+import android.view.MagnificationSpec;
+
+/**
+ * Minimal {@link IAccessibilityServiceConnection} implementation that interacts
+ * with the {@link android.view.AccessibilityInteractionController} of a
+ * {@link android.view.ViewRootImpl}.
+ *
+ * <p>
+ * Uses {@link android.view.ViewRootImpl}'s {@link IAccessibilityServiceConnection} that wraps
+ * {@link android.view.AccessibilityInteractionController} within the app process, so that no
+ * interprocess communication is performed.
+ * </p>
+ *
+ * <p>
+ * Only the following methods are supported:
+ * <li>{@link #findAccessibilityNodeInfoByAccessibilityId}</li>
+ * <li>{@link #findAccessibilityNodeInfosByText}</li>
+ * <li>{@link #findAccessibilityNodeInfosByViewId}</li>
+ * <li>{@link #findFocus}</li>
+ * <li>{@link #focusSearch}</li>
+ * <li>{@link #performAccessibilityAction}</li>
+ * </p>
+ *
+ * <p>
+ * Other methods are no-ops and return default values.
+ * </p>
+ */
+class DirectAccessibilityConnection extends IAccessibilityServiceConnection.Default {
+    private final IAccessibilityInteractionConnection mAccessibilityInteractionConnection;
+
+    // Fetch all views, but do not use prefetching/cache since this "connection" does not
+    // receive cache invalidation events (as it is not linked to an AccessibilityService).
+    private static final int FETCH_FLAGS =
+            AccessibilityNodeInfo.FLAG_REPORT_VIEW_IDS
+                    | AccessibilityNodeInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
+    private static final MagnificationSpec MAGNIFICATION_SPEC = new MagnificationSpec();
+    private static final int PID = Process.myPid();
+    private static final Region INTERACTIVE_REGION = null;
+    private static final float[] TRANSFORM_MATRIX = new float[9];
+
+    static {
+        Matrix.IDENTITY_MATRIX.getValues(TRANSFORM_MATRIX);
+    }
+
+    DirectAccessibilityConnection(
+            IAccessibilityInteractionConnection accessibilityInteractionConnection) {
+        mAccessibilityInteractionConnection = accessibilityInteractionConnection;
+    }
+
+    @Override
+    public String[] findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId,
+            long accessibilityNodeId, int interactionId,
+            IAccessibilityInteractionConnectionCallback callback, int flags, long threadId,
+            Bundle arguments) throws RemoteException {
+        mAccessibilityInteractionConnection.findAccessibilityNodeInfoByAccessibilityId(
+                accessibilityNodeId, INTERACTIVE_REGION, interactionId, callback, FETCH_FLAGS, PID,
+                threadId, MAGNIFICATION_SPEC, TRANSFORM_MATRIX, arguments);
+        return new String[0];
+    }
+
+    @Override
+    public String[] findAccessibilityNodeInfosByText(int accessibilityWindowId,
+            long accessibilityNodeId, String text, int interactionId,
+            IAccessibilityInteractionConnectionCallback callback, long threadId)
+            throws RemoteException {
+        mAccessibilityInteractionConnection.findAccessibilityNodeInfosByText(accessibilityNodeId,
+                text, INTERACTIVE_REGION, interactionId, callback, FETCH_FLAGS, PID, threadId,
+                MAGNIFICATION_SPEC, TRANSFORM_MATRIX);
+        return new String[0];
+    }
+
+    @Override
+    public String[] findAccessibilityNodeInfosByViewId(int accessibilityWindowId,
+            long accessibilityNodeId, String viewId, int interactionId,
+            IAccessibilityInteractionConnectionCallback callback, long threadId)
+            throws RemoteException {
+        mAccessibilityInteractionConnection.findAccessibilityNodeInfosByViewId(accessibilityNodeId,
+                viewId, INTERACTIVE_REGION, interactionId, callback, FETCH_FLAGS, PID, threadId,
+                MAGNIFICATION_SPEC, TRANSFORM_MATRIX);
+        return new String[0];
+    }
+
+    @Override
+    public String[] findFocus(int accessibilityWindowId, long accessibilityNodeId, int focusType,
+            int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId)
+            throws RemoteException {
+        mAccessibilityInteractionConnection.findFocus(accessibilityNodeId, focusType,
+                INTERACTIVE_REGION, interactionId, callback, FETCH_FLAGS, PID, threadId,
+                MAGNIFICATION_SPEC, TRANSFORM_MATRIX);
+        return new String[0];
+    }
+
+    @Override
+    public String[] focusSearch(int accessibilityWindowId, long accessibilityNodeId, int direction,
+            int interactionId, IAccessibilityInteractionConnectionCallback callback, long threadId)
+            throws RemoteException {
+        mAccessibilityInteractionConnection.focusSearch(accessibilityNodeId, direction,
+                INTERACTIVE_REGION, interactionId, callback, FETCH_FLAGS, PID, threadId,
+                MAGNIFICATION_SPEC, TRANSFORM_MATRIX);
+        return new String[0];
+    }
+
+    @Override
+    public boolean performAccessibilityAction(int accessibilityWindowId, long accessibilityNodeId,
+            int action, Bundle arguments, int interactionId,
+            IAccessibilityInteractionConnectionCallback callback, long threadId)
+            throws RemoteException {
+        mAccessibilityInteractionConnection.performAccessibilityAction(accessibilityNodeId, action,
+                arguments, interactionId, callback, FETCH_FLAGS, PID, threadId);
+        return true;
+    }
+}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 8d3cf6d..6049613 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -91,6 +91,7 @@
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewRootImpl;
+import android.view.WindowInsets;
 import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillManager;
@@ -2150,8 +2151,9 @@
                 null /* icProto */);
         synchronized (mH) {
             final View view = getServedViewLocked();
-            if (mImeInsetsConsumer != null && view != null) {
-                if (mImeInsetsConsumer.isRequestedVisible()) {
+            if (view != null) {
+                final WindowInsets rootInsets = view.getRootWindowInsets();
+                if (rootInsets != null && rootInsets.isVisible(WindowInsets.Type.ime())) {
                     hideSoftInputFromWindow(view.getWindowToken(), hideFlags, null,
                             SoftInputShowHideReason.HIDE_TOGGLE_SOFT_INPUT);
                 } else {
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index e567ced..cd15df84 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -16,7 +16,7 @@
 
 package android.window;
 
-import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENT_TO_TASK;
+import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
@@ -26,16 +26,15 @@
 import android.annotation.CallSuper;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.TestApi;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.util.SparseArray;
 import android.view.RemoteAnimationDefinition;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
 
@@ -74,12 +73,6 @@
      */
     private final Executor mExecutor;
 
-    // TODO(b/240519866): doing so to keep CTS compatibility. Remove in the next release.
-    /** Map from Task id to client tokens of TaskFragments in the Task. */
-    private final SparseArray<List<IBinder>> mTaskIdToFragmentTokens = new SparseArray<>();
-    /** Map from Task id to Task configuration. */
-    private final SparseArray<Configuration> mTaskIdToConfigurations = new SparseArray<>();
-
     public TaskFragmentOrganizer(@NonNull Executor executor) {
         mExecutor = executor;
     }
@@ -147,14 +140,35 @@
         }
     }
 
-    /** Called when a TaskFragment is created and organized by this organizer. */
-    public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {}
+    /**
+     * Called when a TaskFragment is created and organized by this organizer.
+     *
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
+     * @param taskFragmentInfo  Info of the TaskFragment that is created.
+     */
+    public void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {}
 
-    /** Called when the status of an organized TaskFragment is changed. */
-    public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {}
+    /**
+     * Called when the status of an organized TaskFragment is changed.
+     *
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
+     * @param taskFragmentInfo  Info of the TaskFragment that is changed.
+     */
+    public void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {}
 
-    /** Called when an organized TaskFragment is removed. */
-    public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {}
+    /**
+     * Called when an organized TaskFragment is removed.
+     *
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
+     * @param taskFragmentInfo  Info of the TaskFragment that is removed.
+     */
+    public void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {}
 
     /**
      * Called when the parent leaf Task of organized TaskFragments is changed.
@@ -164,35 +178,21 @@
      * For case like screen size change, it will trigger onTaskFragmentParentInfoChanged with new
      * Task bounds, but may not trigger onTaskFragmentInfoChanged because there can be an override
      * bounds.
-     */
-    public void onTaskFragmentParentInfoChanged(
-            @NonNull IBinder fragmentToken, @NonNull Configuration parentConfig) {}
-
-    /**
-     * Called when the parent leaf Task of organized TaskFragments is changed.
-     * When the leaf Task is changed, the organizer may want to update the TaskFragments in one
-     * transaction.
      *
-     * For case like screen size change, it will trigger onTaskFragmentParentInfoChanged with new
-     * Task bounds, but may not trigger onTaskFragmentInfoChanged because there can be an override
-     * bounds.
-     * @hide
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
+     * @param taskId    Id of the parent Task that is changed.
+     * @param parentConfig  Config of the parent Task.
      */
-    public void onTaskFragmentParentInfoChanged(int taskId, @NonNull Configuration parentConfig) {
-        // TODO(b/240519866): doing so to keep CTS compatibility. Remove in the next release.
-        final List<IBinder> tokens = mTaskIdToFragmentTokens.get(taskId);
-        if (tokens == null || tokens.isEmpty()) {
-            return;
-        }
-        for (int i = tokens.size() - 1; i >= 0; i--) {
-            onTaskFragmentParentInfoChanged(tokens.get(i), parentConfig);
-        }
-    }
+    public void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct, int taskId,
+            @NonNull Configuration parentConfig) {}
 
     /**
      * Called when the {@link WindowContainerTransaction} created with
      * {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)} failed on the server side.
      *
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
      * @param errorCallbackToken    token set in
      *                             {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)}
      * @param taskFragmentInfo  The {@link TaskFragmentInfo}. This could be {@code null} if no
@@ -201,16 +201,18 @@
      *                          transaction operation.
      * @param exception             exception from the server side.
      */
-    public void onTaskFragmentError(
+    public void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
             @NonNull IBinder errorCallbackToken, @Nullable TaskFragmentInfo taskFragmentInfo,
             int opType, @NonNull Throwable exception) {}
 
     /**
      * Called when an Activity is reparented to the Task with organized TaskFragment. For example,
      * when an Activity enters and then exits Picture-in-picture, it will be reparented back to its
-     * orginial Task. In this case, we need to notify the organizer so that it can check if the
+     * original Task. In this case, we need to notify the organizer so that it can check if the
      * Activity matches any split rule.
      *
+     * @param wct   The {@link WindowContainerTransaction} to make any changes with if needed. No
+     *              need to call {@link #applyTransaction} as it will be applied by the caller.
      * @param taskId            The Task that the activity is reparented to.
      * @param activityIntent    The intent that the activity is original launched with.
      * @param activityToken     If the activity belongs to the same process as the organizer, this
@@ -218,61 +220,41 @@
      *                          different process, the server will generate a temporary token that
      *                          the organizer can use to reparent the activity through
      *                          {@link WindowContainerTransaction} if needed.
-     * @hide
      */
-    public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
-            @NonNull IBinder activityToken) {}
+    public void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
+            int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken) {}
 
     /**
      * Called when the transaction is ready so that the organizer can update the TaskFragments based
      * on the changes in transaction.
+     * Note: {@link WindowOrganizer#applyTransaction} permission requirement is conditional for
+     * {@link TaskFragmentOrganizer}.
+     * @see com.android.server.wm.WindowOrganizerController#enforceTaskPermission
      * @hide
      */
+    @SuppressLint("AndroidFrameworkRequiresPermission")
     public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
         final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
         for (TaskFragmentTransaction.Change change : changes) {
-            // TODO(b/240519866): apply all changes in one WCT.
             final int taskId = change.getTaskId();
             switch (change.getType()) {
                 case TYPE_TASK_FRAGMENT_APPEARED:
-                    // TODO(b/240519866): doing so to keep CTS compatibility. Remove in the next
-                    // release.
-                    if (!mTaskIdToFragmentTokens.contains(taskId)) {
-                        mTaskIdToFragmentTokens.put(taskId, new ArrayList<>());
-                    }
-                    mTaskIdToFragmentTokens.get(taskId).add(change.getTaskFragmentToken());
-                    onTaskFragmentParentInfoChanged(change.getTaskFragmentToken(),
-                            mTaskIdToConfigurations.get(taskId));
-
-                    onTaskFragmentAppeared(change.getTaskFragmentInfo());
+                    onTaskFragmentAppeared(wct, change.getTaskFragmentInfo());
                     break;
                 case TYPE_TASK_FRAGMENT_INFO_CHANGED:
-                    onTaskFragmentInfoChanged(change.getTaskFragmentInfo());
+                    onTaskFragmentInfoChanged(wct, change.getTaskFragmentInfo());
                     break;
                 case TYPE_TASK_FRAGMENT_VANISHED:
-                    // TODO(b/240519866): doing so to keep CTS compatibility. Remove in the next
-                    // release.
-                    if (mTaskIdToFragmentTokens.contains(taskId)) {
-                        final List<IBinder> tokens = mTaskIdToFragmentTokens.get(taskId);
-                        tokens.remove(change.getTaskFragmentToken());
-                        if (tokens.isEmpty()) {
-                            mTaskIdToFragmentTokens.remove(taskId);
-                            mTaskIdToConfigurations.remove(taskId);
-                        }
-                    }
-
-                    onTaskFragmentVanished(change.getTaskFragmentInfo());
+                    onTaskFragmentVanished(wct, change.getTaskFragmentInfo());
                     break;
                 case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED:
-                    // TODO(b/240519866): doing so to keep CTS compatibility. Remove in the next
-                    // release.
-                    mTaskIdToConfigurations.put(taskId, change.getTaskConfiguration());
-
-                    onTaskFragmentParentInfoChanged(taskId, change.getTaskConfiguration());
+                    onTaskFragmentParentInfoChanged(wct, taskId, change.getTaskConfiguration());
                     break;
                 case TYPE_TASK_FRAGMENT_ERROR:
                     final Bundle errorBundle = change.getErrorBundle();
                     onTaskFragmentError(
+                            wct,
                             change.getErrorCallbackToken(),
                             errorBundle.getParcelable(
                                     KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO, TaskFragmentInfo.class),
@@ -280,8 +262,9 @@
                             errorBundle.getSerializable(KEY_ERROR_CALLBACK_EXCEPTION,
                                     java.lang.Throwable.class));
                     break;
-                case TYPE_ACTIVITY_REPARENT_TO_TASK:
-                    onActivityReparentToTask(
+                case TYPE_ACTIVITY_REPARENTED_TO_TASK:
+                    onActivityReparentedToTask(
+                            wct,
                             change.getTaskId(),
                             change.getActivityIntent(),
                             change.getActivityToken());
@@ -291,6 +274,8 @@
                             "Unknown TaskFragmentEvent=" + change.getType());
             }
         }
+        // TODO(b/240519866): notify TaskFragmentOrganizerController that the transition is done.
+        applyTransaction(wct);
     }
 
     @Override
diff --git a/core/java/android/window/TaskFragmentTransaction.java b/core/java/android/window/TaskFragmentTransaction.java
index 755864f..07e8e8c 100644
--- a/core/java/android/window/TaskFragmentTransaction.java
+++ b/core/java/android/window/TaskFragmentTransaction.java
@@ -122,7 +122,7 @@
      * then exits Picture-in-picture, it will be reparented back to its original Task. In this case,
      * we need to notify the organizer so that it can check if the Activity matches any split rule.
      */
-    public static final int TYPE_ACTIVITY_REPARENT_TO_TASK = 6;
+    public static final int TYPE_ACTIVITY_REPARENTED_TO_TASK = 6;
 
     @IntDef(prefix = { "TYPE_" }, value = {
             TYPE_TASK_FRAGMENT_APPEARED,
@@ -130,7 +130,7 @@
             TYPE_TASK_FRAGMENT_VANISHED,
             TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED,
             TYPE_TASK_FRAGMENT_ERROR,
-            TYPE_ACTIVITY_REPARENT_TO_TASK
+            TYPE_ACTIVITY_REPARENTED_TO_TASK
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface ChangeType {}
@@ -247,7 +247,7 @@
 
         /**
          * Intent of the activity that is reparented to the Task for
-         * {@link #TYPE_ACTIVITY_REPARENT_TO_TASK}.
+         * {@link #TYPE_ACTIVITY_REPARENTED_TO_TASK}.
          */
         public Change setActivityIntent(@NonNull Intent intent) {
             mActivityIntent = requireNonNull(intent);
@@ -255,7 +255,7 @@
         }
 
         /**
-         * Token of the reparent activity for {@link #TYPE_ACTIVITY_REPARENT_TO_TASK}.
+         * Token of the reparent activity for {@link #TYPE_ACTIVITY_REPARENTED_TO_TASK}.
          * If the activity belongs to the same process as the organizer, this will be the actual
          * activity token; if the activity belongs to a different process, the server will generate
          * a temporary token that the organizer can use to reparent the activity through
diff --git a/core/java/com/android/internal/app/BilingualSuggestedLocaleAdapter.java b/core/java/com/android/internal/app/BilingualSuggestedLocaleAdapter.java
index d2cb30e..833d88c 100644
--- a/core/java/com/android/internal/app/BilingualSuggestedLocaleAdapter.java
+++ b/core/java/com/android/internal/app/BilingualSuggestedLocaleAdapter.java
@@ -20,6 +20,8 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import com.android.internal.R;
@@ -36,11 +38,21 @@
 
     private final Locale mSecondaryLocale;
     private final int mSecondaryLocaleTextDir;
+    private final boolean mShowSelection;
+    private LocaleStore.LocaleInfo mSelectedLocaleInfo;
 
     public BilingualSuggestedLocaleAdapter(
             Set<LocaleStore.LocaleInfo> localeOptions,
             boolean countryMode,
             Locale secondaryLocale) {
+        this(localeOptions, countryMode, secondaryLocale, false);
+    }
+
+    public BilingualSuggestedLocaleAdapter(
+            Set<LocaleStore.LocaleInfo> localeOptions,
+            boolean countryMode,
+            Locale secondaryLocale,
+            boolean showLastSelected) {
         super(localeOptions, countryMode);
         mSecondaryLocale = secondaryLocale;
         if (TextUtils.getLayoutDirectionFromLocale(secondaryLocale) == View.LAYOUT_DIRECTION_RTL) {
@@ -48,6 +60,7 @@
         } else {
             mSecondaryLocaleTextDir = View.TEXT_DIRECTION_LTR;
         }
+        mShowSelection = showLastSelected;
     }
 
     @Override
@@ -90,11 +103,55 @@
                 }
 
                 LocaleStore.LocaleInfo item = (LocaleStore.LocaleInfo) getItem(position);
+                if (mShowSelection) {
+                    setItemState(isSelectedLocaleInfo(item), convertView);
+                }
                 setLocaleToListItem(convertView, item);
         }
         return convertView;
     }
 
+    /**
+     * Set locale info as selected. Selected info can be the only one. Passing null would result to
+     * nothing is selected.
+     */
+    public void setSelectedLocaleInfo(LocaleStore.LocaleInfo info) {
+        mSelectedLocaleInfo = info;
+        notifyDataSetChanged();
+    }
+
+    /** Return selected locale info. */
+    public LocaleStore.LocaleInfo getSelectedLocaleInfo() {
+        return mSelectedLocaleInfo;
+    }
+
+    private boolean isSelectedLocaleInfo(LocaleStore.LocaleInfo item) {
+        return item != null
+                && mSelectedLocaleInfo != null
+                && item.getId().equals(mSelectedLocaleInfo.getId());
+    }
+
+    private void setItemState(boolean selected, View itemView) {
+        RelativeLayout background = (RelativeLayout) itemView;
+        ImageView indicator = itemView.findViewById(R.id.indicator);
+        TextView textNative = itemView.findViewById(R.id.locale_native);
+        TextView textSecondary = itemView.findViewById(R.id.locale_secondary);
+
+        if (indicator == null || textNative == null || textSecondary == null) {
+            return;
+        }
+
+        textNative.setSelected(selected);
+        textSecondary.setSelected(selected);
+        if (selected) {
+            background.setBackgroundResource(R.drawable.language_picker_item_bg_selected);
+            indicator.setVisibility(View.VISIBLE);
+        } else {
+            background.setBackgroundResource(0);
+            indicator.setVisibility(View.GONE);
+        }
+    }
+
     private void setHeaderText(
             TextView textView, int languageStringResourceId, int regionStringResourceId) {
         if (mCountryMode) {
@@ -114,7 +171,7 @@
         textNative.setTextLocale(localeInfo.getLocale());
         textNative.setContentDescription(localeInfo.getContentDescription(mCountryMode));
 
-        TextView textSecondary = (TextView) itemView.findViewById(R.id.locale_secondary);
+        TextView textSecondary = itemView.findViewById(R.id.locale_secondary);
         textSecondary.setText(localeInfo.getLocale().getDisplayLanguage(mSecondaryLocale));
         textSecondary.setTextDirection(mSecondaryLocaleTextDir);
         if (mCountryMode) {
diff --git a/core/java/com/android/internal/os/BinderCallsStats.java b/core/java/com/android/internal/os/BinderCallsStats.java
index fa6fa55..0a29fc52 100644
--- a/core/java/com/android/internal/os/BinderCallsStats.java
+++ b/core/java/com/android/internal/os/BinderCallsStats.java
@@ -1169,15 +1169,7 @@
 
     }
 
-    /** @hide */
-    public static void startForWifi(Context context) {
-        new BinderCallsStats.SettingsObserver(
-            context,
-            new BinderCallsStats(
-                new BinderCallsStats.Injector(),
-                com.android.internal.os.BinderLatencyProto.Dims.WIFI));
 
-    }
 
     /**
      * Settings observer for other processes (not system_server).
diff --git a/core/java/com/android/internal/view/inline/InlineTooltipUi.java b/core/java/com/android/internal/view/inline/InlineTooltipUi.java
index 3eae89e..836786d 100644
--- a/core/java/com/android/internal/view/inline/InlineTooltipUi.java
+++ b/core/java/com/android/internal/view/inline/InlineTooltipUi.java
@@ -170,9 +170,9 @@
 
             int delayTimeMs = mShowDelayConfigMs;
             try {
-                final float scale = Settings.Global.getFloat(
+                final float scale = WindowManager.fixScale(Settings.Global.getFloat(
                         anchor.getContext().getContentResolver(),
-                        Settings.Global.ANIMATOR_DURATION_SCALE);
+                        Settings.Global.ANIMATOR_DURATION_SCALE));
                 delayTimeMs *= scale;
             } catch (Settings.SettingNotFoundException e) {
                 // do nothing
diff --git a/core/jni/android_hardware_HardwareBuffer.cpp b/core/jni/android_hardware_HardwareBuffer.cpp
index f462523..5fcc46e 100644
--- a/core/jni/android_hardware_HardwareBuffer.cpp
+++ b/core/jni/android_hardware_HardwareBuffer.cpp
@@ -163,7 +163,7 @@
 static jlong android_hardware_HardwareBuffer_getUsage(JNIEnv* env,
     jobject clazz, jlong nativeObject) {
     GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer(nativeObject);
-    return AHardwareBuffer_convertFromGrallocUsageBits(buffer->getUsage());
+    return static_cast<jlong>(AHardwareBuffer_convertFromGrallocUsageBits(buffer->getUsage()));
 }
 
 static jlong android_hardware_HardwareBuffer_estimateSize(jlong nativeObject) {
@@ -177,7 +177,12 @@
 
     const uint32_t bufferStride =
             buffer->getStride() > 0 ? buffer->getStride() : buffer->getWidth();
-    return static_cast<jlong>(buffer->getHeight() * bufferStride * bpp);
+    return static_cast<jlong>(static_cast<uint64_t>(buffer->getHeight() * bufferStride * bpp));
+}
+
+static jlong android_hardware_HardwareBuffer_getId(jlong nativeObject) {
+    GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer(nativeObject);
+    return static_cast<jlong>(buffer->getId());
 }
 
 // ----------------------------------------------------------------------------
@@ -223,16 +228,6 @@
     }
 }
 
-GraphicBuffer* android_hardware_HardwareBuffer_getNativeGraphicBuffer(
-        JNIEnv* env, jobject hardwareBufferObj) {
-    if (env->IsInstanceOf(hardwareBufferObj, gHardwareBufferClassInfo.clazz)) {
-        return GraphicBufferWrapper_to_GraphicBuffer(
-                env->GetLongField(hardwareBufferObj, gHardwareBufferClassInfo.mNativeObject));
-    } else {
-        return nullptr;
-    }
-}
-
 jobject android_hardware_HardwareBuffer_createFromAHardwareBuffer(
         JNIEnv* env, AHardwareBuffer* hardwareBuffer) {
     GraphicBuffer* buffer = AHardwareBuffer_to_GraphicBuffer(hardwareBuffer);
@@ -295,6 +290,7 @@
 
     // --------------- @CriticalNative ----------------------
     { "nEstimateSize", "(J)J",  (void*) android_hardware_HardwareBuffer_estimateSize },
+    { "nGetId", "(J)J",  (void*) android_hardware_HardwareBuffer_getId },
 };
 // clang-format on
 
diff --git a/core/jni/include/android_runtime/android_hardware_HardwareBuffer.h b/core/jni/include/android_runtime/android_hardware_HardwareBuffer.h
index dfd8035..964c28f 100644
--- a/core/jni/include/android_runtime/android_hardware_HardwareBuffer.h
+++ b/core/jni/include/android_runtime/android_hardware_HardwareBuffer.h
@@ -28,10 +28,6 @@
 extern AHardwareBuffer* android_hardware_HardwareBuffer_getNativeHardwareBuffer(
         JNIEnv* env, jobject hardwareBufferObj);
 
-/* Gets the underlying GraphicBuffer for a HardwareBuffer. */
-extern GraphicBuffer* android_hardware_HardwareBuffer_getNativeGraphicBuffer(
-        JNIEnv* env, jobject hardwareBufferObj);
-
 /* Returns a HardwareBuffer wrapper for the underlying AHardwareBuffer. */
 extern jobject android_hardware_HardwareBuffer_createFromAHardwareBuffer(
         JNIEnv* env, AHardwareBuffer* hardwareBuffer);
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 322354b..789ceff 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -89,6 +89,14 @@
         // Setting for accessibility magnification for following typing.
         optional SettingProto accessibility_magnification_follow_typing_enabled = 43 [ (android.privacy).dest = DEST_AUTOMATIC ];
         optional SettingProto accessibility_software_cursor_enabled = 44 [ (android.privacy).dest = DEST_AUTOMATIC ];
+
+        message SoftwareCursorSettings {
+            optional SettingProto trigger_hints_enabled = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
+            optional SettingProto keyboard_shift_enabled = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        }
+
+        optional SoftwareCursorSettings accessibility_software_cursor_settings = 45 [ (android.privacy).dest = DEST_AUTOMATIC ];
+
     }
     optional Accessibility accessibility = 2;
 
diff --git a/core/res/res/drawable/bilingual_language_item_selection_indicator.xml b/core/res/res/drawable/bilingual_language_item_selection_indicator.xml
new file mode 100644
index 0000000..78f26cc
--- /dev/null
+++ b/core/res/res/drawable/bilingual_language_item_selection_indicator.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp" android:height="24dp"
+    android:viewportWidth="24" android:viewportHeight="24">
+  <path android:fillColor="@color/language_picker_item_selected_indicator"
+      android:pathData="M9.55,18l-5.7,-5.7 1.425,-1.425L9.55,15.15l9.175,-9.175L20.15,7.4z"/>
+</vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/language_picker_item_bg_selected.xml b/core/res/res/drawable/language_picker_item_bg_selected.xml
new file mode 100644
index 0000000..ef9a8e7
--- /dev/null
+++ b/core/res/res/drawable/language_picker_item_bg_selected.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+
+  <item
+      android:bottom="-5dp"
+      android:right="-5dp"
+      android:top="-5dp">
+    <shape android:shape="rectangle" >
+      <solid android:color="@color/language_picker_item_selected_bg" />
+
+      <stroke
+          android:width="2dp"
+          android:color="@color/language_picker_item_selected_stroke" />
+    </shape>
+  </item>
+
+</layer-list>
\ No newline at end of file
diff --git a/core/res/res/drawable/language_picker_item_text_color2_selector.xml b/core/res/res/drawable/language_picker_item_text_color2_selector.xml
new file mode 100644
index 0000000..624ae29
--- /dev/null
+++ b/core/res/res/drawable/language_picker_item_text_color2_selector.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+  <item android:state_selected="true"
+      android:color="@color/language_picker_item_text_color_secondary_selected" />
+  <item android:color="@color/language_picker_item_text_color_secondary" />
+</selector>
\ No newline at end of file
diff --git a/core/res/res/drawable/language_picker_item_text_color_selector.xml b/core/res/res/drawable/language_picker_item_text_color_selector.xml
new file mode 100644
index 0000000..8d419f8
--- /dev/null
+++ b/core/res/res/drawable/language_picker_item_text_color_selector.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+   <item android:state_selected="true"
+         android:color="@color/language_picker_item_text_color_selected" />
+   <item android:color="@color/language_picker_item_text_color" />
+</selector>
\ No newline at end of file
diff --git a/core/res/res/layout/language_picker_bilingual_item.xml b/core/res/res/layout/language_picker_bilingual_item.xml
index f56dda9..def0cccf 100644
--- a/core/res/res/layout/language_picker_bilingual_item.xml
+++ b/core/res/res/layout/language_picker_bilingual_item.xml
@@ -1,20 +1,27 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
+<RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/language_item"
+    android:layout_height="wrap_content"
+    android:layout_width="match_parent"
+    android:paddingTop="12dp"
+    android:paddingBottom="12dp"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    >
+
+<LinearLayout
     android:id="@+id/frame"
     android:layout_height="wrap_content"
     android:layout_width="match_parent"
-    android:paddingTop="8dp"
-    android:paddingBottom="8dp"
-    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
-    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
-    android:orientation="vertical">
+    android:orientation="vertical"
+    >
 
     <TextView
         android:id="@+id/locale_native"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:textColor="@color/language_picker_item_text_color"
+        android:textColor="@drawable/language_picker_item_text_color_selector"
         android:textSize="18sp"
         android:textAppearance="?android:attr/textAppearanceListItem"
         />
@@ -23,9 +30,20 @@
         android:id="@+id/locale_secondary"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:textColor="@color/language_picker_item_text_color_secondary"
+        android:textColor="@drawable/language_picker_item_text_color2_selector"
         android:textSize="16sp"
         android:textAppearance="?android:attr/textAppearanceListItem"
         />
+</LinearLayout>
 
-</LinearLayout>
\ No newline at end of file
+<ImageView
+    android:id="@+id/indicator"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_alignParentEnd="true"
+    android:adjustViewBounds="true"
+    android:layout_marginEnd="10dp"
+    android:layout_centerVertical="true"
+    android:src="@drawable/bilingual_language_item_selection_indicator"
+    android:visibility="gone" />
+</RelativeLayout>
\ No newline at end of file
diff --git a/core/res/res/values-night/colors.xml b/core/res/res/values-night/colors.xml
index 88171ce..d3f998f 100644
--- a/core/res/res/values-night/colors.xml
+++ b/core/res/res/values-night/colors.xml
@@ -41,6 +41,11 @@
     <!-- Lily Language Picker language item view colors -->
     <color name="language_picker_item_text_color">#F1F3F4</color>
     <color name="language_picker_item_text_color_secondary">#BDC1C6</color>
+    <color name="language_picker_item_text_color_selected">#202124</color>
+    <color name="language_picker_item_text_color_secondary_selected">#202124</color>
+    <color name="language_picker_item_selected_indicator">#202124</color>
+    <color name="language_picker_item_selected_bg">#92B3F2</color>
+    <color name="language_picker_item_selected_stroke">#185ABC</color>
 
     <!-- Color for side fps toast dark theme-->
     <color name="side_fps_toast_background">#2E3132</color>
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index 8b1b46d..77d7c43 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -451,6 +451,11 @@
     <!-- Lily Language Picker language item view colors -->
     <color name="language_picker_item_text_color">#202124</color>
     <color name="language_picker_item_text_color_secondary">#5F6368</color>
+    <color name="language_picker_item_text_color_selected">#202124</color>
+    <color name="language_picker_item_text_color_secondary_selected">#5F6368</color>
+    <color name="language_picker_item_selected_indicator">#4285F4</color>
+    <color name="language_picker_item_selected_bg">#E8F0FE</color>
+    <color name="language_picker_item_selected_stroke">#4C8DF6</color>
 
     <!-- Color for side fps toast light theme -->
     <color name="side_fps_toast_background">#F7F9FA</color>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 9890614..b59cf7f 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5872,4 +5872,10 @@
 
     <!-- The number of tasks to scan to get the visibility of Home -->
     <integer name="config_maxScanTasksForHomeVisibility">10</integer>
+
+    <!-- Device state that corresponds to rear display mode, feature provided
+         through Jetpack WindowManager
+         TODO(b/236022708) Move rear display state to device state config file
+    -->
+    <integer name="config_deviceStateRearDisplay">-1</integer>
 </resources>
diff --git a/core/res/res/values/locale_config.xml b/core/res/res/values/locale_config.xml
index e9b42d3..78ec145 100644
--- a/core/res/res/values/locale_config.xml
+++ b/core/res/res/values/locale_config.xml
@@ -23,7 +23,7 @@
         <item>ak-GH</item> <!-- Akan (Ghana) -->
         <item>am-ET</item> <!-- Amharic (Ethiopia) -->
         <item>ar-AE</item> <!-- Arabic (United Arab Emirates) -->
-        <item>ar-AE-u-nu-latn</item> <!-- Arabic (United Arab Emirates, Western Digits) -->
+        <item>ar-AE-u-nu-arab</item> <!-- Arabic (United Arab Emirates, Arabic Digits) -->
         <item>ar-BH</item> <!-- Arabic (Bahrain) -->
         <item>ar-BH-u-nu-latn</item> <!-- Arabic (Bahrain, Western Digits) -->
         <item>ar-DJ</item> <!-- Arabic (Djibouti) -->
@@ -190,6 +190,7 @@
         <item>en-MS</item> <!-- English (Montserrat) -->
         <item>en-MT</item> <!-- English (Malta) -->
         <item>en-MU</item> <!-- English (Mauritius) -->
+        <item>en-MV</item> <!-- English (Maldives) -->
         <item>en-MW</item> <!-- English (Malawi) -->
         <item>en-MY</item> <!-- English (Malaysia) -->
         <item>en-NA</item> <!-- English (Namibia) -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d3d0493..b7da6ae 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6321,6 +6321,8 @@
     <string name="vdm_camera_access_denied" product="default">Can’t access the phone’s camera from your <xliff:g id="device" example="Chromebook">%1$s</xliff:g></string>
     <!-- Error message indicating the camera cannot be accessed when running on a virtual device. [CHAR LIMIT=NONE] -->
     <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>
+    <!-- Error message indicating the user cannot access secure content when running on a virtual device. [CHAR LIMIT=NONE] -->
+    <string name="vdm_secure_window">This can’t be accessed while streaming. Try on your phone instead.</string>
 
     <!-- Title for preference of the system default locale. [CHAR LIMIT=50]-->
     <string name="system_locale_title">System default</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 00dab04..d60fc20 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -97,10 +97,12 @@
   <java-symbol type="id" name="icon" />
   <java-symbol type="id" name="image" />
   <java-symbol type="id" name="increment" />
+  <java-symbol type="id" name="indicator" />
   <java-symbol type="id" name="internalEmpty" />
   <java-symbol type="id" name="inputExtractAccessories" />
   <java-symbol type="id" name="inputExtractAction" />
   <java-symbol type="id" name="issued_on" />
+  <java-symbol type="id" name="language_item" />
   <java-symbol type="id" name="left_icon" />
   <java-symbol type="id" name="leftSpacer" />
   <java-symbol type="id" name="line1" />
@@ -3126,6 +3128,12 @@
   <java-symbol type="id" name="locale_search_menu" />
   <java-symbol type="layout" name="language_picker_item" />
   <java-symbol type="layout" name="language_picker_bilingual_item" />
+  <java-symbol type="color" name="language_picker_item_text_color" />
+  <java-symbol type="color" name="language_picker_item_text_color_selected" />
+  <java-symbol type="color" name="language_picker_item_text_color_secondary_selected" />
+  <java-symbol type="drawable" name="language_picker_item_text_color_selector" />
+  <java-symbol type="drawable" name="language_picker_item_text_color2_selector" />
+  <java-symbol type="drawable" name="language_picker_item_bg_selected" />
   <java-symbol type="layout" name="language_picker_section_header" />
   <java-symbol type="layout" name="language_picker_bilingual_section_header" />
   <java-symbol type="menu" name="language_selection_list" />
@@ -4795,6 +4803,7 @@
 
   <!-- For VirtualDeviceManager -->
   <java-symbol type="string" name="vdm_camera_access_denied" />
+  <java-symbol type="string" name="vdm_secure_window" />
 
   <java-symbol type="color" name="camera_privacy_light_day"/>
   <java-symbol type="color" name="camera_privacy_light_night"/>
@@ -4821,6 +4830,7 @@
   <java-symbol type="drawable" name="ic_swap_horiz" />
   <java-symbol type="array" name="config_deviceStatesAvailableForAppRequests" />
   <java-symbol type="array" name="config_serviceStateLocationAllowedPackages" />
+  <java-symbol type="integer" name="config_deviceStateRearDisplay"/>
 
   <!-- For app language picker -->
   <java-symbol type="string" name="system_locale_title" />
diff --git a/core/tests/coretests/src/android/app/NotificationChannelGroupTest.java b/core/tests/coretests/src/android/app/NotificationChannelGroupTest.java
new file mode 100644
index 0000000..2a3da05
--- /dev/null
+++ b/core/tests/coretests/src/android/app/NotificationChannelGroupTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static junit.framework.TestCase.assertEquals;
+
+import android.os.Parcel;
+import android.test.AndroidTestCase;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.base.Strings;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NotificationChannelGroupTest {
+    private final String CLASS = "android.app.NotificationChannelGroup";
+
+    @Test
+    public void testLongStringFields() {
+        NotificationChannelGroup group = new NotificationChannelGroup("my_group_01", "groupName");
+
+        try {
+            String longString = Strings.repeat("A", 65536);
+            Field mName = Class.forName(CLASS).getDeclaredField("mName");
+            mName.setAccessible(true);
+            mName.set(group, longString);
+            Field mId = Class.forName(CLASS).getDeclaredField("mId");
+            mId.setAccessible(true);
+            mId.set(group, longString);
+            Field mDescription = Class.forName(CLASS).getDeclaredField("mDescription");
+            mDescription.setAccessible(true);
+            mDescription.set(group, longString);
+        } catch (NoSuchFieldException e) {
+            e.printStackTrace();
+        } catch (ClassNotFoundException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+
+        Parcel parcel = Parcel.obtain();
+        group.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        NotificationChannelGroup fromParcel =
+                NotificationChannelGroup.CREATOR.createFromParcel(parcel);
+        assertEquals(NotificationChannelGroup.MAX_TEXT_LENGTH, fromParcel.getId().length());
+        assertEquals(NotificationChannelGroup.MAX_TEXT_LENGTH, fromParcel.getName().length());
+        assertEquals(NotificationChannelGroup.MAX_TEXT_LENGTH,
+                fromParcel.getDescription().length());
+    }
+}
diff --git a/core/tests/coretests/src/android/app/NotificationChannelTest.java b/core/tests/coretests/src/android/app/NotificationChannelTest.java
new file mode 100644
index 0000000..647bfe8
--- /dev/null
+++ b/core/tests/coretests/src/android/app/NotificationChannelTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.app;
+
+import static junit.framework.TestCase.assertEquals;
+
+import android.net.Uri;
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.base.Strings;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NotificationChannelTest {
+    private final String CLASS = "android.app.NotificationChannel";
+
+    @Test
+    public void testLongStringFields() {
+        NotificationChannel channel = new NotificationChannel("id", "name", 3);
+
+        try {
+            String longString = Strings.repeat("A", 65536);
+            Field mName = Class.forName(CLASS).getDeclaredField("mName");
+            mName.setAccessible(true);
+            mName.set(channel, longString);
+            Field mId = Class.forName(CLASS).getDeclaredField("mId");
+            mId.setAccessible(true);
+            mId.set(channel, longString);
+            Field mDesc = Class.forName(CLASS).getDeclaredField("mDesc");
+            mDesc.setAccessible(true);
+            mDesc.set(channel, longString);
+            Field mParentId = Class.forName(CLASS).getDeclaredField("mParentId");
+            mParentId.setAccessible(true);
+            mParentId.set(channel, longString);
+            Field mGroup = Class.forName(CLASS).getDeclaredField("mGroup");
+            mGroup.setAccessible(true);
+            mGroup.set(channel, longString);
+            Field mConversationId = Class.forName(CLASS).getDeclaredField("mConversationId");
+            mConversationId.setAccessible(true);
+            mConversationId.set(channel, longString);
+        } catch (NoSuchFieldException e) {
+            e.printStackTrace();
+        } catch (ClassNotFoundException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+
+        Parcel parcel = Parcel.obtain();
+        channel.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        NotificationChannel fromParcel = NotificationChannel.CREATOR.createFromParcel(parcel);
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH, fromParcel.getId().length());
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH, fromParcel.getName().length());
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH,
+                fromParcel.getDescription().length());
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH,
+                fromParcel.getParentChannelId().length());
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH,
+                fromParcel.getGroup().length());
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH,
+                fromParcel.getConversationId().length());
+    }
+
+    @Test
+    public void testLongAlertFields() {
+        NotificationChannel channel = new NotificationChannel("id", "name", 3);
+
+        channel.setSound(Uri.parse("content://" + Strings.repeat("A",65536)),
+                Notification.AUDIO_ATTRIBUTES_DEFAULT);
+        channel.setVibrationPattern(new long[65550/2]);
+
+        Parcel parcel = Parcel.obtain();
+        channel.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        NotificationChannel fromParcel = NotificationChannel.CREATOR.createFromParcel(parcel);
+        assertEquals(NotificationChannel.MAX_VIBRATION_LENGTH,
+                fromParcel.getVibrationPattern().length);
+        assertEquals(NotificationChannel.MAX_TEXT_LENGTH,
+                fromParcel.getSound().toString().length());
+    }
+}
diff --git a/core/tests/coretests/src/android/hardware/input/OWNERS b/core/tests/coretests/src/android/hardware/input/OWNERS
new file mode 100644
index 0000000..3f8a602
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/input/OWNERS
@@ -0,0 +1,2 @@
+include /core/java/android/hardware/input/OWNERS
+
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index ed6a649..cc68fce 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -236,6 +236,21 @@
     }
 
     @Test
+    public void testSystemDrivenInsetsAnimationLoggingListener_onReady() {
+        prepareControls();
+        // only the original thread that created view hierarchy can touch its views
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            WindowInsetsAnimationControlListener loggingListener =
+                    mock(WindowInsetsAnimationControlListener.class);
+            mController.setSystemDrivenInsetsAnimationLoggingListener(loggingListener);
+            mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(true);
+            // since there is no focused view, forcefully make IME visible.
+            mController.show(Type.ime(), true /* fromIme */);
+            verify(loggingListener).onReady(notNull(), anyInt());
+        });
+    }
+
+    @Test
     public void testAnimationEndState() {
         InsetsSourceControl[] controls = prepareControls();
         InsetsSourceControl navBar = controls[0];
@@ -914,6 +929,23 @@
         });
     }
 
+    @Test
+    public void testImeRequestedVisibleWhenImeNotControllable() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            // Simulate IME insets is not controllable
+            mController.onControlsChanged(new InsetsSourceControl[0]);
+            final InsetsSourceConsumer imeInsetsConsumer = mController.getSourceConsumer(ITYPE_IME);
+            assertNull(imeInsetsConsumer.getControl());
+
+            // Verify IME requested visibility should be updated to IME consumer from controller.
+            mController.show(ime());
+            assertTrue(imeInsetsConsumer.isRequestedVisible());
+
+            mController.hide(ime());
+            assertFalse(imeInsetsConsumer.isRequestedVisible());
+        });
+    }
+
     private void waitUntilNextFrame() throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
         Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT,
diff --git a/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java b/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java
index 03c8b1b..690b3587 100644
--- a/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/PendingInsetsControllerTest.java
@@ -213,6 +213,25 @@
     }
 
     @Test
+    public void testSystemDrivenInsetsAnimationLoggingListener() {
+        WindowInsetsAnimationControlListener listener =
+                mock(WindowInsetsAnimationControlListener.class);
+        mPendingInsetsController.setSystemDrivenInsetsAnimationLoggingListener(listener);
+        mPendingInsetsController.replayAndAttach(mReplayedController);
+        verify(mReplayedController).setSystemDrivenInsetsAnimationLoggingListener(eq(listener));
+    }
+
+    @Test
+    public void testSystemDrivenInsetsAnimationLoggingListener_direct() {
+        mPendingInsetsController.replayAndAttach(mReplayedController);
+        WindowInsetsAnimationControlListener listener =
+                mock(WindowInsetsAnimationControlListener.class);
+        mPendingInsetsController.setSystemDrivenInsetsAnimationLoggingListener(listener);
+        verify(mReplayedController).setSystemDrivenInsetsAnimationLoggingListener(
+                eq(listener));
+    }
+
+    @Test
     public void testDetachReattach() {
         mPendingInsetsController.show(systemBars());
         mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
diff --git a/graphics/java/android/graphics/ImageFormat.java b/graphics/java/android/graphics/ImageFormat.java
index b341a4e..c93b733 100644
--- a/graphics/java/android/graphics/ImageFormat.java
+++ b/graphics/java/android/graphics/ImageFormat.java
@@ -26,10 +26,21 @@
      @Retention(RetentionPolicy.SOURCE)
      @IntDef(value = {
              UNKNOWN,
+             /**
+              * Since some APIs accept either ImageFormat or PixelFormat (and the two
+              * enums do not overlap since they're both partial versions of the
+              * internal format enum), add PixelFormat values here so linting
+              * tools won't complain when method arguments annotated with
+              * ImageFormat are provided with PixelFormat values.
+              */
+             PixelFormat.RGBA_8888,
+             PixelFormat.RGBX_8888,
+             PixelFormat.RGB_888,
              RGB_565,
              YV12,
              Y8,
              Y16,
+             YCBCR_P010,
              NV16,
              NV21,
              YUY2,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index bdf703c..7e9c418 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -20,11 +20,14 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
+import androidx.window.extensions.area.WindowAreaComponent;
+import androidx.window.extensions.area.WindowAreaComponentImpl;
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
 import androidx.window.extensions.embedding.SplitController;
 import androidx.window.extensions.layout.WindowLayoutComponent;
 import androidx.window.extensions.layout.WindowLayoutComponentImpl;
 
+
 /**
  * The reference implementation of {@link WindowExtensions} that implements the initial API version.
  */
@@ -33,10 +36,12 @@
     private final Object mLock = new Object();
     private volatile WindowLayoutComponent mWindowLayoutComponent;
     private volatile SplitController mSplitController;
+    private volatile WindowAreaComponent mWindowAreaComponent;
 
+    // TODO(b/241126279) Introduce constants to better version functionality
     @Override
     public int getVendorApiLevel() {
-        return 1;
+        return 2;
     }
 
     /**
@@ -75,4 +80,23 @@
         }
         return mSplitController;
     }
+
+    /**
+     * Returns a reference implementation of {@link WindowAreaComponent} if available,
+     * {@code null} otherwise. The implementation must match the API level reported in
+     * {@link WindowExtensions#getWindowAreaComponent()}.
+     * @return {@link WindowAreaComponent} OEM implementation.
+     */
+    public WindowAreaComponent getWindowAreaComponent() {
+        if (mWindowAreaComponent == null) {
+            synchronized (mLock) {
+                if (mWindowAreaComponent == null) {
+                    Context context = ActivityThread.currentApplication();
+                    mWindowAreaComponent =
+                            new WindowAreaComponentImpl(context);
+                }
+            }
+        }
+        return mWindowAreaComponent;
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
new file mode 100644
index 0000000..3adae70
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -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 androidx.window.extensions.area;
+
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.devicestate.DeviceStateManager;
+import android.hardware.devicestate.DeviceStateRequest;
+import android.util.ArraySet;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * Reference implementation of androidx.window.extensions.area OEM interface for use with
+ * WindowManager Jetpack.
+ *
+ * This component currently supports Rear Display mode with the ability to add and remove
+ * status listeners for this mode.
+ *
+ * The public methods in this class are thread-safe.
+ **/
+public class WindowAreaComponentImpl implements WindowAreaComponent,
+        DeviceStateManager.DeviceStateCallback {
+
+    private final Object mLock = new Object();
+
+    private final DeviceStateManager mDeviceStateManager;
+    private final Executor mExecutor;
+
+    @GuardedBy("mLock")
+    private final ArraySet<Consumer<Integer>> mRearDisplayStatusListeners = new ArraySet<>();
+    private final int mRearDisplayState;
+    @WindowAreaSessionState
+    private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
+
+    @GuardedBy("mLock")
+    private int mCurrentDeviceState = INVALID_DEVICE_STATE;
+    @GuardedBy("mLock")
+    private int mCurrentDeviceBaseState = INVALID_DEVICE_STATE;
+    @GuardedBy("mLock")
+    private DeviceStateRequest mDeviceStateRequest;
+
+    public WindowAreaComponentImpl(@NonNull Context context) {
+        mDeviceStateManager = context.getSystemService(DeviceStateManager.class);
+        mExecutor = context.getMainExecutor();
+
+        // TODO(b/236022708) Move rear display state to device state config file
+        mRearDisplayState = context.getResources().getInteger(
+                R.integer.config_deviceStateRearDisplay);
+
+        mDeviceStateManager.registerCallback(mExecutor, this);
+    }
+
+    /**
+     * Adds a listener interested in receiving updates on the RearDisplayStatus
+     * of the device. Because this is being called from the OEM provided
+     * extensions, we will post the result of the listener on the executor
+     * provided by the developer at the initial call site.
+     *
+     * Depending on the initial state of the device, we will return either
+     * {@link WindowAreaComponent#STATUS_AVAILABLE} or
+     * {@link WindowAreaComponent#STATUS_UNAVAILABLE} if the feature is supported or not in that
+     * state respectively. When the rear display feature is triggered, we update the status to be
+     * {@link WindowAreaComponent#STATUS_UNAVAILABLE}. TODO(b/240727590) Prefix with AREA_
+     *
+     * TODO(b/239833099) Add a STATUS_ACTIVE option to let apps know if a feature is currently
+     * enabled.
+     *
+     * @param consumer {@link Consumer} interested in receiving updates to the status of
+     * rear display mode.
+     */
+    public void addRearDisplayStatusListener(
+            @NonNull Consumer<@WindowAreaStatus Integer> consumer) {
+        synchronized (mLock) {
+            mRearDisplayStatusListeners.add(consumer);
+
+            // If current device state is still invalid, we haven't gotten our initial value yet
+            if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
+                return;
+            }
+            consumer.accept(getCurrentStatus());
+        }
+    }
+
+    /**
+     * Removes a listener no longer interested in receiving updates.
+     * @param consumer no longer interested in receiving updates to RearDisplayStatus
+     */
+    public void removeRearDisplayStatusListener(
+            @NonNull Consumer<@WindowAreaStatus Integer> consumer) {
+        synchronized (mLock) {
+            mRearDisplayStatusListeners.remove(consumer);
+        }
+    }
+
+    /**
+     * Creates and starts a rear display session and provides updates to the
+     * callback provided. Because this is being called from the OEM provided
+     * extensions, we will post the result of the listener on the executor
+     * provided by the developer at the initial call site.
+     *
+     * When we enable rear display mode, we submit a request to {@link DeviceStateManager}
+     * to override the device state to the state that corresponds to RearDisplay
+     * mode. When the {@link DeviceStateRequest} is activated, we let the
+     * consumer know that the session is active by sending
+     * {@link WindowAreaComponent#SESSION_STATE_ACTIVE}.
+     *
+     * @param activity to provide updates to the client on
+     * the status of the Session
+     * @param rearDisplaySessionCallback to provide updates to the client on
+     * the status of the Session
+     */
+    public void startRearDisplaySession(@NonNull Activity activity,
+            @NonNull Consumer<@WindowAreaSessionState Integer> rearDisplaySessionCallback) {
+        synchronized (mLock) {
+            if (mDeviceStateRequest != null) {
+                // Rear display session is already active
+                throw new IllegalStateException(
+                        "Unable to start new rear display session as one is already active");
+            }
+            mDeviceStateRequest = DeviceStateRequest.newBuilder(mRearDisplayState).build();
+            mDeviceStateManager.requestState(
+                    mDeviceStateRequest,
+                    mExecutor,
+                    new DeviceStateRequestCallbackAdapter(rearDisplaySessionCallback)
+            );
+        }
+    }
+
+    /**
+     * Ends the current rear display session and provides updates to the
+     * callback provided. Because this is being called from the OEM provided
+     * extensions, we will post the result of the listener on the executor
+     * provided by the developer.
+     */
+    public void endRearDisplaySession() {
+        synchronized (mLock) {
+            if (mDeviceStateRequest != null || isRearDisplayActive()) {
+                mDeviceStateRequest = null;
+                mDeviceStateManager.cancelStateRequest();
+            } else {
+                throw new IllegalStateException(
+                        "Unable to cancel a rear display session as there is no active session");
+            }
+        }
+    }
+
+    @Override
+    public void onBaseStateChanged(int state) {
+        synchronized (mLock) {
+            mCurrentDeviceBaseState = state;
+            if (state == mCurrentDeviceState) {
+                updateStatusConsumers(getCurrentStatus());
+            }
+        }
+    }
+
+    @Override
+    public void onStateChanged(int state) {
+        synchronized (mLock) {
+            mCurrentDeviceState = state;
+            updateStatusConsumers(getCurrentStatus());
+        }
+    }
+
+    @GuardedBy("mLock")
+    private int getCurrentStatus() {
+        if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE
+                || isRearDisplayActive()) {
+            return WindowAreaComponent.STATUS_UNAVAILABLE;
+        }
+        return WindowAreaComponent.STATUS_AVAILABLE;
+    }
+
+    /**
+     * Helper method to determine if a rear display session is currently active by checking
+     * if the current device configuration matches that of rear display. This would be true
+     * if there is a device override currently active (base state != current state) and the current
+     * state is that which corresponds to {@code mRearDisplayState}
+     * @return {@code true} if the device is in rear display mode and {@code false} if not
+     */
+    @GuardedBy("mLock")
+    private boolean isRearDisplayActive() {
+        return (mCurrentDeviceState != mCurrentDeviceBaseState) && (mCurrentDeviceState
+                == mRearDisplayState);
+    }
+
+    @GuardedBy("mLock")
+    private void updateStatusConsumers(@WindowAreaStatus int windowAreaStatus) {
+        synchronized (mLock) {
+            for (int i = 0; i < mRearDisplayStatusListeners.size(); i++) {
+                mRearDisplayStatusListeners.valueAt(i).accept(windowAreaStatus);
+            }
+        }
+    }
+
+    /**
+     * Callback for the {@link DeviceStateRequest} to be notified of when the request has been
+     * activated or cancelled. This callback provides information to the client library
+     * on the status of the RearDisplay session through {@code mRearDisplaySessionCallback}
+     */
+    private class DeviceStateRequestCallbackAdapter implements DeviceStateRequest.Callback {
+
+        private final Consumer<Integer> mRearDisplaySessionCallback;
+
+        DeviceStateRequestCallbackAdapter(@NonNull Consumer<Integer> callback) {
+            mRearDisplaySessionCallback = callback;
+        }
+
+        @Override
+        public void onRequestActivated(@NonNull DeviceStateRequest request) {
+            synchronized (mLock) {
+                if (request.equals(mDeviceStateRequest)) {
+                    mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_ACTIVE;
+                    mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
+                    updateStatusConsumers(getCurrentStatus());
+                }
+            }
+        }
+
+        @Override
+        public void onRequestCanceled(DeviceStateRequest request) {
+            synchronized (mLock) {
+                if (request.equals(mDeviceStateRequest)) {
+                    mDeviceStateRequest = null;
+                }
+                mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
+                mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
+                updateStatusConsumers(getCurrentStatus());
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index d42fca2..1335e5e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -51,28 +51,37 @@
     @VisibleForTesting
     final Map<IBinder, TaskFragmentInfo> mFragmentInfos = new ArrayMap<>();
 
+    @NonNull
     private final TaskFragmentCallback mCallback;
+
     @VisibleForTesting
+    @Nullable
     TaskFragmentAnimationController mAnimationController;
 
     /**
      * Callback that notifies the controller about changes to task fragments.
      */
     interface TaskFragmentCallback {
-        void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo);
-        void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo);
-        void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo);
-        void onTaskFragmentParentInfoChanged(int taskId, @NonNull Configuration parentConfig);
-        void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
-                @NonNull IBinder activityToken);
-        void onTaskFragmentError(@Nullable TaskFragmentInfo taskFragmentInfo, int opType);
+        void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
+                @NonNull TaskFragmentInfo taskFragmentInfo);
+        void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
+                @NonNull TaskFragmentInfo taskFragmentInfo);
+        void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
+                @NonNull TaskFragmentInfo taskFragmentInfo);
+        void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct,
+                int taskId, @NonNull Configuration parentConfig);
+        void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
+                int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken);
+        void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
+                @Nullable TaskFragmentInfo taskFragmentInfo, int opType);
     }
 
     /**
      * @param executor  callbacks from WM Core are posted on this executor. It should be tied to the
      *                  UI thread that all other calls into methods of this class are also on.
      */
-    JetpackTaskFragmentOrganizer(@NonNull Executor executor, TaskFragmentCallback callback) {
+    JetpackTaskFragmentOrganizer(@NonNull Executor executor,
+            @NonNull TaskFragmentCallback callback) {
         super(executor);
         mCallback = callback;
     }
@@ -147,41 +156,31 @@
      * @param wct WindowContainerTransaction in which the task fragment should be resized.
      * @param fragmentToken token of an existing TaskFragment.
      */
-    void expandTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) {
+    void expandTaskFragment(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken) {
         resizeTaskFragment(wct, fragmentToken, new Rect());
         setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */);
         updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
     }
 
     /**
-     * Expands an existing TaskFragment to fill parent.
-     * @param fragmentToken token of an existing TaskFragment.
-     */
-    void expandTaskFragment(IBinder fragmentToken) {
-        WindowContainerTransaction wct = new WindowContainerTransaction();
-        expandTaskFragment(wct, fragmentToken);
-        applyTransaction(wct);
-    }
-
-    /**
      * Expands an Activity to fill parent by moving it to a new TaskFragment.
      * @param fragmentToken token to create new TaskFragment with.
      * @param activity      activity to move to the fill-parent TaskFragment.
      */
-    void expandActivity(IBinder fragmentToken, Activity activity) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
+    void expandActivity(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
+            @NonNull Activity activity) {
         createTaskFragmentAndReparentActivity(
                 wct, fragmentToken, activity.getActivityToken(), new Rect(),
                 WINDOWING_MODE_UNDEFINED, activity);
-        applyTransaction(wct);
     }
 
     /**
      * @param ownerToken The token of the activity that creates this task fragment. It does not
      *                   have to be a child of this task fragment, but must belong to the same task.
      */
-    void createTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken,
-            IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
+    void createTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
+            @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
         final TaskFragmentCreationParams fragmentOptions =
                 createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode);
         wct.createTaskFragment(fragmentOptions);
@@ -191,9 +190,9 @@
      * @param ownerToken The token of the activity that creates this task fragment. It does not
      *                   have to be a child of this task fragment, but must belong to the same task.
      */
-    private void createTaskFragmentAndReparentActivity(
-            WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken,
-            @NonNull Rect bounds, @WindowingMode int windowingMode, Activity activity) {
+    private void createTaskFragmentAndReparentActivity(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds,
+            @WindowingMode int windowingMode, @NonNull Activity activity) {
         createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
         wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken());
     }
@@ -202,9 +201,9 @@
      * @param ownerToken The token of the activity that creates this task fragment. It does not
      *                   have to be a child of this task fragment, but must belong to the same task.
      */
-    private void createTaskFragmentAndStartActivity(
-            WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken,
-            @NonNull Rect bounds, @WindowingMode int windowingMode, Intent activityIntent,
+    private void createTaskFragmentAndStartActivity(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect bounds,
+            @WindowingMode int windowingMode, @NonNull Intent activityIntent,
             @Nullable Bundle activityOptions) {
         createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
         wct.startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent, activityOptions);
@@ -225,8 +224,8 @@
         wct.setAdjacentTaskFragments(primary, secondary, adjacentParams);
     }
 
-    TaskFragmentCreationParams createFragmentOptions(IBinder fragmentToken, IBinder ownerToken,
-            Rect bounds, @WindowingMode int windowingMode) {
+    TaskFragmentCreationParams createFragmentOptions(@NonNull IBinder fragmentToken,
+            @NonNull IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
         if (mFragmentInfos.containsKey(fragmentToken)) {
             throw new IllegalArgumentException(
                     "There is an existing TaskFragment with fragmentToken=" + fragmentToken);
@@ -241,7 +240,7 @@
                 .build();
     }
 
-    void resizeTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken,
+    void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
             @Nullable Rect bounds) {
         if (!mFragmentInfos.containsKey(fragmentToken)) {
             throw new IllegalArgumentException(
@@ -253,8 +252,8 @@
         wct.setBounds(mFragmentInfos.get(fragmentToken).getToken(), bounds);
     }
 
-    void updateWindowingMode(WindowContainerTransaction wct, IBinder fragmentToken,
-            @WindowingMode int windowingMode) {
+    void updateWindowingMode(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken, @WindowingMode int windowingMode) {
         if (!mFragmentInfos.containsKey(fragmentToken)) {
             throw new IllegalArgumentException(
                     "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
@@ -262,7 +261,8 @@
         wct.setWindowingMode(mFragmentInfos.get(fragmentToken).getToken(), windowingMode);
     }
 
-    void deleteTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken) {
+    void deleteTaskFragment(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken) {
         if (!mFragmentInfos.containsKey(fragmentToken)) {
             throw new IllegalArgumentException(
                     "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken);
@@ -271,60 +271,49 @@
     }
 
     @Override
-    public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {
+    public void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
         mFragmentInfos.put(fragmentToken, taskFragmentInfo);
-
-        if (mCallback != null) {
-            mCallback.onTaskFragmentAppeared(taskFragmentInfo);
-        }
+        mCallback.onTaskFragmentAppeared(wct, taskFragmentInfo);
     }
 
     @Override
-    public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
+    public void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
         mFragmentInfos.put(fragmentToken, taskFragmentInfo);
-
-        if (mCallback != null) {
-            mCallback.onTaskFragmentInfoChanged(taskFragmentInfo);
-        }
+        mCallback.onTaskFragmentInfoChanged(wct, taskFragmentInfo);
     }
 
     @Override
-    public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
+    public void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         mFragmentInfos.remove(taskFragmentInfo.getFragmentToken());
-
-        if (mCallback != null) {
-            mCallback.onTaskFragmentVanished(taskFragmentInfo);
-        }
+        mCallback.onTaskFragmentVanished(wct, taskFragmentInfo);
     }
 
     @Override
-    public void onTaskFragmentParentInfoChanged(int taskId, @NonNull Configuration parentConfig) {
-        if (mCallback != null) {
-            mCallback.onTaskFragmentParentInfoChanged(taskId, parentConfig);
-        }
+    public void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct,
+            int taskId, @NonNull Configuration parentConfig) {
+        mCallback.onTaskFragmentParentInfoChanged(wct, taskId, parentConfig);
     }
 
     @Override
-    public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
-            @NonNull IBinder activityToken) {
-        if (mCallback != null) {
-            mCallback.onActivityReparentToTask(taskId, activityIntent, activityToken);
-        }
+    public void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
+            int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken) {
+        mCallback.onActivityReparentedToTask(wct, taskId, activityIntent, activityToken);
     }
 
     @Override
-    public void onTaskFragmentError(@NonNull IBinder errorCallbackToken,
+    public void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder errorCallbackToken,
             @Nullable TaskFragmentInfo taskFragmentInfo,
             int opType, @NonNull Throwable exception) {
         if (taskFragmentInfo != null) {
             final IBinder fragmentToken = taskFragmentInfo.getFragmentToken();
             mFragmentInfos.put(fragmentToken, taskFragmentInfo);
         }
-
-        if (mCallback != null) {
-            mCallback.onTaskFragmentError(taskFragmentInfo, opType);
-        }
+        mCallback.onTaskFragmentError(wct, taskFragmentInfo, opType);
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index f09a910..c8ac0fc 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -16,17 +16,21 @@
 
 package androidx.window.extensions.embedding;
 
-import android.annotation.NonNull;
 import android.app.Activity;
 import android.util.Pair;
 import android.util.Size;
 
+import androidx.annotation.NonNull;
+
 /**
  * Client-side descriptor of a split that holds two containers.
  */
 class SplitContainer {
+    @NonNull
     private final TaskFragmentContainer mPrimaryContainer;
+    @NonNull
     private final TaskFragmentContainer mSecondaryContainer;
+    @NonNull
     private final SplitRule mSplitRule;
 
     SplitContainer(@NonNull TaskFragmentContainer primaryContainer,
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 dad0739..0597809f 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -145,35 +145,36 @@
     }
 
     @Override
-    public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {
+    public void onTaskFragmentAppeared(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         synchronized (mLock) {
             TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
             if (container == null) {
                 return;
             }
 
-            container.setInfo(taskFragmentInfo);
+            container.setInfo(wct, taskFragmentInfo);
             if (container.isFinished()) {
-                mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
+                mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
             } else {
                 // Update with the latest Task configuration.
-                mPresenter.updateContainer(container);
+                updateContainer(wct, container);
             }
             updateCallbackIfNecessary();
         }
     }
 
     @Override
-    public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
+    public void onTaskFragmentInfoChanged(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         synchronized (mLock) {
             TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
             if (container == null) {
                 return;
             }
 
-            final WindowContainerTransaction wct = new WindowContainerTransaction();
             final boolean wasInPip = isInPictureInPicture(container);
-            container.setInfo(taskFragmentInfo);
+            container.setInfo(wct, taskFragmentInfo);
             final boolean isInPip = isInPictureInPicture(container);
             // Check if there are no running activities - consider the container empty if there are
             // no non-finishing activities left.
@@ -183,15 +184,15 @@
                     // Instead, the original split should be cleanup, and the dependent may be
                     // expanded to fullscreen.
                     cleanupForEnterPip(wct, container);
-                    mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
+                    mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
                 } else if (taskFragmentInfo.isTaskClearedForReuse()) {
                     // Do not finish the dependents if this TaskFragment was cleared due to
                     // launching activity in the Task.
-                    mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
+                    mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
                 } else if (!container.isWaitingActivityAppear()) {
                     // Do not finish the container before the expected activity appear until
                     // timeout.
-                    mPresenter.cleanupContainer(container, true /* shouldFinishDependent */, wct);
+                    mPresenter.cleanupContainer(wct, container, true /* shouldFinishDependent */);
                 }
             } else if (wasInPip && isInPip) {
                 // No update until exit PIP.
@@ -208,13 +209,13 @@
                 // needed.
                 updateContainer(wct, container);
             }
-            mPresenter.applyTransaction(wct);
             updateCallbackIfNecessary();
         }
     }
 
     @Override
-    public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
+    public void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentInfo taskFragmentInfo) {
         synchronized (mLock) {
             final TaskFragmentContainer container = getContainer(
                     taskFragmentInfo.getFragmentToken());
@@ -225,9 +226,7 @@
                 final TaskFragmentContainer newTopContainer = getTopActiveContainer(
                         container.getTaskId());
                 if (newTopContainer != null) {
-                    final WindowContainerTransaction wct = new WindowContainerTransaction();
                     updateContainer(wct, newTopContainer);
-                    mPresenter.applyTransaction(wct);
                 }
                 updateCallbackIfNecessary();
             }
@@ -236,7 +235,8 @@
     }
 
     @Override
-    public void onTaskFragmentParentInfoChanged(int taskId, @NonNull Configuration parentConfig) {
+    public void onTaskFragmentParentInfoChanged(@NonNull WindowContainerTransaction wct,
+            int taskId, @NonNull Configuration parentConfig) {
         synchronized (mLock) {
             onTaskConfigurationChanged(taskId, parentConfig);
             if (isInPictureInPicture(parentConfig)) {
@@ -256,7 +256,7 @@
                 final TaskFragmentContainer container = containers.get(i);
                 // Wait until onTaskFragmentAppeared to update new container.
                 if (!container.isFinished() && !container.isWaitingActivityAppear()) {
-                    mPresenter.updateContainer(container);
+                    updateContainer(wct, container);
                 }
             }
             updateCallbackIfNecessary();
@@ -264,7 +264,8 @@
     }
 
     @Override
-    public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
+    public void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
+            int taskId, @NonNull Intent activityIntent,
             @NonNull IBinder activityToken) {
         synchronized (mLock) {
             // If the activity belongs to the current app process, we treat it as a new activity
@@ -275,10 +276,10 @@
                 // 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 /* isOnReparent */)) {
+                if (!resolveActivityToContainer(wct, activity, true /* isOnReparent */)) {
                     // When there is no embedding rule matched, try to place it in the top container
                     // like a normal launch.
-                    placeActivityInTopContainer(activity);
+                    placeActivityInTopContainer(wct, activity);
                 }
                 updateCallbackIfNecessary();
                 return;
@@ -293,7 +294,6 @@
             // 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) {
@@ -306,14 +306,14 @@
             }
             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().
         }
     }
 
     @Override
-    public void onTaskFragmentError(@Nullable TaskFragmentInfo taskFragmentInfo, int opType) {
+    public void onTaskFragmentError(@NonNull WindowContainerTransaction wct,
+            @Nullable TaskFragmentInfo taskFragmentInfo, int opType) {
         synchronized (mLock) {
             switch (opType) {
                 case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT:
@@ -329,10 +329,11 @@
                     }
 
                     // Update the latest taskFragmentInfo and perform necessary clean-up
-                    container.setInfo(taskFragmentInfo);
+                    container.setInfo(wct, taskFragmentInfo);
                     container.clearPendingAppearedActivities();
                     if (container.isEmpty()) {
-                        mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
+                        mPresenter.cleanupContainer(wct, container,
+                                false /* shouldFinishDependent */);
                     }
                     break;
                 }
@@ -343,7 +344,7 @@
         }
     }
 
-    /** Called on receiving {@link #onTaskFragmentVanished(TaskFragmentInfo)} for cleanup. */
+    /** Called on receiving {@link #onTaskFragmentVanished} for cleanup. */
     private void cleanupTaskFragment(@NonNull IBinder taskFragmentToken) {
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
             final TaskContainer taskContainer = mTaskContainers.valueAt(i);
@@ -422,10 +423,12 @@
     }
 
     @VisibleForTesting
-    void onActivityCreated(@NonNull Activity launchedActivity) {
+    @GuardedBy("mLock")
+    void onActivityCreated(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity 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 /* isOnReparent */);
+        resolveActivityToContainer(wct, launchedActivity, false /* isOnReparent */);
         updateCallbackIfNecessary();
     }
 
@@ -440,7 +443,8 @@
      */
     @VisibleForTesting
     @GuardedBy("mLock")
-    boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) {
+    boolean resolveActivityToContainer(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity activity, boolean isOnReparent) {
         if (isInPictureInPicture(activity) || activity.isFinishing()) {
             // We don't embed activity when it is in PIP, or finishing. Return true since we don't
             // want any extra handling.
@@ -472,12 +476,12 @@
 
         // 1. Whether the new launched activity should always expand.
         if (shouldExpand(activity, null /* intent */)) {
-            expandActivity(activity);
+            expandActivity(wct, activity);
             return true;
         }
 
         // 2. Whether the new launched activity should launch a placeholder.
-        if (launchPlaceholderIfNecessary(activity, !isOnReparent)) {
+        if (launchPlaceholderIfNecessary(wct, activity, !isOnReparent)) {
             return true;
         }
 
@@ -492,11 +496,11 @@
             // Can't find any activity below.
             return false;
         }
-        if (putActivitiesIntoSplitIfNecessary(activityBelow, activity)) {
+        if (putActivitiesIntoSplitIfNecessary(wct, activityBelow, activity)) {
             // Have split rule of [ activityBelow | launchedActivity ].
             return true;
         }
-        if (isOnReparent && putActivitiesIntoSplitIfNecessary(activity, activityBelow)) {
+        if (isOnReparent && putActivitiesIntoSplitIfNecessary(wct, activity, activityBelow)) {
             // Have split rule of [ launchedActivity | activityBelow].
             return true;
         }
@@ -519,19 +523,20 @@
             // Can't find the top activity on the other split TaskFragment.
             return false;
         }
-        if (putActivitiesIntoSplitIfNecessary(otherTopActivity, activity)) {
+        if (putActivitiesIntoSplitIfNecessary(wct, otherTopActivity, activity)) {
             // Have split rule of [ otherTopActivity | launchedActivity ].
             return true;
         }
         // Have split rule of [ launchedActivity | otherTopActivity].
-        return isOnReparent && putActivitiesIntoSplitIfNecessary(activity, otherTopActivity);
+        return isOnReparent && putActivitiesIntoSplitIfNecessary(wct, activity, otherTopActivity);
     }
 
     /**
      * Places the given activity to the top most TaskFragment in the task if there is any.
      */
     @VisibleForTesting
-    void placeActivityInTopContainer(@NonNull Activity activity) {
+    void placeActivityInTopContainer(@NonNull WindowContainerTransaction wct,
+            @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.
@@ -547,20 +552,20 @@
             return;
         }
         targetContainer.addPendingAppearedActivity(activity);
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
         wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(),
                 activity.getActivityToken());
-        mPresenter.applyTransaction(wct);
     }
 
     /**
      * Starts an activity to side of the launchingActivity with the provided split config.
      */
-    private void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent,
+    @GuardedBy("mLock")
+    private void startActivityToSide(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity launchingActivity, @NonNull Intent intent,
             @Nullable Bundle options, @NonNull SplitRule sideRule,
             @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) {
         try {
-            mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule,
+            mPresenter.startActivityToSide(wct, launchingActivity, intent, options, sideRule,
                     isPlaceholder);
         } catch (Exception e) {
             if (failureCallback != null) {
@@ -573,15 +578,17 @@
      * 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) {
+    @GuardedBy("mLock")
+    private void expandActivity(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity activity) {
         final TaskFragmentContainer container = getContainerWithActivity(activity);
         if (shouldContainerBeExpanded(container)) {
             // Make sure that the existing container is expanded.
-            mPresenter.expandTaskFragment(container.getTaskFragmentToken());
+            mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
         } else {
             // Put activity into a new expanded container.
             final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity));
-            mPresenter.expandActivity(newContainer.getTaskFragmentToken(), activity);
+            mPresenter.expandActivity(wct, newContainer.getTaskFragmentToken(), activity);
         }
     }
 
@@ -667,8 +674,8 @@
      * and returns {@code true}. Otherwise, returns {@code false}.
      */
     @GuardedBy("mLock")
-    private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity,
-            @NonNull Activity secondaryActivity) {
+    private boolean putActivitiesIntoSplitIfNecessary(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) {
         final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity);
         if (splitRule == null) {
             return false;
@@ -686,23 +693,23 @@
                 return true;
             }
             secondaryContainer.addPendingAppearedActivity(secondaryActivity);
-            final WindowContainerTransaction wct = new WindowContainerTransaction();
             if (mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity,
                     secondaryActivity, null /* secondaryIntent */)
                     != RESULT_EXPAND_FAILED_NO_TF_INFO) {
                 wct.reparentActivityToTaskFragment(
                         secondaryContainer.getTaskFragmentToken(),
                         secondaryActivity.getActivityToken());
-                mPresenter.applyTransaction(wct);
                 return true;
             }
         }
         // Create new split pair.
-        mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule);
+        mPresenter.createNewSplitContainer(wct, primaryActivity, secondaryActivity, splitRule);
         return true;
     }
 
-    private void onActivityConfigurationChanged(@NonNull Activity activity) {
+    @GuardedBy("mLock")
+    private void onActivityConfigurationChanged(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity activity) {
         if (activity.isFinishing()) {
             // Do nothing if the activity is currently finishing.
             return;
@@ -721,7 +728,7 @@
         }
 
         // Check if activity requires a placeholder
-        launchPlaceholderIfNecessary(activity, false /* isOnCreated */);
+        launchPlaceholderIfNecessary(wct, activity, false /* isOnCreated */);
     }
 
     @VisibleForTesting
@@ -741,7 +748,22 @@
      * creation.
      */
     void onTaskFragmentAppearEmptyTimeout(@NonNull TaskFragmentContainer container) {
-        mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
+        synchronized (mLock) {
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            onTaskFragmentAppearEmptyTimeout(wct, container);
+            mPresenter.applyTransaction(wct);
+        }
+    }
+
+    /**
+     * Called when we have been waiting too long for the TaskFragment to become non-empty after
+     * creation.
+     */
+    void onTaskFragmentAppearEmptyTimeout(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer container) {
+        synchronized (mLock) {
+            mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
+        }
     }
 
     /**
@@ -971,6 +993,7 @@
     }
 
     /** Cleanups all the dependencies when the TaskFragment is entering PIP. */
+    @GuardedBy("mLock")
     private void cleanupForEnterPip(@NonNull WindowContainerTransaction wct,
             @NonNull TaskFragmentContainer container) {
         final TaskContainer taskContainer = container.getTaskContainer();
@@ -1084,9 +1107,10 @@
      * Updates the presentation of the container. If the container is part of the split or should
      * have a placeholder, it will also update the other part of the split.
      */
+    @GuardedBy("mLock")
     void updateContainer(@NonNull WindowContainerTransaction wct,
             @NonNull TaskFragmentContainer container) {
-        if (launchPlaceholderIfNecessary(container)) {
+        if (launchPlaceholderIfNecessary(wct, container)) {
             // Placeholder was launched, the positions will be updated when the activity is added
             // to the secondary container.
             return;
@@ -1111,7 +1135,7 @@
             // Skip position update - one or both containers are finished.
             return;
         }
-        if (dismissPlaceholderIfNecessary(splitContainer)) {
+        if (dismissPlaceholderIfNecessary(wct, splitContainer)) {
             // Placeholder was finished, the positions will be updated when its container is emptied
             return;
         }
@@ -1173,16 +1197,20 @@
     /**
      * Checks if the container requires a placeholder and launches it if necessary.
      */
-    private boolean launchPlaceholderIfNecessary(@NonNull TaskFragmentContainer container) {
+    @GuardedBy("mLock")
+    private boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer container) {
         final Activity topActivity = container.getTopNonFinishingActivity();
         if (topActivity == null) {
             return false;
         }
 
-        return launchPlaceholderIfNecessary(topActivity, false /* isOnCreated */);
+        return launchPlaceholderIfNecessary(wct, topActivity, false /* isOnCreated */);
     }
 
-    boolean launchPlaceholderIfNecessary(@NonNull Activity activity, boolean isOnCreated) {
+    @GuardedBy("mLock")
+    boolean launchPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity activity, boolean isOnCreated) {
         if (activity.isFinishing()) {
             return false;
         }
@@ -1216,7 +1244,7 @@
 
         // TODO(b/190433398): Handle failed request
         final Bundle options = getPlaceholderOptions(activity, isOnCreated);
-        startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), options,
+        startActivityToSide(wct, activity, placeholderRule.getPlaceholderIntent(), options,
                 placeholderRule, null /* failureCallback */, true /* isPlaceholder */);
         return true;
     }
@@ -1243,7 +1271,9 @@
     }
 
     @VisibleForTesting
-    boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) {
+    @GuardedBy("mLock")
+    boolean dismissPlaceholderIfNecessary(@NonNull WindowContainerTransaction wct,
+            @NonNull SplitContainer splitContainer) {
         if (!splitContainer.isPlaceholderContainer()) {
             return false;
         }
@@ -1257,7 +1287,7 @@
             return false;
         }
 
-        mPresenter.cleanupContainer(splitContainer.getSecondaryContainer(),
+        mPresenter.cleanupContainer(wct, splitContainer.getSecondaryContainer(),
                 false /* shouldFinishDependent */);
         return true;
     }
@@ -1523,7 +1553,8 @@
     private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter {
 
         @Override
-        public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) {
+        public void onActivityPreCreated(@NonNull Activity activity,
+                @Nullable Bundle savedInstanceState) {
             synchronized (mLock) {
                 final IBinder activityToken = activity.getActivityToken();
                 final IBinder initialTaskFragmentToken = getInitialTaskFragmentToken(activity);
@@ -1552,25 +1583,30 @@
         }
 
         @Override
-        public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) {
+        public void onActivityPostCreated(@NonNull Activity activity,
+                @Nullable Bundle savedInstanceState) {
             // Calling after Activity#onCreate is complete to allow the app launch something
             // first. In case of a configured placeholder activity we want to make sure
             // that we don't launch it if an activity itself already requested something to be
             // launched to side.
             synchronized (mLock) {
-                SplitController.this.onActivityCreated(activity);
+                final WindowContainerTransaction wct = new WindowContainerTransaction();
+                SplitController.this.onActivityCreated(wct, activity);
+                mPresenter.applyTransaction(wct);
             }
         }
 
         @Override
-        public void onActivityConfigurationChanged(Activity activity) {
+        public void onActivityConfigurationChanged(@NonNull Activity activity) {
             synchronized (mLock) {
-                SplitController.this.onActivityConfigurationChanged(activity);
+                final WindowContainerTransaction wct = new WindowContainerTransaction();
+                SplitController.this.onActivityConfigurationChanged(wct, activity);
+                mPresenter.applyTransaction(wct);
             }
         }
 
         @Override
-        public void onActivityPostDestroyed(Activity activity) {
+        public void onActivityPostDestroyed(@NonNull Activity activity) {
             synchronized (mLock) {
                 SplitController.this.onActivityDestroyed(activity);
             }
@@ -1582,7 +1618,7 @@
         private final Handler mHandler = new Handler(Looper.getMainLooper());
 
         @Override
-        public void execute(Runnable r) {
+        public void execute(@NonNull Runnable r) {
             mHandler.post(r);
         }
     }
@@ -1662,7 +1698,7 @@
      * If the two rules have the same presentation, we can reuse the same {@link SplitContainer} if
      * there is any.
      */
-    private static boolean canReuseContainer(SplitRule rule1, SplitRule rule2) {
+    private static boolean canReuseContainer(@NonNull SplitRule rule1, @NonNull SplitRule rule2) {
         if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) {
             return false;
         }
@@ -1670,7 +1706,8 @@
     }
 
     /** Whether the two rules have the same presentation. */
-    private static boolean haveSamePresentation(SplitPairRule rule1, SplitPairRule rule2) {
+    private static boolean haveSamePresentation(@NonNull SplitPairRule rule1,
+            @NonNull SplitPairRule rule2) {
         // TODO(b/231655482): add util method to do the comparison in SplitPairRule.
         return rule1.getSplitRatio() == rule2.getSplitRatio()
                 && rule1.getLayoutDirection() == rule2.getLayoutDirection()
@@ -1684,7 +1721,7 @@
      * Whether it is ok for other rule to reuse the {@link TaskFragmentContainer} of the given
      * rule.
      */
-    private static boolean isContainerReusableRule(SplitRule rule) {
+    private static boolean isContainerReusableRule(@NonNull SplitRule rule) {
         // We don't expect to reuse the placeholder rule.
         if (!(rule instanceof SplitPairRule)) {
             return false;
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 a89847a..2b069d7 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -102,37 +102,18 @@
 
     private final SplitController mController;
 
-    SplitPresenter(@NonNull Executor executor, SplitController controller) {
+    SplitPresenter(@NonNull Executor executor, @NonNull SplitController controller) {
         super(executor, controller);
         mController = controller;
         registerOrganizer();
     }
 
     /**
-     * Updates the presentation of the provided container.
-     */
-    void updateContainer(@NonNull TaskFragmentContainer container) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
-        mController.updateContainer(wct, container);
-        applyTransaction(wct);
-    }
-
-    /**
      * Deletes the specified container and all other associated and dependent containers in the same
      * transaction.
      */
-    void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
-        cleanupContainer(container, shouldFinishDependent, wct);
-        applyTransaction(wct);
-    }
-
-    /**
-     * Deletes the specified container and all other associated and dependent containers in the same
-     * transaction.
-     */
-    void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent,
-            @NonNull WindowContainerTransaction wct) {
+    void cleanupContainer(@NonNull WindowContainerTransaction wct,
+            @NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
         container.finish(shouldFinishDependent, this, wct, mController);
 
         final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer(
@@ -190,10 +171,9 @@
      *                          created and the activity will be re-parented to it.
      * @param rule The split rule to be applied to the container.
      */
-    void createNewSplitContainer(@NonNull Activity primaryActivity,
-            @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule) {
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
-
+    void createNewSplitContainer(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity,
+            @NonNull SplitPairRule rule) {
         final Rect parentBounds = getParentContainerBounds(primaryActivity);
         final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity,
                 secondaryActivity);
@@ -219,8 +199,6 @@
                 minDimensionsPair);
 
         mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule);
-
-        applyTransaction(wct);
     }
 
     /**
@@ -262,7 +240,8 @@
      * @param rule              The split rule to be applied to the container.
      * @param isPlaceholder     Whether the launch is a placeholder.
      */
-    void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent,
+    void startActivityToSide(@NonNull WindowContainerTransaction wct,
+            @NonNull Activity launchingActivity, @NonNull Intent activityIntent,
             @Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) {
         final Rect parentBounds = getParentContainerBounds(launchingActivity);
         final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
@@ -284,7 +263,6 @@
                 launchingActivity, taskId);
         final int windowingMode = mController.getTaskContainer(taskId)
                 .getWindowingModeForSplitTaskFragment(primaryRectBounds);
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
         mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
                 rule);
         startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds,
@@ -294,7 +272,6 @@
             // When placeholder is launched in split, we should keep the focus on the primary.
             wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
         }
-        applyTransaction(wct);
     }
 
     /**
@@ -502,14 +479,14 @@
     }
 
     @NonNull
-    static Pair<Size, Size> getActivitiesMinDimensionsPair(Activity primaryActivity,
-            Activity secondaryActivity) {
+    static Pair<Size, Size> getActivitiesMinDimensionsPair(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity) {
         return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity));
     }
 
     @NonNull
-    static Pair<Size, Size> getActivityIntentMinDimensionsPair(Activity primaryActivity,
-            Intent secondaryIntent) {
+    static Pair<Size, Size> getActivityIntentMinDimensionsPair(@NonNull Activity primaryActivity,
+            @NonNull Intent secondaryIntent) {
         return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent));
     }
 
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 0ea5603..77e26c0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -21,8 +21,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.WindowConfiguration;
 import android.app.WindowConfiguration.WindowingMode;
@@ -31,6 +29,9 @@
 import android.util.ArraySet;
 import android.window.TaskFragmentInfo;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
index f721341..ee2e139 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
@@ -30,6 +30,8 @@
 import android.view.RemoteAnimationDefinition;
 import android.window.TaskFragmentOrganizer;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.annotations.VisibleForTesting;
 
 /** Controls the TaskFragment remote animations. */
@@ -45,7 +47,7 @@
     /** Task Ids that we have registered for remote animation. */
     private final ArraySet<Integer> mRegisterTasks = new ArraySet<>();
 
-    TaskFragmentAnimationController(TaskFragmentOrganizer organizer) {
+    TaskFragmentAnimationController(@NonNull TaskFragmentOrganizer organizer) {
         mOrganizer = organizer;
         mDefinition = new RemoteAnimationDefinition();
         final RemoteAnimationAdapter animationAdapter =
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
index c4f3709..8af2d9c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -112,6 +112,7 @@
     }
 
     /** Creates the animator given the transition type and windows. */
+    @NonNull
     private Animator createAnimator(@WindowManager.TransitionOldType int transit,
             @NonNull RemoteAnimationTarget[] targets,
             @NonNull IRemoteAnimationFinishedCallback finishedCallback) {
@@ -161,6 +162,7 @@
     }
 
     /** List of {@link TaskFragmentAnimationAdapter} to handle animations on all window targets. */
+    @NonNull
     private List<TaskFragmentAnimationAdapter> createAnimationAdapters(
             @WindowManager.TransitionOldType int transit,
             @NonNull RemoteAnimationTarget[] targets) {
@@ -180,12 +182,14 @@
         }
     }
 
+    @NonNull
     private List<TaskFragmentAnimationAdapter> createOpenAnimationAdapters(
             @NonNull RemoteAnimationTarget[] targets) {
         return createOpenCloseAnimationAdapters(targets, true /* isOpening */,
                 mAnimationSpec::loadOpenAnimation);
     }
 
+    @NonNull
     private List<TaskFragmentAnimationAdapter> createCloseAnimationAdapters(
             @NonNull RemoteAnimationTarget[] targets) {
         return createOpenCloseAnimationAdapters(targets, false /* isOpening */,
@@ -196,6 +200,7 @@
      * Creates {@link TaskFragmentAnimationAdapter} for OPEN and CLOSE types of transition.
      * @param isOpening {@code true} for OPEN type, {@code false} for CLOSE type.
      */
+    @NonNull
     private List<TaskFragmentAnimationAdapter> createOpenCloseAnimationAdapters(
             @NonNull RemoteAnimationTarget[] targets, boolean isOpening,
             @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider) {
@@ -238,6 +243,7 @@
         return adapters;
     }
 
+    @NonNull
     private TaskFragmentAnimationAdapter createOpenCloseAnimationAdapter(
             @NonNull RemoteAnimationTarget target,
             @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider,
@@ -259,6 +265,7 @@
         return new TaskFragmentAnimationAdapter(animation, target);
     }
 
+    @NonNull
     private List<TaskFragmentAnimationAdapter> createChangeAnimationAdapters(
             @NonNull RemoteAnimationTarget[] targets) {
         final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
index 5cc496a..97d42391b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
@@ -26,6 +26,7 @@
 import android.os.Handler;
 import android.provider.Settings;
 import android.view.RemoteAnimationTarget;
+import android.view.WindowManager;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
 import android.view.animation.AnimationSet;
@@ -68,16 +69,14 @@
 
         // The transition animation should be adjusted based on the developer option.
         final ContentResolver resolver = mContext.getContentResolver();
-        mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver,
-                Settings.Global.TRANSITION_ANIMATION_SCALE,
-                mContext.getResources().getFloat(
-                        R.dimen.config_appTransitionAnimationDurationScaleDefault));
+        mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
         resolver.registerContentObserver(
                 Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false,
                 new SettingsObserver(handler));
     }
 
     /** For target that doesn't need to be animated. */
+    @NonNull
     static Animation createNoopAnimation(@NonNull RemoteAnimationTarget target) {
         // Noop but just keep the target showing/hiding.
         final float alpha = target.mode == MODE_CLOSING ? 0f : 1f;
@@ -85,6 +84,7 @@
     }
 
     /** Animation for target that is opening in a change transition. */
+    @NonNull
     Animation createChangeBoundsOpenAnimation(@NonNull RemoteAnimationTarget target) {
         final Rect bounds = target.localBounds;
         // The target will be animated in from left or right depends on its position.
@@ -101,6 +101,7 @@
     }
 
     /** Animation for target that is closing in a change transition. */
+    @NonNull
     Animation createChangeBoundsCloseAnimation(@NonNull RemoteAnimationTarget target) {
         final Rect bounds = target.localBounds;
         // The target will be animated out to left or right depends on its position.
@@ -121,6 +122,7 @@
      * @return the return array always has two elements. The first one is for the start leash, and
      *         the second one is for the end leash.
      */
+    @NonNull
     Animation[] createChangeBoundsChangeAnimations(@NonNull RemoteAnimationTarget target) {
         // Both start bounds and end bounds are in screen coordinates. We will post translate
         // to the local coordinates in TaskFragmentAnimationAdapter#onAnimationUpdate
@@ -177,6 +179,7 @@
         return new Animation[]{startSet, endSet};
     }
 
+    @NonNull
     Animation loadOpenAnimation(@NonNull RemoteAnimationTarget target,
             @NonNull Rect wholeAnimationBounds) {
         final boolean isEnter = target.mode != MODE_CLOSING;
@@ -198,6 +201,7 @@
         return animation;
     }
 
+    @NonNull
     Animation loadCloseAnimation(@NonNull RemoteAnimationTarget target,
             @NonNull Rect wholeAnimationBounds) {
         final boolean isEnter = target.mode != MODE_CLOSING;
@@ -217,6 +221,12 @@
         return animation;
     }
 
+    private float getTransitionAnimationScaleSetting() {
+        return WindowManager.fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
+                                R.dimen.config_appTransitionAnimationDurationScaleDefault)));
+    }
+
     private class SettingsObserver extends ContentObserver {
         SettingsObserver(@NonNull Handler handler) {
             super(handler);
@@ -224,9 +234,7 @@
 
         @Override
         public void onChange(boolean selfChange) {
-            mTransitionAnimationScaleSetting = Settings.Global.getFloat(
-                    mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE,
-                    mTransitionAnimationScaleSetting);
+            mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
         }
     }
 }
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 37f5b6d..11c0db3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -18,8 +18,6 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
-import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.WindowConfiguration.WindowingMode;
 import android.content.Intent;
@@ -30,6 +28,9 @@
 import android.window.TaskFragmentInfo;
 import android.window.WindowContainerTransaction;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
@@ -175,6 +176,7 @@
                 && mInfo.getActivities().size() == collectNonFinishingActivities().size();
     }
 
+    @NonNull
     ActivityStack toActivityStack() {
         return new ActivityStack(collectNonFinishingActivities(), isEmpty());
     }
@@ -249,19 +251,22 @@
         return mInfo;
     }
 
-    void setInfo(@NonNull TaskFragmentInfo info) {
+    void setInfo(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentInfo info) {
         if (!mIsFinished && mInfo == null && info.isEmpty()) {
             // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if no
             // pending appeared intent/activities. Otherwise, wait and removing the TaskFragment if
             // it is still empty after timeout.
-            mAppearEmptyTimeout = () -> {
-                mAppearEmptyTimeout = null;
-                mController.onTaskFragmentAppearEmptyTimeout(this);
-            };
             if (mPendingAppearedIntent != null || !mPendingAppearedActivities.isEmpty()) {
+                mAppearEmptyTimeout = () -> {
+                    mAppearEmptyTimeout = null;
+                    // Call without the pass-in wct when timeout. We need to applyWct directly
+                    // in this case.
+                    mController.onTaskFragmentAppearEmptyTimeout(this);
+                };
                 mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS);
             } else {
-                mAppearEmptyTimeout.run();
+                mAppearEmptyTimeout = null;
+                mController.onTaskFragmentAppearEmptyTimeout(wct, this);
             }
         } else if (mAppearEmptyTimeout != null && !info.isEmpty()) {
             mController.getHandler().removeCallbacks(mAppearEmptyTimeout);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index 4d25952..21cf7a6 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
@@ -56,6 +56,8 @@
  * Build/Install/Run:
  *  atest WMJetpackUnitTests:JetpackTaskFragmentOrganizerTest
  */
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
 @Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -119,7 +121,7 @@
                 new Intent(), taskContainer, mSplitController);
         final TaskFragmentInfo info = createMockInfo(container);
         mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info);
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken());
 
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 4bc5033..07758d2 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
@@ -89,6 +89,8 @@
  * Build/Install/Run:
  *  atest WMJetpackUnitTests:SplitControllerTest
  */
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
 @Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -158,14 +160,14 @@
         final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
         doReturn(new ArrayList<>()).when(info).getActivities();
         doReturn(true).when(info).isEmpty();
-        tf1.setInfo(info);
+        tf1.setInfo(mTransaction, info);
 
         assertWithMessage("Must return tf because we are waiting for tf1 to become non-empty after"
                 + " creation.")
                 .that(mSplitController.getTopActiveContainer(TASK_ID)).isEqualTo(tf1);
 
         doReturn(false).when(info).isEmpty();
-        tf1.setInfo(info);
+        tf1.setInfo(mTransaction, info);
 
         assertWithMessage("Must return null because tf1 becomes empty.")
                 .that(mSplitController.getTopActiveContainer(TASK_ID)).isNull();
@@ -177,7 +179,7 @@
         doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken();
 
         // The TaskFragment has been removed in the server, we only need to cleanup the reference.
-        mSplitController.onTaskFragmentVanished(mInfo);
+        mSplitController.onTaskFragmentVanished(mTransaction, mInfo);
 
         verify(mSplitPresenter, never()).deleteTaskFragment(any(), any());
         verify(mSplitController).removeContainer(tf);
@@ -187,9 +189,10 @@
     @Test
     public void testOnTaskFragmentAppearEmptyTimeout() {
         final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
-        mSplitController.onTaskFragmentAppearEmptyTimeout(tf);
+        mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf);
 
-        verify(mSplitPresenter).cleanupContainer(tf, false /* shouldFinishDependent */);
+        verify(mSplitPresenter).cleanupContainer(mTransaction, tf,
+                false /* shouldFinishDependent */);
     }
 
     @Test
@@ -229,8 +232,8 @@
         spyOn(tf);
         doReturn(mActivity).when(tf).getTopNonFinishingActivity();
         doReturn(true).when(tf).isEmpty();
-        doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mActivity,
-                false /* isOnCreated */);
+        doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mTransaction,
+                mActivity, false /* isOnCreated */);
         doNothing().when(mSplitPresenter).updateSplitContainer(any(), any(), any());
 
         mSplitController.updateContainer(mTransaction, tf);
@@ -250,7 +253,7 @@
 
         mSplitController.updateContainer(mTransaction, tf);
 
-        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any());
+        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any());
 
         // Verify if tf is not in the top splitContainer,
         final SplitContainer splitContainer = mock(SplitContainer.class);
@@ -264,7 +267,7 @@
 
         mSplitController.updateContainer(mTransaction, tf);
 
-        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any());
+        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any());
 
         // Verify if one or both containers in the top SplitContainer are finished,
         // dismissPlaceholder() won't be called.
@@ -273,12 +276,12 @@
 
         mSplitController.updateContainer(mTransaction, tf);
 
-        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any());
+        verify(mSplitController, never()).dismissPlaceholderIfNecessary(any(), any());
 
         // Verify if placeholder should be dismissed, updateSplitContainer() won't be called.
         doReturn(false).when(tf).isFinished();
         doReturn(true).when(mSplitController)
-                .dismissPlaceholderIfNecessary(splitContainer);
+                .dismissPlaceholderIfNecessary(mTransaction, splitContainer);
 
         mSplitController.updateContainer(mTransaction, tf);
 
@@ -286,7 +289,7 @@
 
         // Verify if the top active split is updated if both of its containers are not finished.
         doReturn(false).when(mSplitController)
-                        .dismissPlaceholderIfNecessary(splitContainer);
+                        .dismissPlaceholderIfNecessary(mTransaction, splitContainer);
 
         mSplitController.updateContainer(mTransaction, tf);
 
@@ -315,34 +318,36 @@
 
     @Test
     public void testOnActivityCreated() {
-        mSplitController.onActivityCreated(mActivity);
+        mSplitController.onActivityCreated(mTransaction, mActivity);
 
         // Disallow to split as primary because we want the new launch to be always on top.
-        verify(mSplitController).resolveActivityToContainer(mActivity, false /* isOnReparent */);
+        verify(mSplitController).resolveActivityToContainer(mTransaction, mActivity,
+                false /* isOnReparent */);
     }
 
     @Test
-    public void testOnActivityReparentToTask_sameProcess() {
-        mSplitController.onActivityReparentToTask(TASK_ID, new Intent(),
+    public void testOnActivityReparentedToTask_sameProcess() {
+        mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, new Intent(),
                 mActivity.getActivityToken());
 
         // Treated as on activity created, but allow to split as primary.
-        verify(mSplitController).resolveActivityToContainer(mActivity, true /* isOnReparent */);
+        verify(mSplitController).resolveActivityToContainer(mTransaction,
+                mActivity, true /* isOnReparent */);
         // Try to place the activity to the top TaskFragment when there is no matched rule.
-        verify(mSplitController).placeActivityInTopContainer(mActivity);
+        verify(mSplitController).placeActivityInTopContainer(mTransaction, mActivity);
     }
 
     @Test
-    public void testOnActivityReparentToTask_diffProcess() {
+    public void testOnActivityReparentedToTask_diffProcess() {
         // Create an empty TaskFragment to initialize for the Task.
         mSplitController.newContainer(new Intent(), mActivity, TASK_ID);
         final IBinder activityToken = new Binder();
         final Intent intent = new Intent();
 
-        mSplitController.onActivityReparentToTask(TASK_ID, intent, activityToken);
+        mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken);
 
         // Treated as starting new intent
-        verify(mSplitController, never()).resolveActivityToContainer(any(), anyBoolean());
+        verify(mSplitController, never()).resolveActivityToContainer(any(), any(), anyBoolean());
         verify(mSplitController).resolveStartActivityIntent(any(), eq(TASK_ID), eq(intent),
                 isNull());
     }
@@ -504,26 +509,29 @@
 
     @Test
     public void testPlaceActivityInTopContainer() {
-        mSplitController.placeActivityInTopContainer(mActivity);
+        mSplitController.placeActivityInTopContainer(mTransaction, mActivity);
 
-        verify(mSplitPresenter, never()).applyTransaction(any());
+        verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any());
 
-        mSplitController.newContainer(new Intent(), mActivity, TASK_ID);
-        mSplitController.placeActivityInTopContainer(mActivity);
+        // Place in the top container if there is no other rule matched.
+        final TaskFragmentContainer topContainer = mSplitController
+                .newContainer(new Intent(), mActivity, TASK_ID);
+        mSplitController.placeActivityInTopContainer(mTransaction, mActivity);
 
-        verify(mSplitPresenter).applyTransaction(any());
+        verify(mTransaction).reparentActivityToTaskFragment(topContainer.getTaskFragmentToken(),
+                mActivity.getActivityToken());
 
         // Not reparent if activity is in a TaskFragment.
-        clearInvocations(mSplitPresenter);
+        clearInvocations(mTransaction);
         mSplitController.newContainer(mActivity, TASK_ID);
-        mSplitController.placeActivityInTopContainer(mActivity);
+        mSplitController.placeActivityInTopContainer(mTransaction, mActivity);
 
-        verify(mSplitPresenter, never()).applyTransaction(any());
+        verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any());
     }
 
     @Test
     public void testResolveActivityToContainer_noRuleMatched() {
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
@@ -535,7 +543,7 @@
         setupExpandRule(mActivity);
 
         // When the activity is not in any TaskFragment, create a new expanded TaskFragment for it.
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
         final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
                 mActivity);
@@ -543,7 +551,8 @@
         assertTrue(result);
         assertNotNull(container);
         verify(mSplitController).newContainer(mActivity, TASK_ID);
-        verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity);
+        verify(mSplitPresenter).expandActivity(mTransaction, container.getTaskFragmentToken(),
+                mActivity);
     }
 
     @Test
@@ -552,11 +561,11 @@
 
         // 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,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
-        verify(mSplitPresenter).expandTaskFragment(container.getTaskFragmentToken());
+        verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken());
     }
 
     @Test
@@ -566,14 +575,15 @@
         // 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,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
         final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
                 mActivity);
 
         assertTrue(result);
         assertNotNull(container);
-        verify(mSplitPresenter).expandActivity(container.getTaskFragmentToken(), mActivity);
+        verify(mSplitPresenter).expandActivity(mTransaction, container.getTaskFragmentToken(),
+                mActivity);
     }
 
     @Test
@@ -583,11 +593,11 @@
                 (SplitPlaceholderRule) mSplitController.getSplitRules().get(0);
 
         // Launch placeholder if the activity is not in any TaskFragment.
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
-        verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+        verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT,
                 mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */),
                 placeholderRule, true /* isPlaceholder */);
     }
@@ -600,11 +610,11 @@
         final Activity activity = createMockActivity();
         mSplitController.newContainer(mActivity, TASK_ID);
         mSplitController.newContainer(activity, TASK_ID);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
-        verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(),
+        verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(),
                 anyBoolean());
     }
 
@@ -616,11 +626,11 @@
 
         // Launch placeholder if the activity is in the topmost expanded TaskFragment.
         mSplitController.newContainer(mActivity, TASK_ID);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
-        verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+        verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT,
                 mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */),
                 placeholderRule, true /* isPlaceholder */);
     }
@@ -632,11 +642,11 @@
         // Don't launch placeholder if the activity is in primary split.
         final Activity secondaryActivity = createMockActivity();
         addSplitTaskFragments(mActivity, secondaryActivity);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
-        verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(),
+        verify(mSplitPresenter, never()).startActivityToSide(any(), any(), any(), any(), any(),
                 anyBoolean());
     }
 
@@ -649,11 +659,11 @@
         // Launch placeholder if the activity is in secondary split.
         final Activity primaryActivity = createMockActivity();
         addSplitTaskFragments(primaryActivity, mActivity);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
-        verify(mSplitPresenter).startActivityToSide(mActivity, PLACEHOLDER_INTENT,
+        verify(mSplitPresenter).startActivityToSide(mTransaction, mActivity, PLACEHOLDER_INTENT,
                 mSplitController.getPlaceholderOptions(mActivity, true /* isOnCreated */),
                 placeholderRule, true /* isPlaceholder */);
     }
@@ -676,7 +686,7 @@
                 secondaryContainer,
                 splitRule);
         clearInvocations(mSplitController);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
@@ -705,7 +715,7 @@
         final Activity launchedActivity = createMockActivity();
         primaryContainer.addPendingAppearedActivity(launchedActivity);
 
-        assertFalse(mSplitController.resolveActivityToContainer(launchedActivity,
+        assertFalse(mSplitController.resolveActivityToContainer(mTransaction, launchedActivity,
                 false /* isOnReparent */));
     }
 
@@ -717,7 +727,7 @@
         // Activity is already in secondary split, no need to create new split.
         addSplitTaskFragments(primaryActivity, mActivity);
         clearInvocations(mSplitController);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
@@ -735,7 +745,7 @@
         addSplitTaskFragments(primaryActivity, secondaryActivity);
         mSplitController.getContainerWithActivity(secondaryActivity)
                 .addPendingAppearedActivity(mActivity);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
@@ -760,7 +770,7 @@
                 mActivity,
                 secondaryContainer,
                 placeholderRule);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
@@ -774,7 +784,7 @@
         final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
                 TASK_ID);
         container.addPendingAppearedActivity(mActivity);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
@@ -790,14 +800,15 @@
         final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
                 TASK_ID);
         container.addPendingAppearedActivity(mActivity);
-        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
         assertEquals(container, mSplitController.getContainerWithActivity(mActivity));
 
         // Allow to split as primary.
-        result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */);
+        result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
+                true /* isOnReparent */);
 
         assertTrue(result);
         assertSplitPair(mActivity, activityBelow);
@@ -815,7 +826,7 @@
         final TaskFragmentContainer secondaryContainer = mSplitController.getContainerWithActivity(
                 activityBelow);
         secondaryContainer.addPendingAppearedActivity(mActivity);
-        final boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
         final TaskFragmentContainer container = mSplitController.getContainerWithActivity(
                 mActivity);
@@ -836,14 +847,15 @@
         final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
                 primaryActivity);
         primaryContainer.addPendingAppearedActivity(mActivity);
-        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertFalse(result);
         assertEquals(primaryContainer, mSplitController.getContainerWithActivity(mActivity));
 
 
-        result = mSplitController.resolveActivityToContainer(mActivity, true /* isOnReparent */);
+        result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
+                true /* isOnReparent */);
 
         assertTrue(result);
         assertSplitPair(mActivity, primaryActivity);
@@ -861,7 +873,7 @@
         container.addPendingAppearedActivity(mActivity);
 
         // Allow to split as primary.
-        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 true /* isOnReparent */);
 
         assertTrue(result);
@@ -879,15 +891,13 @@
                 TASK_ID);
         container.addPendingAppearedActivity(mActivity);
 
-        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
         assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */);
     }
 
-    // Suppress GuardedBy warning on unit tests
-    @SuppressWarnings("GuardedBy")
     @Test
     public void testResolveActivityToContainer_minDimensions_shouldExpandSplitContainer() {
         final Activity primaryActivity = createMockActivity();
@@ -899,14 +909,14 @@
         doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity));
 
         clearInvocations(mSplitPresenter);
-        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+        boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */);
 
         assertTrue(result);
         assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */);
         assertEquals(mSplitController.getContainerWithActivity(secondaryActivity),
                 mSplitController.getContainerWithActivity(mActivity));
-        verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any());
+        verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any(), any());
     }
 
     @Test
@@ -914,7 +924,7 @@
         doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity);
 
         // No need to handle when the new launched activity is in an unknown TaskFragment.
-        assertTrue(mSplitController.resolveActivityToContainer(mActivity,
+        assertTrue(mSplitController.resolveActivityToContainer(mTransaction, mActivity,
                 false /* isOnReparent */));
     }
 
@@ -993,7 +1003,7 @@
     private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container,
             @NonNull Activity activity) {
         final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity);
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
         mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info);
     }
 
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index d7931966..3fdf8e5 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -78,6 +78,8 @@
  * Build/Install/Run:
  *  atest WMJetpackUnitTests:SplitPresenterTest
  */
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
 @Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -226,8 +228,9 @@
                 mTransaction, splitContainer, mActivity, secondaryActivity,
                 null /* secondaryIntent */));
 
-        primaryTf.setInfo(createMockTaskFragmentInfo(primaryTf, mActivity));
-        secondaryTf.setInfo(createMockTaskFragmentInfo(secondaryTf, secondaryActivity));
+        primaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(primaryTf, mActivity));
+        secondaryTf.setInfo(mTransaction,
+                createMockTaskFragmentInfo(secondaryTf, secondaryActivity));
 
         assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
                 splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
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 44c7e6c..6cbecff 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
@@ -19,6 +19,8 @@
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
 import static org.junit.Assert.assertEquals;
@@ -36,7 +38,6 @@
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Binder;
-import android.os.Handler;
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
 import android.window.TaskFragmentInfo;
@@ -62,25 +63,27 @@
  * Build/Install/Run:
  *  atest WMJetpackUnitTests:TaskFragmentContainerTest
  */
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
 @Presubmit
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class TaskFragmentContainerTest {
     @Mock
     private SplitPresenter mPresenter;
-    @Mock
     private SplitController mController;
     @Mock
     private TaskFragmentInfo mInfo;
     @Mock
-    private Handler mHandler;
+    private WindowContainerTransaction mTransaction;
     private Activity mActivity;
     private Intent mIntent;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        doReturn(mHandler).when(mController).getHandler();
+        mController = new SplitController();
+        spyOn(mController);
         mActivity = createMockActivity();
         mIntent = new Intent();
     }
@@ -123,7 +126,7 @@
 
         // Remove all references after the container has appeared in server.
         doReturn(new ArrayList<>()).when(mInfo).getActivities();
-        container.setInfo(mInfo);
+        container.setInfo(mTransaction, mInfo);
         container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
 
         verify(mActivity, never()).finish();
@@ -137,7 +140,7 @@
         final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity,
                 null /* pendingAppearedIntent */, taskContainer, mController);
         final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity);
-        container0.setInfo(info);
+        container0.setInfo(mTransaction, info);
         // Request to reparent the activity to a new TaskFragment.
         final TaskFragmentContainer container1 = new TaskFragmentContainer(mActivity,
                 null /* pendingAppearedIntent */, taskContainer, mController);
@@ -163,7 +166,7 @@
 
         final TaskFragmentInfo info0 = createMockTaskFragmentInfo(pendingActivityContainer,
                 mActivity);
-        pendingActivityContainer.setInfo(info0);
+        pendingActivityContainer.setInfo(mTransaction, info0);
 
         assertTrue(pendingActivityContainer.mPendingAppearedActivities.isEmpty());
 
@@ -175,7 +178,7 @@
 
         final TaskFragmentInfo info1 = createMockTaskFragmentInfo(pendingIntentContainer,
                 mActivity);
-        pendingIntentContainer.setInfo(info1);
+        pendingIntentContainer.setInfo(mTransaction, info1);
 
         assertNull(pendingIntentContainer.getPendingAppearedIntent());
     }
@@ -191,18 +194,19 @@
         final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
         doReturn(new ArrayList<>()).when(info).getActivities();
         doReturn(true).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         assertTrue(container.isWaitingActivityAppear());
 
         doReturn(false).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         assertFalse(container.isWaitingActivityAppear());
     }
 
     @Test
     public void testAppearEmptyTimeout() {
+        doNothing().when(mController).onTaskFragmentAppearEmptyTimeout(any(), any());
         final TaskContainer taskContainer = new TaskContainer(TASK_ID);
         final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
                 mIntent, taskContainer, mController);
@@ -213,20 +217,20 @@
         final TaskFragmentInfo info = mock(TaskFragmentInfo.class);
         container.mInfo = null;
         doReturn(true).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         assertNotNull(container.mAppearEmptyTimeout);
 
         // Not set if it is not appeared empty.
         doReturn(new ArrayList<>()).when(info).getActivities();
         doReturn(false).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         assertNull(container.mAppearEmptyTimeout);
 
         // Remove timeout after the container becomes non-empty.
         doReturn(false).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
 
         assertNull(container.mAppearEmptyTimeout);
 
@@ -234,7 +238,7 @@
         container.mInfo = null;
         container.setPendingAppearedIntent(mIntent);
         doReturn(true).when(info).isEmpty();
-        container.setInfo(info);
+        container.setInfo(mTransaction, info);
         container.mAppearEmptyTimeout.run();
 
         assertNull(container.mAppearEmptyTimeout);
@@ -260,7 +264,7 @@
         final List<IBinder> runningActivities = Lists.newArrayList(activity0.getActivityToken(),
                 activity1.getActivityToken());
         doReturn(runningActivities).when(mInfo).getActivities();
-        container.setInfo(mInfo);
+        container.setInfo(mTransaction, mInfo);
         activities = container.collectNonFinishingActivities();
 
         assertEquals(3, activities.size());
@@ -295,7 +299,7 @@
         final Activity activity = createMockActivity();
         final List<IBinder> runningActivities = Lists.newArrayList(activity.getActivityToken());
         doReturn(runningActivities).when(mInfo).getActivities();
-        container.setInfo(mInfo);
+        container.setInfo(mTransaction, mInfo);
 
         assertEquals(activity, container.getBottomMostActivity());
     }
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index 918e514..e9a1721 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
index 8533a59..b0dab90 100644
--- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
@@ -16,92 +16,98 @@
 -->
 <!-- Layout for TvPipMenuView -->
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-            android:id="@+id/tv_pip_menu"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:gravity="center|top">
+                android:id="@+id/tv_pip_menu"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:gravity="center|top">
 
     <!-- Matches the PiP app content -->
-    <View
+    <FrameLayout
         android:id="@+id/tv_pip"
         android:layout_width="0dp"
         android:layout_height="0dp"
-        android:alpha="0"
-        android:background="@color/tv_pip_menu_background"
         android:layout_marginTop="@dimen/pip_menu_outer_space"
         android:layout_marginStart="@dimen/pip_menu_outer_space"
-        android:layout_marginEnd="@dimen/pip_menu_outer_space"/>
+        android:layout_marginEnd="@dimen/pip_menu_outer_space">
 
-    <ScrollView
-        android:id="@+id/tv_pip_menu_scroll"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_alignTop="@+id/tv_pip"
-        android:layout_alignStart="@+id/tv_pip"
-        android:layout_alignEnd="@+id/tv_pip"
-        android:layout_alignBottom="@+id/tv_pip"
-        android:scrollbars="none"
-        android:visibility="gone"/>
+        <View
+            android:id="@+id/tv_pip_menu_background"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@color/tv_pip_menu_background"
+            android:alpha="0"/>
 
-    <HorizontalScrollView
-        android:id="@+id/tv_pip_menu_horizontal_scroll"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_alignTop="@+id/tv_pip"
-        android:layout_alignStart="@+id/tv_pip"
-        android:layout_alignEnd="@+id/tv_pip"
-        android:layout_alignBottom="@+id/tv_pip"
-        android:scrollbars="none">
+        <View
+            android:id="@+id/tv_pip_menu_dim_layer"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@color/tv_pip_menu_dim_layer"
+            android:alpha="0"/>
 
-        <LinearLayout
-            android:id="@+id/tv_pip_menu_action_buttons"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:orientation="horizontal"
-            android:alpha="0">
+        <ScrollView
+            android:id="@+id/tv_pip_menu_scroll"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:scrollbars="none"
+            android:visibility="gone"/>
 
-            <Space
-                android:layout_width="@dimen/pip_menu_button_wrapper_margin"
-                android:layout_height="@dimen/pip_menu_button_wrapper_margin"/>
+        <HorizontalScrollView
+            android:id="@+id/tv_pip_menu_horizontal_scroll"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:scrollbars="none">
 
-            <com.android.wm.shell.common.TvWindowMenuActionButton
-                android:id="@+id/tv_pip_menu_fullscreen_button"
+            <LinearLayout
+                android:id="@+id/tv_pip_menu_action_buttons"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:src="@drawable/pip_ic_fullscreen_white"
-                android:text="@string/pip_fullscreen" />
+                android:orientation="horizontal"
+                android:alpha="0">
 
-            <com.android.wm.shell.common.TvWindowMenuActionButton
-                android:id="@+id/tv_pip_menu_close_button"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:src="@drawable/pip_ic_close_white"
-                android:text="@string/pip_close" />
+                <Space
+                    android:layout_width="@dimen/pip_menu_button_wrapper_margin"
+                    android:layout_height="@dimen/pip_menu_button_wrapper_margin"/>
 
-            <!-- More TvPipMenuActionButtons may be added here at runtime. -->
+                <com.android.wm.shell.pip.tv.TvPipMenuActionButton
+                    android:id="@+id/tv_pip_menu_fullscreen_button"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:src="@drawable/pip_ic_fullscreen_white"
+                    android:text="@string/pip_fullscreen" />
 
-            <com.android.wm.shell.common.TvWindowMenuActionButton
-                android:id="@+id/tv_pip_menu_move_button"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:src="@drawable/pip_ic_move_white"
-                android:text="@string/pip_move" />
+                <com.android.wm.shell.pip.tv.TvPipMenuActionButton
+                    android:id="@+id/tv_pip_menu_close_button"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:src="@drawable/pip_ic_close_white"
+                    android:text="@string/pip_close" />
 
-            <com.android.wm.shell.common.TvWindowMenuActionButton
-                android:id="@+id/tv_pip_menu_expand_button"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:src="@drawable/pip_ic_collapse"
-                android:visibility="gone"
-                android:text="@string/pip_collapse" />
+                <!-- More TvPipMenuActionButtons may be added here at runtime. -->
 
-            <Space
-                android:layout_width="@dimen/pip_menu_button_wrapper_margin"
-                android:layout_height="@dimen/pip_menu_button_wrapper_margin"/>
+                <com.android.wm.shell.pip.tv.TvPipMenuActionButton
+                    android:id="@+id/tv_pip_menu_move_button"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:src="@drawable/pip_ic_move_white"
+                    android:text="@string/pip_move" />
 
-        </LinearLayout>
-    </HorizontalScrollView>
+                <com.android.wm.shell.pip.tv.TvPipMenuActionButton
+                    android:id="@+id/tv_pip_menu_expand_button"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:src="@drawable/pip_ic_collapse"
+                    android:visibility="gone"
+                    android:text="@string/pip_collapse" />
 
+                <Space
+                    android:layout_width="@dimen/pip_menu_button_wrapper_margin"
+                    android:layout_height="@dimen/pip_menu_button_wrapper_margin"/>
+
+            </LinearLayout>
+        </HorizontalScrollView>
+    </FrameLayout>
+
+    <!-- Frame around the content, just overlapping the corners to make them round -->
     <View
         android:id="@+id/tv_pip_border"
         android:layout_width="0dp"
@@ -111,6 +117,7 @@
         android:layout_marginEnd="@dimen/pip_menu_outer_space_frame"
         android:background="@drawable/tv_pip_menu_border"/>
 
+    <!-- Temporarily extending the background to show an edu text hint for opening the menu -->
     <FrameLayout
         android:id="@+id/tv_pip_menu_edu_text_container"
         android:layout_width="match_parent"
@@ -138,6 +145,7 @@
             android:textAppearance="@style/TvPipEduText"/>
     </FrameLayout>
 
+    <!-- Frame around the PiP content + edu text hint - used to highlight open menu -->
     <View
         android:id="@+id/tv_pip_menu_frame"
         android:layout_width="match_parent"
@@ -145,7 +153,8 @@
         android:layout_margin="@dimen/pip_menu_outer_space_frame"
         android:background="@drawable/tv_pip_menu_border"/>
 
-    <com.android.wm.shell.common.TvWindowMenuActionButton
+    <!-- Move menu -->
+    <com.android.wm.shell.pip.tv.TvPipMenuActionButton
         android:id="@+id/tv_pip_menu_done_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/libs/WindowManager/Shell/res/values/colors_tv.xml b/libs/WindowManager/Shell/res/values/colors_tv.xml
index 3e71c10..e6933ca 100644
--- a/libs/WindowManager/Shell/res/values/colors_tv.xml
+++ b/libs/WindowManager/Shell/res/values/colors_tv.xml
@@ -25,6 +25,7 @@
     <color name="tv_window_menu_icon_bg_unfocused">#990E0E0F</color>
 
     <color name="tv_pip_menu_focus_border">#E8EAED</color>
+    <color name="tv_pip_menu_dim_layer">#990E0E0F</color>
     <color name="tv_pip_menu_background">#1E232C</color>
 
     <color name="tv_pip_edu_text">#99D2E3FC</color>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
index 764e650..b085b73 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java
@@ -16,14 +16,20 @@
 
 package com.android.wm.shell;
 
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+
+import android.app.WindowConfiguration;
 import android.util.SparseArray;
 import android.view.SurfaceControl;
 import android.window.DisplayAreaAppearedInfo;
 import android.window.DisplayAreaInfo;
 import android.window.DisplayAreaOrganizer;
+import android.window.WindowContainerTransaction;
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.protolog.common.ProtoLog;
+
 import java.io.PrintWriter;
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -102,10 +108,44 @@
         mDisplayAreasInfo.put(displayId, displayAreaInfo);
     }
 
+    /**
+     * Create a {@link WindowContainerTransaction} to update display windowing mode.
+     *
+     * @param displayId display id to update windowing mode for
+     * @param windowingMode target {@link WindowConfiguration.WindowingMode}
+     * @return {@link WindowContainerTransaction} with pending operation to set windowing mode
+     */
+    public WindowContainerTransaction prepareWindowingModeChange(int displayId,
+            @WindowConfiguration.WindowingMode int windowingMode) {
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId);
+        if (displayAreaInfo == null) {
+            ProtoLog.e(WM_SHELL_DESKTOP_MODE,
+                    "unable to update windowing mode for display %d display not found", displayId);
+            return wct;
+        }
+
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                "setWindowingMode: displayId=%d current wmMode=%d new wmMode=%d", displayId,
+                displayAreaInfo.configuration.windowConfiguration.getWindowingMode(),
+                windowingMode);
+
+        wct.setWindowingMode(displayAreaInfo.token, windowingMode);
+        return wct;
+    }
+
     public void dump(@NonNull PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
         final String childPrefix = innerPrefix + "  ";
         pw.println(prefix + this);
+
+        for (int i = 0; i < mDisplayAreasInfo.size(); i++) {
+            int displayId = mDisplayAreasInfo.keyAt(i);
+            DisplayAreaInfo displayAreaInfo = mDisplayAreasInfo.get(displayId);
+            int windowingMode =
+                    displayAreaInfo.configuration.windowConfiguration.getWindowingMode();
+            pw.println(innerPrefix + "# displayId=" + displayId + " wmMode=" + windowingMode);
+        }
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 6ae0f9b..d5d4935 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -22,6 +22,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
@@ -46,6 +47,7 @@
 import android.window.StartingWindowRemovalInfo;
 import android.window.TaskAppearedInfo;
 import android.window.TaskOrganizer;
+import android.window.WindowContainerTransaction;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
@@ -690,6 +692,49 @@
         taskListener.reparentChildSurfaceToTask(taskId, sc, t);
     }
 
+    /**
+     * Create a {@link WindowContainerTransaction} to clear task bounds.
+     *
+     * @param displayId display id for tasks that will have bounds cleared
+     * @return {@link WindowContainerTransaction} with pending operations to clear bounds
+     */
+    public WindowContainerTransaction prepareClearBoundsForTasks(int displayId) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearBoundsForTasks: displayId=%d", displayId);
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        for (int i = 0; i < mTasks.size(); i++) {
+            RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
+            if (taskInfo.displayId == displayId) {
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE, "clearing bounds for token=%s taskInfo=%s",
+                        taskInfo.token, taskInfo);
+                wct.setBounds(taskInfo.token, null);
+            }
+        }
+        return wct;
+    }
+
+    /**
+     * Create a {@link WindowContainerTransaction} to clear task level freeform setting.
+     *
+     * @param displayId display id for tasks that will have windowing mode reset to {@link
+     *                  WindowConfiguration#WINDOWING_MODE_UNDEFINED}
+     * @return {@link WindowContainerTransaction} with pending operations to clear windowing mode
+     */
+    public WindowContainerTransaction prepareClearFreeformForTasks(int displayId) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "prepareClearFreeformForTasks: displayId=%d", displayId);
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        for (int i = 0; i < mTasks.size(); i++) {
+            RunningTaskInfo taskInfo = mTasks.valueAt(i).getTaskInfo();
+            if (taskInfo.displayId == displayId
+                    && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                        "clearing windowing mode for token=%s taskInfo=%s", taskInfo.token,
+                        taskInfo);
+                wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+            }
+        }
+        return wct;
+    }
+
     private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info,
             int event) {
         ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo;
@@ -816,7 +861,14 @@
                 final int key = mTasks.keyAt(i);
                 final TaskAppearedInfo info = mTasks.valueAt(i);
                 final TaskListener listener = getTaskListener(info.getTaskInfo());
-                pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener);
+                final int windowingMode = info.getTaskInfo().getWindowingMode();
+                String pkg = "";
+                if (info.getTaskInfo().baseActivity != null) {
+                    pkg = info.getTaskInfo().baseActivity.getPackageName();
+                }
+                Rect bounds = info.getTaskInfo().getConfiguration().windowConfiguration.getBounds();
+                pw.println(innerPrefix + "#" + i + " task=" + key + " listener=" + listener
+                        + " wmMode=" + windowingMode + " pkg=" + pkg + " bounds=" + bounds);
             }
 
             pw.println();
@@ -826,6 +878,7 @@
                 final TaskListener listener = mLaunchCookieToListener.valueAt(i);
                 pw.println(innerPrefix + "#" + i + " cookie=" + key + " listener=" + listener);
             }
+
         }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index d5875c0..e270edb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -221,8 +221,7 @@
             }
             final Display display = mDisplayController.getDisplay(mDisplayId);
             SurfaceControlViewHost viewRoot =
-                    new SurfaceControlViewHost(
-                            view.getContext(), display, wwm, true /* useSfChoreographer */);
+                    new SurfaceControlViewHost(view.getContext(), display, wwm);
             attrs.flags |= FLAG_HARDWARE_ACCELERATED;
             viewRoot.setView(view, attrs);
             mViewRoots.put(view, viewRoot);
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 85c8ebf..83ba909 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
@@ -80,7 +80,10 @@
     public static final int PARALLAX_DISMISSING = 1;
     public static final int PARALLAX_ALIGN_CENTER = 2;
 
-    private static final int FLING_ANIMATION_DURATION = 250;
+    private static final int FLING_RESIZE_DURATION = 250;
+    private static final int FLING_SWITCH_DURATION = 350;
+    private static final int FLING_ENTER_DURATION = 350;
+    private static final int FLING_EXIT_DURATION = 350;
 
     private final int mDividerWindowWidth;
     private final int mDividerInsets;
@@ -93,6 +96,9 @@
     private final Rect mBounds1 = new Rect();
     // Bounds2 final position should be always at bottom or right
     private final Rect mBounds2 = new Rect();
+    // The temp bounds outside of display bounds for side stage when split screen inactive to avoid
+    // flicker next time active split screen.
+    private final Rect mInvisibleBounds = new Rect();
     private final Rect mWinBounds1 = new Rect();
     private final Rect mWinBounds2 = new Rect();
     private final SplitLayoutHandler mSplitLayoutHandler;
@@ -141,6 +147,10 @@
         resetDividerPosition();
 
         mDimNonImeSide = resources.getBoolean(R.bool.config_dimNonImeAttachedSide);
+
+        mInvisibleBounds.set(mRootBounds);
+        mInvisibleBounds.offset(isLandscape() ? mRootBounds.right : 0,
+                isLandscape() ? 0 : mRootBounds.bottom);
     }
 
     private int getDividerInsets(Resources resources, Display display) {
@@ -239,6 +249,12 @@
         rect.offset(-mRootBounds.left, -mRootBounds.top);
     }
 
+    /** Gets bounds size equal to root bounds but outside of screen, used for position side stage
+     * when split inactive to avoid flicker when next time active. */
+    public void getInvisibleBounds(Rect rect) {
+        rect.set(mInvisibleBounds);
+    }
+
     /** Returns leash of the current divider bar. */
     @Nullable
     public SurfaceControl getDividerLeash() {
@@ -284,6 +300,10 @@
         mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds, null);
         initDividerPosition(mTempRect);
 
+        mInvisibleBounds.set(mRootBounds);
+        mInvisibleBounds.offset(isLandscape() ? mRootBounds.right : 0,
+                isLandscape() ? 0 : mRootBounds.bottom);
+
         return true;
     }
 
@@ -405,6 +425,13 @@
         mFreezeDividerWindow = freezeDividerWindow;
     }
 
+    /** Update current layout as divider put on start or end position. */
+    public void setDividerAtBorder(boolean start) {
+        final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position
+                : mDividerSnapAlgorithm.getDismissEndTarget().position;
+        setDividePosition(pos, false /* applyLayoutChange */);
+    }
+
     /**
      * Updates bounds with the passing position. Usually used to update recording bounds while
      * performing animation or dragging divider bar to resize the splits.
@@ -449,17 +476,17 @@
     public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) {
         switch (snapTarget.flag) {
             case FLAG_DISMISS_START:
-                flingDividePosition(currentPosition, snapTarget.position,
+                flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
                         () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */,
                                 EXIT_REASON_DRAG_DIVIDER));
                 break;
             case FLAG_DISMISS_END:
-                flingDividePosition(currentPosition, snapTarget.position,
+                flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
                         () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */,
                                 EXIT_REASON_DRAG_DIVIDER));
                 break;
             default:
-                flingDividePosition(currentPosition, snapTarget.position,
+                flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
                         () -> setDividePosition(snapTarget.position, true /* applyLayoutChange */));
                 break;
         }
@@ -516,12 +543,20 @@
     public void flingDividerToDismiss(boolean toEnd, int reason) {
         final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position
                 : mDividerSnapAlgorithm.getDismissStartTarget().position;
-        flingDividePosition(getDividePosition(), target,
+        flingDividePosition(getDividePosition(), target, FLING_EXIT_DURATION,
                 () -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason));
     }
 
+    /** Fling divider from current position to center position. */
+    public void flingDividerToCenter() {
+        final int pos = mDividerSnapAlgorithm.getMiddleTarget().position;
+        flingDividePosition(getDividePosition(), pos, FLING_ENTER_DURATION,
+                () -> setDividePosition(pos, true /* applyLayoutChange */));
+    }
+
     @VisibleForTesting
-    void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) {
+    void flingDividePosition(int from, int to, int duration,
+            @Nullable Runnable flingFinishedCallback) {
         if (from == to) {
             // No animation run, still callback to stop resizing.
             mSplitLayoutHandler.onLayoutSizeChanged(this);
@@ -531,7 +566,7 @@
         }
         ValueAnimator animator = ValueAnimator
                 .ofInt(from, to)
-                .setDuration(FLING_ANIMATION_DURATION);
+                .setDuration(duration);
         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
         animator.addUpdateListener(
                 animation -> updateDivideBounds((int) animation.getAnimatedValue()));
@@ -588,7 +623,7 @@
 
         AnimatorSet set = new AnimatorSet();
         set.playTogether(animator1, animator2, animator3);
-        set.setDuration(FLING_ANIMATION_DURATION);
+        set.setDuration(FLING_SWITCH_DURATION);
         set.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
index 35a309a..0cc545a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
@@ -20,7 +20,6 @@
 import static android.os.Process.THREAD_PRIORITY_DISPLAY;
 import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST;
 
-import android.animation.AnimationHandler;
 import android.content.Context;
 import android.os.Build;
 import android.os.Handler;
@@ -31,11 +30,9 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.HandlerExecutor;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ChoreographerSfVsync;
 import com.android.wm.shell.common.annotations.ExternalMainThread;
 import com.android.wm.shell.common.annotations.ShellAnimationThread;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
@@ -195,30 +192,6 @@
     }
 
     /**
-     * Provide a Shell main-thread AnimationHandler.  The AnimationHandler can be set on
-     * {@link android.animation.ValueAnimator}s and will ensure that the animation will run on
-     * the Shell main-thread with the SF vsync.
-     */
-    @WMSingleton
-    @Provides
-    @ChoreographerSfVsync
-    public static AnimationHandler provideShellMainExecutorSfVsyncAnimationHandler(
-            @ShellMainThread ShellExecutor mainExecutor) {
-        try {
-            AnimationHandler handler = new AnimationHandler();
-            mainExecutor.executeBlocking(() -> {
-                // This is called on the animation thread since it calls
-                // Choreographer.getSfInstance() which returns a thread-local Choreographer instance
-                // that uses the SF vsync
-                handler.setProvider(new SfVsyncFrameCallbackProvider());
-            });
-            return handler;
-        } catch (InterruptedException e) {
-            throw new RuntimeException("Failed to initialize SfVsync animation handler in 1s", e);
-        }
-    }
-
-    /**
      * Provides a Shell background thread Handler for low priority background tasks.
      */
     @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 2ca9c3b..2bcc134 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
@@ -27,6 +27,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.RootDisplayAreaOrganizer;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.TaskViewTransitions;
@@ -48,6 +49,8 @@
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.desktopmode.DesktopModeConstants;
+import com.android.wm.shell.desktopmode.DesktopModeController;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.freeform.FreeformComponents;
 import com.android.wm.shell.freeform.FreeformTaskListener;
@@ -574,6 +577,27 @@
     }
 
     //
+    // Desktop mode (optional feature)
+    //
+
+    @WMSingleton
+    @Provides
+    static Optional<DesktopModeController> provideDesktopModeController(
+            Context context, ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
+            RootDisplayAreaOrganizer rootDisplayAreaOrganizer,
+            @ShellMainThread Handler mainHandler
+    ) {
+        if (DesktopModeConstants.IS_FEATURE_ENABLED) {
+            return Optional.of(new DesktopModeController(context, shellInit, shellTaskOrganizer,
+                    rootDisplayAreaOrganizer,
+                    mainHandler));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    //
     // Misc
     //
 
@@ -583,7 +607,8 @@
     @ShellCreateTriggerOverride
     @Provides
     static Object provideIndependentShellComponentsToCreate(
-            SplitscreenPipMixedHandler splitscreenPipMixedHandler) {
+            SplitscreenPipMixedHandler splitscreenPipMixedHandler,
+            Optional<DesktopModeController> desktopModeController) {
         return new Object();
     }
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeConstants.java
similarity index 61%
copy from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
copy to libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeConstants.java
index 8dde897..e62a63a 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeConstants.java
@@ -14,10 +14,18 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spaprivileged.framework.app
+package com.android.wm.shell.desktopmode;
 
-import android.content.pm.ApplicationInfo
+import android.os.SystemProperties;
 
-interface AppRecord {
-    val app: ApplicationInfo
+/**
+ * Constants for desktop mode feature
+ */
+public class DesktopModeConstants {
+
+    /**
+     * Flag to indicate whether desktop mode is available on the device
+     */
+    public static final boolean IS_FEATURE_ENABLED = SystemProperties.getBoolean(
+            "persist.wm.debug.desktop_mode", false);
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
new file mode 100644
index 0000000..5849e16
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -0,0 +1,137 @@
+/*
+ * 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.desktopmode;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.RootDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
+
+/**
+ * Handles windowing changes when desktop mode system setting changes
+ */
+public class DesktopModeController {
+
+    private final Context mContext;
+    private final ShellTaskOrganizer mShellTaskOrganizer;
+    private final RootDisplayAreaOrganizer mRootDisplayAreaOrganizer;
+    private final SettingsObserver mSettingsObserver;
+
+    public DesktopModeController(Context context, ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
+            RootDisplayAreaOrganizer rootDisplayAreaOrganizer,
+            @ShellMainThread Handler mainHandler) {
+        mContext = context;
+        mShellTaskOrganizer = shellTaskOrganizer;
+        mRootDisplayAreaOrganizer = rootDisplayAreaOrganizer;
+        mSettingsObserver = new SettingsObserver(mContext, mainHandler);
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopModeController");
+        mSettingsObserver.observe();
+    }
+
+    @VisibleForTesting
+    void updateDesktopModeEnabled(boolean enabled) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeState: enabled=%s", enabled);
+
+        int displayId = mContext.getDisplayId();
+
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        // Reset freeform windowing mode that is set per task level (tasks should inherit
+        // container value)
+        wct.merge(mShellTaskOrganizer.prepareClearFreeformForTasks(displayId), true /* transfer */);
+        int targetWindowingMode;
+        if (enabled) {
+            targetWindowingMode = WINDOWING_MODE_FREEFORM;
+        } else {
+            targetWindowingMode = WINDOWING_MODE_FULLSCREEN;
+            // Clear any resized bounds
+            wct.merge(mShellTaskOrganizer.prepareClearBoundsForTasks(displayId),
+                    true /* transfer */);
+        }
+        wct.merge(mRootDisplayAreaOrganizer.prepareWindowingModeChange(displayId,
+                targetWindowingMode), true /* transfer */);
+        mRootDisplayAreaOrganizer.applyTransaction(wct);
+    }
+
+    /**
+     * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE}
+     */
+    private final class SettingsObserver extends ContentObserver {
+
+        private final Uri mDesktopModeSetting = Settings.System.getUriFor(
+                Settings.System.DESKTOP_MODE);
+
+        private final Context mContext;
+
+        SettingsObserver(Context context, Handler handler) {
+            super(handler);
+            mContext = context;
+        }
+
+        public void observe() {
+            // TODO(b/242867463): listen for setting change for all users
+            mContext.getContentResolver().registerContentObserver(mDesktopModeSetting,
+                    false /* notifyForDescendants */, this /* observer */, UserHandle.USER_CURRENT);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, @Nullable Uri uri) {
+            if (mDesktopModeSetting.equals(uri)) {
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Received update for desktop mode setting");
+                desktopModeSettingChanged();
+            }
+        }
+
+        private void desktopModeSettingChanged() {
+            boolean enabled = isDesktopModeEnabled();
+            updateDesktopModeEnabled(enabled);
+        }
+
+        private boolean isDesktopModeEnabled() {
+            try {
+                int result = Settings.System.getIntForUser(mContext.getContentResolver(),
+                        Settings.System.DESKTOP_MODE, UserHandle.USER_CURRENT);
+                ProtoLog.d(WM_SHELL_DESKTOP_MODE, "isDesktopModeEnabled=%s", result);
+                return result != 0;
+            } catch (Settings.SettingNotFoundException e) {
+                ProtoLog.e(WM_SHELL_DESKTOP_MODE, "Failed to read DESKTOP_MODE setting %s", e);
+                return false;
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
index 504dc02..a0a8f9f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java
@@ -31,8 +31,6 @@
 import android.util.Size;
 import android.view.MotionEvent;
 import android.view.SurfaceControl;
-import android.view.SyncRtSurfaceTransactionApplier;
-import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
 import android.view.WindowManagerGlobal;
 
 import com.android.internal.protolog.common.ProtoLog;
@@ -42,6 +40,7 @@
 import com.android.wm.shell.pip.PipMediaController;
 import com.android.wm.shell.pip.PipMediaController.ActionListener;
 import com.android.wm.shell.pip.PipMenuController;
+import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
 import com.android.wm.shell.pip.PipUiEventLogger;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -115,6 +114,10 @@
     private final ShellExecutor mMainExecutor;
     private final Handler mMainHandler;
 
+    private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+            mSurfaceControlTransactionFactory;
+    private final float[] mTmpTransform = new float[9];
+
     private final ArrayList<Listener> mListeners = new ArrayList<>();
     private final SystemWindows mSystemWindows;
     private final Optional<SplitScreenController> mSplitScreenController;
@@ -124,7 +127,6 @@
     private RemoteAction mCloseAction;
     private List<RemoteAction> mMediaActions;
 
-    private SyncRtSurfaceTransactionApplier mApplier;
     private int mMenuState;
 
     private PipMenuView mPipMenuView;
@@ -150,6 +152,9 @@
         mMainHandler = mainHandler;
         mSplitScreenController = splitScreenOptional;
         mPipUiEventLogger = pipUiEventLogger;
+
+        mSurfaceControlTransactionFactory =
+                new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
     }
 
     public boolean isMenuVisible() {
@@ -194,7 +199,6 @@
             return;
         }
 
-        mApplier = null;
         mSystemWindows.removeView(mPipMenuView);
         mPipMenuView = null;
     }
@@ -289,7 +293,7 @@
                     willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, "    "));
         }
 
-        if (!maybeCreateSyncApplier()) {
+        if (!checkPipMenuState()) {
             return;
         }
 
@@ -312,7 +316,7 @@
             return;
         }
 
-        if (!maybeCreateSyncApplier()) {
+        if (!checkPipMenuState()) {
             return;
         }
 
@@ -328,18 +332,15 @@
         mTmpSourceRectF.set(mTmpSourceBounds);
         mTmpDestinationRectF.set(destinationBounds);
         mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
-        SurfaceControl surfaceControl = getSurfaceControl();
-        SurfaceParams params = new SurfaceParams.Builder(surfaceControl)
-                .withMatrix(mMoveTransform)
-                .build();
+        final SurfaceControl surfaceControl = getSurfaceControl();
+        final SurfaceControl.Transaction menuTx =
+                mSurfaceControlTransactionFactory.getTransaction();
+        menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform);
         if (pipLeash != null && t != null) {
-            SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash)
-                    .withMergeTransaction(t)
-                    .build();
-            mApplier.scheduleApply(params, pipParams);
-        } else {
-            mApplier.scheduleApply(params);
+            // Merge the two transactions, vsyncId has been set on menuTx.
+            menuTx.merge(t);
         }
+        menuTx.apply();
     }
 
     /**
@@ -353,36 +354,29 @@
             return;
         }
 
-        if (!maybeCreateSyncApplier()) {
+        if (!checkPipMenuState()) {
             return;
         }
 
-        SurfaceControl surfaceControl = getSurfaceControl();
-        SurfaceParams params = new SurfaceParams.Builder(surfaceControl)
-                .withWindowCrop(destinationBounds)
-                .build();
+        final SurfaceControl surfaceControl = getSurfaceControl();
+        final SurfaceControl.Transaction menuTx =
+                mSurfaceControlTransactionFactory.getTransaction();
+        menuTx.setCrop(surfaceControl, destinationBounds);
         if (pipLeash != null && t != null) {
-            SurfaceParams pipParams = new SurfaceParams.Builder(pipLeash)
-                    .withMergeTransaction(t)
-                    .build();
-            mApplier.scheduleApply(params, pipParams);
-        } else {
-            mApplier.scheduleApply(params);
+            // Merge the two transactions, vsyncId has been set on menuTx.
+            menuTx.merge(t);
         }
+        menuTx.apply();
     }
 
-    private boolean maybeCreateSyncApplier() {
+    private boolean checkPipMenuState() {
         if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: Not going to move PiP, either menu or its parent is not created.", TAG);
             return false;
         }
 
-        if (mApplier == null) {
-            mApplier = new SyncRtSurfaceTransactionApplier(mPipMenuView);
-        }
-
-        return mApplier != null;
+        return true;
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
index 0f3ff36..8e3376f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipInputConsumer.java
@@ -146,11 +146,8 @@
                     "%s: Failed to create input consumer, %s", TAG, e);
         }
         mMainExecutor.execute(() -> {
-            // Choreographer.getSfInstance() must be called on the thread that the input event
-            // receiver should be receiving events
-            // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions
             mInputEventReceiver = new InputEventReceiver(inputChannel,
-                Looper.myLooper(), Choreographer.getSfInstance());
+                Looper.myLooper(), Choreographer.getInstance());
             if (mRegistrationListener != null) {
                 mRegistrationListener.onRegistrationChanged(true /* isRegistered */);
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
index abf1a95..89d85e4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipResizeGestureHandler.java
@@ -625,8 +625,7 @@
 
     class PipResizeInputEventReceiver extends BatchedInputEventReceiver {
         PipResizeInputEventReceiver(InputChannel channel, Looper looper) {
-            // TODO(b/222697646): remove getSfInstance usage and use vsyncId for transactions
-            super(channel, looper, Choreographer.getSfInstance());
+            super(channel, looper, Choreographer.getInstance());
         }
 
         public void onInputEvent(InputEvent event) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
index 4d7c846..97e017a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
@@ -97,6 +97,9 @@
     private final ImageView mArrowLeft;
     private final TvWindowMenuActionButton mA11yDoneButton;
 
+    private final View mPipBackground;
+    private final View mDimLayer;
+
     private final ScrollView mScrollView;
     private final HorizontalScrollView mHorizontalScrollView;
     private View mFocusedButton;
@@ -148,6 +151,9 @@
         mExpandButton = findViewById(R.id.tv_pip_menu_expand_button);
         mExpandButton.setOnClickListener(this);
 
+        mPipBackground = findViewById(R.id.tv_pip_menu_background);
+        mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer);
+
         mScrollView = findViewById(R.id.tv_pip_menu_scroll);
         mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll);
 
@@ -231,7 +237,7 @@
                     mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(),
                     finishBounds.width() / (float) finishBounds.height());
             if (ratioChanged) {
-                mPipView.animate()
+                mPipBackground.animate()
                         .alpha(1f)
                         .setInterpolator(TvPipInterpolators.EXIT)
                         .setDuration(mResizeAnimationDuration / 2)
@@ -272,7 +278,7 @@
                 "%s: onPipTransitionFinished()", TAG);
 
         // Fade in content by fading out view on top.
-        mPipView.animate()
+        mPipBackground.animate()
                 .alpha(0f)
                 .setDuration(mResizeAnimationDuration / 2)
                 .setInterpolator(TvPipInterpolators.ENTER)
@@ -770,6 +776,7 @@
             refocusPreviousButton();
         }
         animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
+        animateAlphaTo(show ? 1 : 0, mDimLayer);
     }
 
     private void setFrameHighlighted(boolean highlighted) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index b296151..93c7529 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -46,6 +46,8 @@
             Consts.TAG_WM_SHELL),
     WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
+    WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+            Consts.TAG_WM_SHELL),
     TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest");
 
     private final boolean mEnabled;
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 7e83d2f..21fc01e 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
@@ -25,6 +25,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
@@ -488,13 +489,6 @@
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct);
 
-        // If split still not active, apply windows bounds first to avoid surface reset to
-        // wrong pos by SurfaceAnimator from wms.
-        // TODO(b/223325631): check  is it still necessary after improve enter transition done.
-        if (!mMainStage.isActive()) {
-            updateWindowBounds(mSplitLayout, wct);
-        }
-
         wct.sendPendingIntent(intent, fillInIntent, options);
         mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
     }
@@ -641,7 +635,7 @@
             wct.startTask(sideTaskId, sideOptions);
         }
         // Using legacy transitions, so we can't use blast sync since it conflicts.
-        mTaskOrganizer.applyTransaction(wct);
+        mSyncQueue.queue(wct);
         mSyncQueue.runInSync(t -> {
             setDividerVisibility(true, t);
             updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
@@ -893,10 +887,13 @@
         mShouldUpdateRecents = false;
         mIsDividerRemoteAnimating = false;
 
+        mSplitLayout.getInvisibleBounds(mTempRect1);
         if (childrenToTop == null) {
             mSideStage.removeAllTasks(wct, false /* toTop */);
             mMainStage.deactivate(wct, false /* toTop */);
             wct.reorder(mRootTaskInfo.token, false /* onTop */);
+            wct.setForceTranslucent(mRootTaskInfo.token, true);
+            wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
             onTransitionAnimationComplete();
         } else {
             // Expand to top side split as full screen for fading out decor animation and dismiss
@@ -907,27 +904,32 @@
                     ? mSideStage : mMainStage;
             tempFullStage.resetBounds(wct);
             wct.setSmallestScreenWidthDp(tempFullStage.mRootTaskInfo.token,
-                    mRootTaskInfo.configuration.smallestScreenWidthDp);
+                    SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
             dismissStage.dismiss(wct, false /* toTop */);
         }
         mSyncQueue.queue(wct);
         mSyncQueue.runInSync(t -> {
             t.setWindowCrop(mMainStage.mRootLeash, null)
                     .setWindowCrop(mSideStage.mRootLeash, null);
-            t.setPosition(mMainStage.mRootLeash, 0, 0)
-                    .setPosition(mSideStage.mRootLeash, 0, 0);
             t.hide(mMainStage.mDimLayer).hide(mSideStage.mDimLayer);
             setDividerVisibility(false, t);
 
-            // In this case, exit still under progress, fade out the split decor after first WCT
-            // done and do remaining WCT after animation finished.
-            if (childrenToTop != null) {
+            if (childrenToTop == null) {
+                t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right);
+            } else {
+                // In this case, exit still under progress, fade out the split decor after first WCT
+                // done and do remaining WCT after animation finished.
                 childrenToTop.fadeOutDecor(() -> {
                     WindowContainerTransaction finishedWCT = new WindowContainerTransaction();
                     mIsExiting = false;
                     childrenToTop.dismiss(finishedWCT, true /* toTop */);
                     finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */);
-                    mTaskOrganizer.applyTransaction(finishedWCT);
+                    finishedWCT.setForceTranslucent(mRootTaskInfo.token, true);
+                    finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
+                    mSyncQueue.queue(finishedWCT);
+                    mSyncQueue.runInSync(at -> {
+                        at.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right);
+                    });
                     onTransitionAnimationComplete();
                 });
             }
@@ -996,6 +998,7 @@
         mMainStage.activate(wct, true /* includingTopTask */);
         updateWindowBounds(mSplitLayout, wct);
         wct.reorder(mRootTaskInfo.token, true);
+        wct.setForceTranslucent(mRootTaskInfo.token, false);
     }
 
     void finishEnterSplitScreen(SurfaceControl.Transaction t) {
@@ -1221,7 +1224,13 @@
         // Make the stages adjacent to each other so they occlude what's behind them.
         wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
         wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token);
-        mTaskOrganizer.applyTransaction(wct);
+        wct.setForceTranslucent(mRootTaskInfo.token, true);
+        mSplitLayout.getInvisibleBounds(mTempRect1);
+        wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
+        mSyncQueue.queue(wct);
+        mSyncQueue.runInSync(t -> {
+            t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top);
+        });
     }
 
     private void onRootTaskVanished() {
@@ -1377,10 +1386,17 @@
             // TODO (b/238697912) : Add the validation to prevent entering non-recovered status
             final WindowContainerTransaction wct = new WindowContainerTransaction();
             mSplitLayout.init();
-            prepareEnterSplitScreen(wct);
+            mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+            mMainStage.activate(wct, true /* includingTopTask */);
+            updateWindowBounds(mSplitLayout, wct);
+            wct.reorder(mRootTaskInfo.token, true);
+            wct.setForceTranslucent(mRootTaskInfo.token, false);
             mSyncQueue.queue(wct);
-            mSyncQueue.runInSync(t ->
-                    updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */));
+            mSyncQueue.runInSync(t -> {
+                updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
+
+                mSplitLayout.flingDividerToCenter();
+            });
         }
         if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) {
             mShouldUpdateRecents = true;
@@ -1822,6 +1838,7 @@
             // properly for the animation itself.
             mSplitLayout.release();
             mSplitLayout.resetDividerPosition();
+            mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
             mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 45b69f1..6388ca1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -86,8 +86,6 @@
     private final float[] mTmpFloats = new float[9];
     /** The leash of the changing window container. */
     private final SurfaceControl mSurfaceControl;
-    private final Rect mStartBounds = new Rect();
-    private final Rect mEndBounds = new Rect();
 
     private final int mAnimHint;
     private final int mStartWidth;
@@ -105,8 +103,7 @@
      */
     private SurfaceControl mBackColorSurface;
     /** The leash using to animate screenshot layer. */
-    private SurfaceControl mAnimLeash;
-    private Transaction mTransaction;
+    private final SurfaceControl mAnimLeash;
 
     // The current active animation to move from the old to the new rotated
     // state.  Which animation is run here will depend on the old and new
@@ -134,9 +131,6 @@
         mStartRotation = change.getStartRotation();
         mEndRotation = change.getEndRotation();
 
-        mStartBounds.set(change.getStartAbsBounds());
-        mEndBounds.set(change.getEndAbsBounds());
-
         mAnimLeash = new SurfaceControl.Builder(session)
                 .setParent(rootLeash)
                 .setEffectLayer()
@@ -169,6 +163,8 @@
 
             t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE);
             t.show(mAnimLeash);
+            // Crop the real content in case it contains a larger child layer, e.g. wallpaper.
+            t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight));
 
             final ColorSpace colorSpace = screenshotBuffer.getColorSpace();
             final HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
@@ -306,7 +302,6 @@
         mRotateEnterAnimation.restrictDuration(MAX_ANIMATION_DURATION);
         mRotateEnterAnimation.scaleCurrentDuration(animationScale);
 
-        mTransaction = mTransactionPool.acquire();
         if (customRotate) {
             mRotateAlphaAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight);
             mRotateAlphaAnimation.restrictDuration(MAX_ANIMATION_DURATION);
@@ -386,22 +381,16 @@
     }
 
     public void kill() {
-        Transaction t = mTransaction != null ? mTransaction : mTransactionPool.acquire();
+        final Transaction t = mTransactionPool.acquire();
         if (mAnimLeash.isValid()) {
             t.remove(mAnimLeash);
         }
 
-        if (mScreenshotLayer != null) {
-            if (mScreenshotLayer.isValid()) {
-                t.remove(mScreenshotLayer);
-            }
-            mScreenshotLayer = null;
+        if (mScreenshotLayer != null && mScreenshotLayer.isValid()) {
+            t.remove(mScreenshotLayer);
         }
-        if (mBackColorSurface != null) {
-            if (mBackColorSurface.isValid()) {
-                t.remove(mBackColorSurface);
-            }
-            mBackColorSurface = null;
+        if (mBackColorSurface != null && mBackColorSurface.isValid()) {
+            t.remove(mBackColorSurface);
         }
         t.apply();
         mTransactionPool.release(t);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 9335438..26d0ec6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -23,6 +23,7 @@
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.view.WindowManager.fixScale;
 import static android.window.TransitionInfo.FLAG_IS_INPUT_METHOD;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
@@ -167,10 +168,7 @@
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
 
         ContentResolver resolver = mContext.getContentResolver();
-        mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver,
-                Settings.Global.TRANSITION_ANIMATION_SCALE,
-                mContext.getResources().getFloat(
-                        R.dimen.config_appTransitionAnimationDurationScaleDefault));
+        mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
         dispatchAnimScaleSetting(mTransitionAnimationScaleSetting);
 
         resolver.registerContentObserver(
@@ -185,6 +183,12 @@
         }
     }
 
+    private float getTransitionAnimationScaleSetting() {
+        return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
+                                R.dimen.config_appTransitionAnimationDurationScaleDefault)));
+    }
+
     public ShellTransitions asRemoteTransitions() {
         return mImpl;
     }
@@ -963,9 +967,7 @@
         @Override
         public void onChange(boolean selfChange) {
             super.onChange(selfChange);
-            mTransitionAnimationScaleSetting = Settings.Global.getFloat(
-                    mContext.getContentResolver(), Settings.Global.TRANSITION_ANIMATION_SCALE,
-                    mTransitionAnimationScaleSetting);
+            mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
 
             mMainExecutor.execute(() -> dispatchAnimScaleSetting(mTransitionAnimationScaleSetting));
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 98b5ee9..dc3deb1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -34,6 +34,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.desktopmode.DesktopModeConstants;
 
 /**
  * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
@@ -163,7 +164,13 @@
         View caption = mResult.mRootView.findViewById(R.id.caption);
         caption.setOnTouchListener(mOnCaptionTouchListener);
         View maximize = caption.findViewById(R.id.maximize_window);
-        maximize.setOnClickListener(mOnCaptionButtonClickListener);
+        if (DesktopModeConstants.IS_FEATURE_ENABLED) {
+            // Hide maximize button when desktop mode is available
+            maximize.setVisibility(View.GONE);
+        } else {
+            maximize.setVisibility(View.VISIBLE);
+            maximize.setOnClickListener(mOnCaptionButtonClickListener);
+        }
         View close = caption.findViewById(R.id.close_window);
         close.setOnClickListener(mOnCaptionButtonClickListener);
         View minimize = caption.findViewById(R.id.minimize_window);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 506a4c0..5e64a06 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -248,7 +248,7 @@
         lp.setTrustedOverlay();
         if (mViewHost == null) {
             mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay,
-                    mCaptionWindowManager, true);
+                    mCaptionWindowManager);
             mViewHost.setView(outResult.mRootView, lp);
         } else {
             mViewHost.relayout(lp);
@@ -345,9 +345,8 @@
     }
 
     interface SurfaceControlViewHostFactory {
-        default SurfaceControlViewHost create(
-                Context c, Display d, WindowlessWindowManager wmm, boolean useSfChoreographer) {
-            return new SurfaceControlViewHost(c, d, wmm, useSfChoreographer);
+        default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
+            return new SurfaceControlViewHost(c, d, wmm);
         }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index f865649..b29c436 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -16,9 +16,11 @@
 
 package com.android.wm.shell;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -30,6 +32,8 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeFalse;
@@ -38,9 +42,11 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.TaskInfo;
+import android.app.WindowConfiguration;
 import android.content.LocusId;
 import android.content.pm.ParceledListSlice;
 import android.os.Binder;
@@ -53,6 +59,8 @@
 import android.window.ITaskOrganizerController;
 import android.window.TaskAppearedInfo;
 import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransaction.Change;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -628,6 +636,71 @@
         verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token);
     }
 
+    @Test
+    public void testPrepareClearBoundsForTasks() {
+        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_UNDEFINED);
+        task1.displayId = 1;
+        MockToken token1 = new MockToken();
+        task1.token = token1.token();
+        mOrganizer.onTaskAppeared(task1, null);
+
+        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_UNDEFINED);
+        task2.displayId = 1;
+        MockToken token2 = new MockToken();
+        task2.token = token2.token();
+        mOrganizer.onTaskAppeared(task2, null);
+
+        RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_UNDEFINED);
+        otherDisplayTask.displayId = 2;
+        MockToken otherDisplayToken = new MockToken();
+        otherDisplayTask.token = otherDisplayToken.token();
+        mOrganizer.onTaskAppeared(otherDisplayTask, null);
+
+        WindowContainerTransaction wct = mOrganizer.prepareClearBoundsForTasks(1);
+
+        assertEquals(wct.getChanges().size(), 2);
+        Change boundsChange1 = wct.getChanges().get(token1.binder());
+        assertNotNull(boundsChange1);
+        assertNotEquals(
+                (boundsChange1.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
+        assertTrue(boundsChange1.getConfiguration().windowConfiguration.getBounds().isEmpty());
+
+        Change boundsChange2 = wct.getChanges().get(token2.binder());
+        assertNotNull(boundsChange2);
+        assertNotEquals(
+                (boundsChange2.getWindowSetMask() & WindowConfiguration.WINDOW_CONFIG_BOUNDS), 0);
+        assertTrue(boundsChange2.getConfiguration().windowConfiguration.getBounds().isEmpty());
+    }
+
+    @Test
+    public void testPrepareClearFreeformForTasks() {
+        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FREEFORM);
+        task1.displayId = 1;
+        MockToken token1 = new MockToken();
+        task1.token = token1.token();
+        mOrganizer.onTaskAppeared(task1, null);
+
+        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
+        task2.displayId = 1;
+        MockToken token2 = new MockToken();
+        task2.token = token2.token();
+        mOrganizer.onTaskAppeared(task2, null);
+
+        RunningTaskInfo otherDisplayTask = createTaskInfo(3, WINDOWING_MODE_FREEFORM);
+        otherDisplayTask.displayId = 2;
+        MockToken otherDisplayToken = new MockToken();
+        otherDisplayTask.token = otherDisplayToken.token();
+        mOrganizer.onTaskAppeared(otherDisplayTask, null);
+
+        WindowContainerTransaction wct = mOrganizer.prepareClearFreeformForTasks(1);
+
+        // Only task with freeform windowing mode and the right display should be updated
+        assertEquals(wct.getChanges().size(), 1);
+        Change wmModeChange1 = wct.getChanges().get(token1.binder());
+        assertNotNull(wmModeChange1);
+        assertEquals(wmModeChange1.getWindowingMode(), WINDOWING_MODE_UNDEFINED);
+    }
+
     private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
         RunningTaskInfo taskInfo = new RunningTaskInfo();
         taskInfo.taskId = taskId;
@@ -635,4 +708,22 @@
         return taskInfo;
     }
 
+    private static class MockToken {
+        private final WindowContainerToken mToken;
+        private final IBinder mBinder;
+
+        MockToken() {
+            mToken = mock(WindowContainerToken.class);
+            mBinder = mock(IBinder.class);
+            when(mToken.asBinder()).thenReturn(mBinder);
+        }
+
+        WindowContainerToken token() {
+            return mToken;
+        }
+
+        IBinder binder() {
+            return mBinder;
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index 95725bb..695550d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -159,7 +159,8 @@
     }
 
     private void waitDividerFlingFinished() {
-        verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), mRunnableCaptor.capture());
+        verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), anyInt(),
+                mRunnableCaptor.capture());
         mRunnableCaptor.getValue().run();
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
new file mode 100644
index 0000000..58f20da
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.desktopmode;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.WindowConfiguration;
+import android.os.Handler;
+import android.os.IBinder;
+import android.testing.AndroidTestingRunner;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import android.window.WindowContainerTransaction.Change;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.RootDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DesktopModeControllerTest extends ShellTestCase {
+
+    @Mock
+    private ShellTaskOrganizer mShellTaskOrganizer;
+    @Mock
+    private RootDisplayAreaOrganizer mRootDisplayAreaOrganizer;
+    @Mock
+    private ShellExecutor mTestExecutor;
+    @Mock
+    private Handler mMockHandler;
+
+    private DesktopModeController mController;
+    private ShellInit mShellInit;
+
+    @Before
+    public void setUp() {
+        mShellInit = Mockito.spy(new ShellInit(mTestExecutor));
+
+        mController = new DesktopModeController(mContext, mShellInit, mShellTaskOrganizer,
+                mRootDisplayAreaOrganizer, mMockHandler);
+
+        mShellInit.init();
+    }
+
+    @Test
+    public void instantiate_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
+    public void testDesktopModeEnabled_taskWmClearedDisplaySetToFreeform() {
+        // Create a fake WCT to simulate setting task windowing mode to undefined
+        WindowContainerTransaction taskWct = new WindowContainerTransaction();
+        MockToken taskMockToken = new MockToken();
+        taskWct.setWindowingMode(taskMockToken.token(), WINDOWING_MODE_UNDEFINED);
+        when(mShellTaskOrganizer.prepareClearFreeformForTasks(mContext.getDisplayId())).thenReturn(
+                taskWct);
+
+        // Create a fake WCT to simulate setting display windowing mode to freeform
+        WindowContainerTransaction displayWct = new WindowContainerTransaction();
+        MockToken displayMockToken = new MockToken();
+        displayWct.setWindowingMode(displayMockToken.token(), WINDOWING_MODE_FREEFORM);
+        when(mRootDisplayAreaOrganizer.prepareWindowingModeChange(mContext.getDisplayId(),
+                WINDOWING_MODE_FREEFORM)).thenReturn(displayWct);
+
+        // The test
+        mController.updateDesktopModeEnabled(true);
+
+        ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+                WindowContainerTransaction.class);
+        verify(mRootDisplayAreaOrganizer).applyTransaction(arg.capture());
+
+        // WCT should have 2 changes - clear task wm mode and set display wm mode
+        WindowContainerTransaction wct = arg.getValue();
+        assertThat(wct.getChanges()).hasSize(2);
+
+        // Verify executed WCT has a change for setting task windowing mode to undefined
+        Change taskWmModeChange = wct.getChanges().get(taskMockToken.binder());
+        assertThat(taskWmModeChange).isNotNull();
+        assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+
+        // Verify executed WCT has a change for setting display windowing mode to freeform
+        Change displayWmModeChange = wct.getChanges().get(displayMockToken.binder());
+        assertThat(displayWmModeChange).isNotNull();
+        assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FREEFORM);
+    }
+
+    @Test
+    public void testDesktopModeDisabled_taskWmAndBoundsClearedDisplaySetToFullscreen() {
+        // Create a fake WCT to simulate setting task windowing mode to undefined
+        WindowContainerTransaction taskWmWct = new WindowContainerTransaction();
+        MockToken taskWmMockToken = new MockToken();
+        taskWmWct.setWindowingMode(taskWmMockToken.token(), WINDOWING_MODE_UNDEFINED);
+        when(mShellTaskOrganizer.prepareClearFreeformForTasks(mContext.getDisplayId())).thenReturn(
+                taskWmWct);
+
+        // Create a fake WCT to simulate clearing task bounds
+        WindowContainerTransaction taskBoundsWct = new WindowContainerTransaction();
+        MockToken taskBoundsMockToken = new MockToken();
+        taskBoundsWct.setBounds(taskBoundsMockToken.token(), null);
+        when(mShellTaskOrganizer.prepareClearBoundsForTasks(mContext.getDisplayId())).thenReturn(
+                taskBoundsWct);
+
+        // Create a fake WCT to simulate setting display windowing mode to fullscreen
+        WindowContainerTransaction displayWct = new WindowContainerTransaction();
+        MockToken displayMockToken = new MockToken();
+        displayWct.setWindowingMode(displayMockToken.token(), WINDOWING_MODE_FULLSCREEN);
+        when(mRootDisplayAreaOrganizer.prepareWindowingModeChange(mContext.getDisplayId(),
+                WINDOWING_MODE_FULLSCREEN)).thenReturn(displayWct);
+
+        // The test
+        mController.updateDesktopModeEnabled(false);
+
+        ArgumentCaptor<WindowContainerTransaction> arg = ArgumentCaptor.forClass(
+                WindowContainerTransaction.class);
+        verify(mRootDisplayAreaOrganizer).applyTransaction(arg.capture());
+
+        // WCT should have 3 changes - clear task wm mode and bounds and set display wm mode
+        WindowContainerTransaction wct = arg.getValue();
+        assertThat(wct.getChanges()).hasSize(3);
+
+        // Verify executed WCT has a change for setting task windowing mode to undefined
+        Change taskWmModeChange = wct.getChanges().get(taskWmMockToken.binder());
+        assertThat(taskWmModeChange).isNotNull();
+        assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+
+        // Verify executed WCT has a change for clearing task bounds
+        Change taskBoundsChange = wct.getChanges().get(taskBoundsMockToken.binder());
+        assertThat(taskBoundsChange).isNotNull();
+        assertThat(taskBoundsChange.getWindowSetMask()
+                & WindowConfiguration.WINDOW_CONFIG_BOUNDS).isNotEqualTo(0);
+        assertThat(taskBoundsChange.getConfiguration().windowConfiguration.getBounds().isEmpty())
+                .isTrue();
+
+        // Verify executed WCT has a change for setting display windowing mode to fullscreen
+        Change displayWmModeChange = wct.getChanges().get(displayMockToken.binder());
+        assertThat(displayWmModeChange).isNotNull();
+        assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN);
+    }
+
+    private static class MockToken {
+        private final WindowContainerToken mToken;
+        private final IBinder mBinder;
+
+        MockToken() {
+            mToken = mock(WindowContainerToken.class);
+            mBinder = mock(IBinder.class);
+            when(mToken.asBinder()).thenReturn(mBinder);
+        }
+
+        WindowContainerToken token() {
+            return mToken;
+        }
+
+        IBinder binder() {
+            return mBinder;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 226843e..e11be31 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -24,7 +24,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
@@ -107,7 +106,7 @@
         mMockSurfaceControlFinishT = createMockSurfaceControlTransaction();
 
         doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory)
-                .create(any(), any(), any(), anyBoolean());
+                .create(any(), any(), any());
     }
 
     @Test
@@ -148,8 +147,7 @@
 
         verify(decorContainerSurfaceBuilder, never()).build();
         verify(taskBackgroundSurfaceBuilder, never()).build();
-        verify(mMockSurfaceControlViewHostFactory, never())
-                .create(any(), any(), any(), anyBoolean());
+        verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any());
 
         verify(mMockSurfaceControlFinishT).hide(taskSurface);
 
@@ -207,8 +205,7 @@
         verify(mMockSurfaceControlStartT).setLayer(taskBackgroundSurface, -1);
         verify(mMockSurfaceControlStartT).show(taskBackgroundSurface);
 
-        verify(mMockSurfaceControlViewHostFactory)
-                .create(any(), eq(defaultDisplay), any(), anyBoolean());
+        verify(mMockSurfaceControlViewHostFactory).create(any(), eq(defaultDisplay), any());
         verify(mMockSurfaceControlViewHost)
                 .setView(same(mMockView),
                         argThat(lp -> lp.height == 64
@@ -326,8 +323,7 @@
         verify(mMockDisplayController).removeDisplayWindowListener(same(listener));
 
         assertThat(mRelayoutResult.mRootView).isSameInstanceAs(mMockView);
-        verify(mMockSurfaceControlViewHostFactory)
-                .create(any(), eq(mockDisplay), any(), anyBoolean());
+        verify(mMockSurfaceControlViewHostFactory).create(any(), eq(mockDisplay), any());
         verify(mMockSurfaceControlViewHost).setView(same(mMockView), any());
     }
 
diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp
index 9a4bda2..3c67edc 100644
--- a/libs/hwui/hwui/Typeface.cpp
+++ b/libs/hwui/hwui/Typeface.cpp
@@ -125,9 +125,14 @@
 }
 
 Typeface* Typeface::createFromFamilies(std::vector<std::shared_ptr<minikin::FontFamily>>&& families,
-                                       int weight, int italic) {
+                                       int weight, int italic, const Typeface* fallback) {
     Typeface* result = new Typeface;
-    result->fFontCollection = minikin::FontCollection::create(families);
+    if (fallback == nullptr) {
+        result->fFontCollection = minikin::FontCollection::create(std::move(families));
+    } else {
+        result->fFontCollection =
+                fallback->fFontCollection->createCollectionWithFamilies(std::move(families));
+    }
 
     if (weight == RESOLVE_BY_FONT_TABLE || italic == RESOLVE_BY_FONT_TABLE) {
         int weightFromFont;
diff --git a/libs/hwui/hwui/Typeface.h b/libs/hwui/hwui/Typeface.h
index 0c3ef01..565136e 100644
--- a/libs/hwui/hwui/Typeface.h
+++ b/libs/hwui/hwui/Typeface.h
@@ -78,7 +78,8 @@
             Typeface* src, const std::vector<minikin::FontVariation>& variations);
 
     static Typeface* createFromFamilies(
-            std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic);
+            std::vector<std::shared_ptr<minikin::FontFamily>>&& families, int weight, int italic,
+            const Typeface* fallback);
 
     static void setDefault(const Typeface* face);
 
diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp
index f5ed568..209b35c 100644
--- a/libs/hwui/jni/Typeface.cpp
+++ b/libs/hwui/jni/Typeface.cpp
@@ -109,27 +109,14 @@
 static jlong Typeface_createFromArray(JNIEnv *env, jobject, jlongArray familyArray,
                                       jlong fallbackPtr, int weight, int italic) {
     ScopedLongArrayRO families(env, familyArray);
-    std::vector<std::shared_ptr<minikin::FontFamily>> familyVec;
     Typeface* typeface = (fallbackPtr == 0) ? nullptr : toTypeface(fallbackPtr);
-    if (typeface != nullptr) {
-        const std::shared_ptr<minikin::FontCollection>& fallbackCollection =
-                toTypeface(fallbackPtr)->fFontCollection;
-        familyVec.reserve(families.size() + fallbackCollection->getFamilyCount());
-        for (size_t i = 0; i < families.size(); i++) {
-            FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]);
-            familyVec.emplace_back(family->family);
-        }
-        for (size_t i = 0; i < fallbackCollection->getFamilyCount(); i++) {
-            familyVec.emplace_back(fallbackCollection->getFamilyAt(i));
-        }
-    } else {
-        familyVec.reserve(families.size());
-        for (size_t i = 0; i < families.size(); i++) {
-            FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]);
-            familyVec.emplace_back(family->family);
-        }
+    std::vector<std::shared_ptr<minikin::FontFamily>> familyVec;
+    familyVec.reserve(families.size());
+    for (size_t i = 0; i < families.size(); i++) {
+        FontFamilyWrapper* family = reinterpret_cast<FontFamilyWrapper*>(families[i]);
+        familyVec.emplace_back(family->family);
     }
-    return toJLong(Typeface::createFromFamilies(std::move(familyVec), weight, italic));
+    return toJLong(Typeface::createFromFamilies(std::move(familyVec), weight, italic, typeface));
 }
 
 // CriticalNative
diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h
index 81bcb4e..c5196ee 100644
--- a/libs/hwui/renderthread/VulkanManager.h
+++ b/libs/hwui/renderthread/VulkanManager.h
@@ -54,8 +54,6 @@
 #include <SkColorSpace.h>
 #include <SkRefCnt.h>
 
-class GrVkExtensions;
-
 namespace android {
 namespace uirenderer {
 namespace renderthread {
diff --git a/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp b/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp
index a0bc5aa..2aeb42c 100644
--- a/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/SimpleColorMatrixAnimation.cpp
@@ -16,7 +16,8 @@
 
 #include "TestSceneBase.h"
 
-#include <SkColorMatrixFilter.h>
+#include <SkColorFilter.h>
+#include <SkColorMatrix.h>
 #include <SkGradientShader.h>
 
 class SimpleColorMatrixAnimation;
diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp
index 25cc8ca..499afa0 100644
--- a/libs/hwui/tests/unit/TypefaceTests.cpp
+++ b/libs/hwui/tests/unit/TypefaceTests.cpp
@@ -73,7 +73,8 @@
 
 TEST(TypefaceTest, resolveDefault_and_setDefaultTest) {
     std::unique_ptr<Typeface> regular(Typeface::createFromFamilies(
-            makeSingleFamlyVector(kRobotoVariable), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+            makeSingleFamlyVector(kRobotoVariable), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
+            nullptr /* fallback */));
     EXPECT_EQ(regular.get(), Typeface::resolveDefault(regular.get()));
 
     // Keep the original to restore it later.
@@ -351,24 +352,24 @@
 TEST(TypefaceTest, createFromFamilies_Single) {
     // In Java, new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(false).build();
-    std::unique_ptr<Typeface> regular(
-            Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 400, false));
+    std::unique_ptr<Typeface> regular(Typeface::createFromFamilies(
+            makeSingleFamlyVector(kRobotoVariable), 400, false, nullptr /* fallback */));
     EXPECT_EQ(400, regular->fStyle.weight());
     EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
     EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
 
     // In Java, new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(false).build();
-    std::unique_ptr<Typeface> bold(
-            Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 700, false));
+    std::unique_ptr<Typeface> bold(Typeface::createFromFamilies(
+            makeSingleFamlyVector(kRobotoVariable), 700, false, nullptr /* fallback */));
     EXPECT_EQ(700, bold->fStyle.weight());
     EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
     EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
 
     // In Java, new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(400).setItalic(true).build();
-    std::unique_ptr<Typeface> italic(
-            Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 400, true));
+    std::unique_ptr<Typeface> italic(Typeface::createFromFamilies(
+            makeSingleFamlyVector(kRobotoVariable), 400, true, nullptr /* fallback */));
     EXPECT_EQ(400, italic->fStyle.weight());
     EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
     EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
@@ -376,8 +377,8 @@
     // In Java,
     // new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(700).setItalic(true).build();
-    std::unique_ptr<Typeface> boldItalic(
-            Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 700, true));
+    std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies(
+            makeSingleFamlyVector(kRobotoVariable), 700, true, nullptr /* fallback */));
     EXPECT_EQ(700, boldItalic->fStyle.weight());
     EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
     EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
@@ -385,8 +386,8 @@
     // In Java,
     // new
     // Typeface.Builder("Roboto-Regular.ttf").setWeight(1100).setItalic(false).build();
-    std::unique_ptr<Typeface> over1000(
-            Typeface::createFromFamilies(makeSingleFamlyVector(kRobotoVariable), 1100, false));
+    std::unique_ptr<Typeface> over1000(Typeface::createFromFamilies(
+            makeSingleFamlyVector(kRobotoVariable), 1100, false, nullptr /* fallback */));
     EXPECT_EQ(1000, over1000->fStyle.weight());
     EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, over1000->fStyle.slant());
     EXPECT_EQ(Typeface::kBold, over1000->fAPIStyle);
@@ -394,30 +395,33 @@
 
 TEST(TypefaceTest, createFromFamilies_Single_resolveByTable) {
     // In Java, new Typeface.Builder("Family-Regular.ttf").build();
-    std::unique_ptr<Typeface> regular(Typeface::createFromFamilies(
-            makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+    std::unique_ptr<Typeface> regular(
+            Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE,
+                                         RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
     EXPECT_EQ(400, regular->fStyle.weight());
     EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
     EXPECT_EQ(Typeface::kNormal, regular->fAPIStyle);
 
     // In Java, new Typeface.Builder("Family-Bold.ttf").build();
-    std::unique_ptr<Typeface> bold(Typeface::createFromFamilies(
-            makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+    std::unique_ptr<Typeface> bold(
+            Typeface::createFromFamilies(makeSingleFamlyVector(kBoldFont), RESOLVE_BY_FONT_TABLE,
+                                         RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
     EXPECT_EQ(700, bold->fStyle.weight());
     EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, bold->fStyle.slant());
     EXPECT_EQ(Typeface::kBold, bold->fAPIStyle);
 
     // In Java, new Typeface.Builder("Family-Italic.ttf").build();
-    std::unique_ptr<Typeface> italic(Typeface::createFromFamilies(
-            makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+    std::unique_ptr<Typeface> italic(
+            Typeface::createFromFamilies(makeSingleFamlyVector(kItalicFont), RESOLVE_BY_FONT_TABLE,
+                                         RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
     EXPECT_EQ(400, italic->fStyle.weight());
     EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, italic->fStyle.slant());
     EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
 
     // In Java, new Typeface.Builder("Family-BoldItalic.ttf").build();
-    std::unique_ptr<Typeface> boldItalic(
-            Typeface::createFromFamilies(makeSingleFamlyVector(kBoldItalicFont),
-                                         RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+    std::unique_ptr<Typeface> boldItalic(Typeface::createFromFamilies(
+            makeSingleFamlyVector(kBoldItalicFont), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE,
+            nullptr /* fallback */));
     EXPECT_EQ(700, boldItalic->fStyle.weight());
     EXPECT_EQ(minikin::FontStyle::Slant::ITALIC, boldItalic->fStyle.slant());
     EXPECT_EQ(Typeface::kItalic, italic->fAPIStyle);
@@ -427,8 +431,9 @@
     std::vector<std::shared_ptr<minikin::FontFamily>> families = {
             buildFamily(kRegularFont), buildFamily(kBoldFont), buildFamily(kItalicFont),
             buildFamily(kBoldItalicFont)};
-    std::unique_ptr<Typeface> typeface(Typeface::createFromFamilies(
-            std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+    std::unique_ptr<Typeface> typeface(
+            Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE,
+                                         RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
     EXPECT_EQ(400, typeface->fStyle.weight());
     EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant());
 }
@@ -436,10 +441,24 @@
 TEST(TypefaceTest, createFromFamilies_Family_withoutRegular) {
     std::vector<std::shared_ptr<minikin::FontFamily>> families = {
             buildFamily(kBoldFont), buildFamily(kItalicFont), buildFamily(kBoldItalicFont)};
-    std::unique_ptr<Typeface> typeface(Typeface::createFromFamilies(
-            std::move(families), RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE));
+    std::unique_ptr<Typeface> typeface(
+            Typeface::createFromFamilies(std::move(families), RESOLVE_BY_FONT_TABLE,
+                                         RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
     EXPECT_EQ(700, typeface->fStyle.weight());
     EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, typeface->fStyle.slant());
 }
 
+TEST(TypefaceTest, createFromFamilies_Family_withFallback) {
+    std::vector<std::shared_ptr<minikin::FontFamily>> fallbackFamilies = {
+            buildFamily(kBoldFont), buildFamily(kItalicFont), buildFamily(kBoldItalicFont)};
+    std::unique_ptr<Typeface> fallback(
+            Typeface::createFromFamilies(std::move(fallbackFamilies), RESOLVE_BY_FONT_TABLE,
+                                         RESOLVE_BY_FONT_TABLE, nullptr /* fallback */));
+    std::unique_ptr<Typeface> regular(
+            Typeface::createFromFamilies(makeSingleFamlyVector(kRegularFont), RESOLVE_BY_FONT_TABLE,
+                                         RESOLVE_BY_FONT_TABLE, fallback.get()));
+    EXPECT_EQ(400, regular->fStyle.weight());
+    EXPECT_EQ(minikin::FontStyle::Slant::UPRIGHT, regular->fStyle.slant());
+}
+
 }  // namespace
diff --git a/location/java/android/location/util/identity/CallerIdentity.java b/location/java/android/location/util/identity/CallerIdentity.java
index ade0ea4..2f8d92b 100644
--- a/location/java/android/location/util/identity/CallerIdentity.java
+++ b/location/java/android/location/util/identity/CallerIdentity.java
@@ -124,15 +124,22 @@
                 packageName, attributionTag, listenerId);
     }
 
+    // in some tests these constants are loaded too early leading to an "incorrect" view of the
+    // current pid and uid. load lazily to prevent this problem in tests.
+    private static class Loader {
+        private static final int MY_UID = Process.myUid();
+        private static final int MY_PID = Process.myPid();
+    }
+
     private final int mUid;
 
     private final int mPid;
 
     private final String mPackageName;
 
-    private final @Nullable String mAttributionTag;
+    @Nullable private final String mAttributionTag;
 
-    private final @Nullable String mListenerId;
+    @Nullable private final String mListenerId;
 
     private CallerIdentity(int uid, int pid, String packageName,
             @Nullable String attributionTag, @Nullable String listenerId) {
@@ -181,6 +188,24 @@
         return mUid == Process.SYSTEM_UID;
     }
 
+    /** Returns true if this identity represents the same user this code is running in. */
+    public boolean isMyUser() {
+        return UserHandle.getUserId(mUid) == UserHandle.getUserId(Loader.MY_UID);
+    }
+
+    /** Returns true if this identity represents the same uid this code is running in. */
+    public boolean isMyUid() {
+        return mUid == Loader.MY_UID;
+    }
+
+    /**
+     * Returns true if this identity represents the same process this code is running in. Returns
+     * false if the identity process is unknown.
+     */
+    public boolean isMyProcess() {
+        return mPid == Loader.MY_PID;
+    }
+
     /**
      * Adds this identity to the worksource supplied, or if not worksource is supplied, creates a
      * new worksource representing this identity.
diff --git a/media/Android.bp b/media/Android.bp
index d28a21c..ec243bf 100644
--- a/media/Android.bp
+++ b/media/Android.bp
@@ -113,7 +113,7 @@
             min_sdk_version: "29",
             apex_available: [
                 "//apex_available:platform",
-                "com.android.bluetooth",
+                "com.android.btservices",
             ],
         },
     },
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index c666140..650f360 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -1299,8 +1299,8 @@
     /** @hide */ public static final String DEVICE_OUT_REMOTE_SUBMIX_NAME = "remote_submix";
     /** @hide */ public static final String DEVICE_OUT_TELEPHONY_TX_NAME = "telephony_tx";
     /** @hide */ public static final String DEVICE_OUT_LINE_NAME = "line";
-    /** @hide */ public static final String DEVICE_OUT_HDMI_ARC_NAME = "hmdi_arc";
-    /** @hide */ public static final String DEVICE_OUT_HDMI_EARC_NAME = "hmdi_earc";
+    /** @hide */ public static final String DEVICE_OUT_HDMI_ARC_NAME = "hdmi_arc";
+    /** @hide */ public static final String DEVICE_OUT_HDMI_EARC_NAME = "hdmi_earc";
     /** @hide */ public static final String DEVICE_OUT_SPDIF_NAME = "spdif";
     /** @hide */ public static final String DEVICE_OUT_FM_NAME = "fm_transmitter";
     /** @hide */ public static final String DEVICE_OUT_AUX_LINE_NAME = "aux_line";
diff --git a/media/java/android/media/BluetoothProfileConnectionInfo.java b/media/java/android/media/BluetoothProfileConnectionInfo.java
index c148846..a316c21 100644
--- a/media/java/android/media/BluetoothProfileConnectionInfo.java
+++ b/media/java/android/media/BluetoothProfileConnectionInfo.java
@@ -126,6 +126,20 @@
     }
 
     /**
+     * Factory method for <code>BluetoothProfileConnectionInfo</code> for an LE output device
+     * @param suppressNoisyIntent if true the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY}
+     *     intent will not be sent.
+     * @param volume the volume index of the device, -1 if unknown or to be ignored
+     * @return an instance of BluetoothProfileConnectionInfo for the BLE output device that reflects
+     *     the given parameters
+     */
+    public static @NonNull BluetoothProfileConnectionInfo createLeAudioOutputInfo(
+            boolean suppressNoisyIntent, int volume) {
+        return new BluetoothProfileConnectionInfo(BluetoothProfile.LE_AUDIO, suppressNoisyIntent,
+                volume, /*isLeOutput*/ true);
+    }
+
+    /**
      * @return The profile connection
      */
     public int getProfile() {
diff --git a/media/java/android/media/tv/AdResponse.java b/media/java/android/media/tv/AdResponse.java
index 0c20954..08c66ab 100644
--- a/media/java/android/media/tv/AdResponse.java
+++ b/media/java/android/media/tv/AdResponse.java
@@ -25,7 +25,7 @@
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * An advertisement request which can be sent to TV interactive App service to inform AD status.
+ * An advertisement response which can be sent to TV interactive App service to inform AD status.
  */
 public final class AdResponse implements Parcelable {
     /** @hide */
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index 9f92887..a72f34c 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -217,7 +217,7 @@
             case DO_DISPATCH_SURFACE_CHANGED: {
                 SomeArgs args = (SomeArgs) msg.obj;
                 mSessionImpl.dispatchSurfaceChanged(
-                        (Integer) args.arg1, (Integer) args.arg2, (Integer) args.arg3);
+                        (Integer) args.argi1, (Integer) args.argi2, (Integer) args.argi3);
                 args.recycle();
                 break;
             }
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/BluetoothProfileConnectionInfoTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/BluetoothProfileConnectionInfoTest.java
new file mode 100644
index 0000000..ae162b5
--- /dev/null
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/BluetoothProfileConnectionInfoTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.mediaframeworktest.unit;
+
+import static org.junit.Assert.assertEquals;
+
+import android.bluetooth.BluetoothProfile;
+import android.media.BluetoothProfileConnectionInfo;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class BluetoothProfileConnectionInfoTest {
+
+    @Test
+    public void testCoverageLeAudioOutputVolume() {
+        final boolean supprNoisy = false;
+        final int volume = 1;
+        final BluetoothProfileConnectionInfo info = BluetoothProfileConnectionInfo
+                .createLeAudioOutputInfo(supprNoisy, volume);
+        assertEquals(info.getProfile(), BluetoothProfile.LE_AUDIO);
+        assertEquals(info.isSuppressNoisyIntent(), supprNoisy);
+        assertEquals(info.isLeOutput(), true);
+        assertEquals(info.getVolume(), volume);
+    }
+
+}
diff --git a/native/android/system_fonts.cpp b/native/android/system_fonts.cpp
index 4df745f..30d0c35 100644
--- a/native/android/system_fonts.cpp
+++ b/native/android/system_fonts.cpp
@@ -245,32 +245,23 @@
     std::unique_ptr<ASystemFontIterator> ite(new ASystemFontIterator());
 
     std::unordered_set<AFont, FontHasher> fonts;
-    minikin::SystemFonts::getFontMap(
-            [&fonts](const std::vector<std::shared_ptr<minikin::FontCollection>>& collections) {
-                for (const auto& fc : collections) {
-                    for (uint32_t i = 0; i < fc->getFamilyCount(); ++i) {
-                        const auto& family = fc->getFamilyAt(i);
-                        for (uint32_t j = 0; j < family->getNumFonts(); ++j) {
-                            const minikin::Font* font = family->getFont(j);
-
-                            std::optional<std::string> locale;
-                            uint32_t localeId = font->getLocaleListId();
-                            if (localeId != minikin::kEmptyLocaleListId) {
-                                locale.emplace(minikin::getLocaleString(localeId));
-                            }
-                            std::vector<std::pair<uint32_t, float>> axes;
-                            for (const auto& [tag, value] : font->typeface()->GetAxes()) {
-                                axes.push_back(std::make_pair(tag, value));
-                            }
-
-                            fonts.insert(
-                                    {font->typeface()->GetFontPath(), std::move(locale),
-                                     font->style().weight(),
-                                     font->style().slant() == minikin::FontStyle::Slant::ITALIC,
-                                     static_cast<uint32_t>(font->typeface()->GetFontIndex()),
-                                     axes});
-                        }
+    minikin::SystemFonts::getFontSet(
+            [&fonts](const std::vector<std::shared_ptr<minikin::Font>>& fontSet) {
+                for (const auto& font : fontSet) {
+                    std::optional<std::string> locale;
+                    uint32_t localeId = font->getLocaleListId();
+                    if (localeId != minikin::kEmptyLocaleListId) {
+                        locale.emplace(minikin::getLocaleString(localeId));
                     }
+                    std::vector<std::pair<uint32_t, float>> axes;
+                    for (const auto& [tag, value] : font->typeface()->GetAxes()) {
+                        axes.push_back(std::make_pair(tag, value));
+                    }
+
+                    fonts.insert({font->typeface()->GetFontPath(), std::move(locale),
+                                  font->style().weight(),
+                                  font->style().slant() == minikin::FontStyle::Slant::ITALIC,
+                                  static_cast<uint32_t>(font->typeface()->GetFontIndex()), axes});
                 }
             });
 
diff --git a/packages/SettingsLib/Spa/.idea/codeStyles/Project.xml b/packages/SettingsLib/Spa/.idea/codeStyles/Project.xml
index 318eeef..cb757d3 100644
--- a/packages/SettingsLib/Spa/.idea/codeStyles/Project.xml
+++ b/packages/SettingsLib/Spa/.idea/codeStyles/Project.xml
@@ -4,6 +4,9 @@
       <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
       <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
     </JetCodeStyleSettings>
+    <XML>
+      <option name="XML_KEEP_LINE_BREAKS" value="true" />
+    </XML>
     <codeStyleSettings language="XML">
       <option name="FORCE_REARRANGE_MODE" value="1" />
       <indentOptions>
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index 914a45b..b5be7cd 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -18,7 +18,8 @@
     package="com.android.settingslib.spa.gallery">
 
     <application
-        android:label="@string/app_name"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_label"
         android:supportsRtl="true">
         <activity
             android:name="com.android.settingslib.spa.gallery.MainActivity"
diff --git a/packages/SettingsLib/Spa/gallery/res/mipmap-anydpi-v26/ic_launcher.xml b/packages/SettingsLib/Spa/gallery/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..623ef56
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,21 @@
+<?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.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@mipmap/ic_launcher_background" />
+    <foreground android:drawable="@mipmap/ic_launcher_foreground" />
+</adaptive-icon>
diff --git a/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher.png b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..c3f1ab9
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_background.png b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..7da1549
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_background.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..187964f
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/gallery/res/values/strings.xml b/packages/SettingsLib/Spa/gallery/res/values/strings.xml
index 510e6c2..0d08d68 100644
--- a/packages/SettingsLib/Spa/gallery/res/values/strings.xml
+++ b/packages/SettingsLib/Spa/gallery/res/values/strings.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
 <!--
   Copyright (C) 2022 The Android Open Source Project
 
@@ -14,6 +15,8 @@
   limitations under the License.
   -->
 <resources>
+    <!-- Gallery App label. [DO NOT TRANSLATE] -->
+    <string name="app_label" translatable="false">Gallery</string>
     <!-- Gallery App name. [DO NOT TRANSLATE] -->
     <string name="app_name" translatable="false">SpaLib Gallery</string>
 </resources>
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
index 937e594..36361dd 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ArgumentPage.kt
@@ -17,7 +17,6 @@
 package com.android.settingslib.spa.gallery.page
 
 import android.os.Bundle
-import androidx.compose.foundation.layout.Column
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.navigation.NavType
@@ -28,12 +27,14 @@
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
+private const val TITLE = "Sample page with arguments"
 private const val STRING_PARAM_NAME = "stringParam"
 private const val INT_PARAM_NAME = "intParam"
 
 object ArgumentPageProvider : SettingsPageProvider {
-    override val name = Destinations.Argument
+    override val name = "Argument"
 
     override val arguments = listOf(
         navArgument(STRING_PARAM_NAME) { type = NavType.StringType },
@@ -51,17 +52,17 @@
     @Composable
     fun EntryItem(stringParam: String, intParam: Int) {
         Preference(object : PreferenceModel {
-            override val title = "Sample page with arguments"
+            override val title = TITLE
             override val summary =
                 "$STRING_PARAM_NAME=$stringParam, $INT_PARAM_NAME=$intParam".toState()
-            override val onClick = navigator("${Destinations.Argument}/$stringParam/$intParam")
+            override val onClick = navigator("$name/$stringParam/$intParam")
         })
     }
 }
 
 @Composable
 fun ArgumentPage(stringParam: String, intParam: Int) {
-    Column {
+    RegularScaffold(title = TITLE) {
         Preference(object : PreferenceModel {
             override val title = "String param value"
             override val summary = stringParam.toState()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
index 143c365..82005ec 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
@@ -17,12 +17,8 @@
 package com.android.settingslib.spa.gallery.page
 
 import android.os.Bundle
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.api.SettingsPageProvider
 import com.android.settingslib.spa.framework.compose.navigator
@@ -30,8 +26,11 @@
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 import com.android.settingslib.spa.widget.ui.Footer
 
+private const val TITLE = "Sample Footer"
+
 object FooterPageProvider : SettingsPageProvider {
     override val name = "Footer"
 
@@ -43,7 +42,7 @@
     @Composable
     fun EntryItem() {
         Preference(object : PreferenceModel {
-            override val title = "Sample Footer"
+            override val title = TITLE
             override val onClick = navigator(name)
         })
     }
@@ -51,7 +50,7 @@
 
 @Composable
 private fun FooterPage() {
-    Column(Modifier.verticalScroll(rememberScrollState())) {
+    RegularScaffold(title = TITLE) {
         Preference(remember {
             object : PreferenceModel {
                 override val title = "Some Preference"
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/HomePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/HomePage.kt
index 0d17cd2..6b7de8d 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/HomePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/HomePage.kt
@@ -31,7 +31,7 @@
 import com.android.settingslib.spa.gallery.R
 
 object HomePageProvider : SettingsPageProvider {
-    override val name = Destinations.Home
+    override val name = "Home"
 
     @Composable
     override fun Page(arguments: Bundle?) {
@@ -54,6 +54,7 @@
         ArgumentPageProvider.EntryItem(stringParam = "foo", intParam = 0)
 
         SliderPageProvider.EntryItem()
+        SettingsPagerPageProvider.EntryItem()
         FooterPageProvider.EntryItem()
     }
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PageRepository.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PageRepository.kt
index 8f38d82..cbfc603 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PageRepository.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PageRepository.kt
@@ -18,14 +18,6 @@
 
 import com.android.settingslib.spa.framework.api.SettingsPageRepository
 
-object Destinations {
-    const val Home = "Home"
-    const val Preference = "Preference"
-    const val SwitchPreference = "SwitchPreference"
-    const val Argument = "Argument"
-    const val Slider = "Slider"
-}
-
 val galleryPageRepository = SettingsPageRepository(
     allPages = listOf(
         HomePageProvider,
@@ -33,7 +25,8 @@
         SwitchPreferencePageProvider,
         ArgumentPageProvider,
         SliderPageProvider,
+        SettingsPagerPageProvider,
         FooterPageProvider,
     ),
-    startDestination = Destinations.Home,
+    startDestination = HomePageProvider.name,
 )
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PreferencePage.kt
index 3077a15..0463e58 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/PreferencePage.kt
@@ -17,18 +17,17 @@
 package com.android.settingslib.spa.gallery.page
 
 import android.os.Bundle
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.DisabledByDefault
+import androidx.compose.material.icons.outlined.TouchApp
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.produceState
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.api.SettingsPageProvider
 import com.android.settingslib.spa.framework.compose.navigator
@@ -36,10 +35,14 @@
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.SettingsIcon
 import kotlinx.coroutines.delay
 
+private const val TITLE = "Sample Preference"
+
 object PreferencePageProvider : SettingsPageProvider {
-    override val name = Destinations.Preference
+    override val name = "Preference"
 
     @Composable
     override fun Page(arguments: Bundle?) {
@@ -49,15 +52,15 @@
     @Composable
     fun EntryItem() {
         Preference(object : PreferenceModel {
-            override val title = "Sample Preference"
-            override val onClick = navigator(Destinations.Preference)
+            override val title = TITLE
+            override val onClick = navigator(name)
         })
     }
 }
 
 @Composable
 private fun PreferencePage() {
-    Column(Modifier.verticalScroll(rememberScrollState())) {
+    RegularScaffold(title = TITLE) {
         Preference(object : PreferenceModel {
             override val title = "Preference"
         })
@@ -75,14 +78,17 @@
             }
         })
 
-        var count by remember { mutableStateOf(0) }
+        var count by rememberSaveable { mutableStateOf(0) }
         Preference(object : PreferenceModel {
             override val title = "Click me"
             override val summary = derivedStateOf { count.toString() }
             override val onClick: (() -> Unit) = { count++ }
+            override val icon = @Composable {
+                SettingsIcon(imageVector = Icons.Outlined.TouchApp)
+            }
         })
 
-        var ticks by remember { mutableStateOf(0) }
+        var ticks by rememberSaveable { mutableStateOf(0) }
         LaunchedEffect(ticks) {
             delay(1000L)
             ticks++
@@ -96,6 +102,9 @@
             override val title = "Disabled"
             override val summary = "Disabled".toState()
             override val enabled = false.toState()
+            override val icon = @Composable {
+              SettingsIcon(imageVector = Icons.Outlined.DisabledByDefault)
+            }
         })
     }
 }
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
new file mode 100644
index 0000000..5351ea6
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SettingsPagerPage.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.page
+
+import android.os.Bundle
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.api.SettingsPageProvider
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.SettingsPager
+import com.android.settingslib.spa.widget.ui.SettingsTitle
+
+object SettingsPagerPageProvider : SettingsPageProvider {
+    override val name = "SettingsPager"
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        SettingsPagerPage()
+    }
+
+    @Composable
+    fun EntryItem() {
+        Preference(object : PreferenceModel {
+            override val title = "Sample SettingsPager"
+            override val onClick = navigator(name)
+        })
+    }
+}
+
+@Composable
+private fun SettingsPagerPage() {
+    SettingsPager(listOf("Personal", "Work")) {
+        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+            SettingsTitle(title = "Page $it")
+        }
+    }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SettingsPagerPagePreview() {
+    SettingsTheme {
+        SettingsPagerPage()
+    }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
index 9bcac1b..04046fa 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
@@ -17,9 +17,6 @@
 package com.android.settingslib.spa.gallery.page
 
 import android.os.Bundle
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.AccessAlarm
 import androidx.compose.material.icons.outlined.MusicNote
@@ -29,18 +26,20 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.api.SettingsPageProvider
 import com.android.settingslib.spa.framework.compose.navigator
 import com.android.settingslib.spa.framework.theme.SettingsTheme
 import com.android.settingslib.spa.widget.preference.Preference
 import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 import com.android.settingslib.spa.widget.ui.SettingsSlider
 import com.android.settingslib.spa.widget.ui.SettingsSliderModel
 
+private const val TITLE = "Sample Slider"
+
 object SliderPageProvider : SettingsPageProvider {
-    override val name = Destinations.Slider
+    override val name = "Slider"
 
     @Composable
     override fun Page(arguments: Bundle?) {
@@ -50,15 +49,15 @@
     @Composable
     fun EntryItem() {
         Preference(object : PreferenceModel {
-            override val title = "Sample Slider"
-            override val onClick = navigator(Destinations.Slider)
+            override val title = TITLE
+            override val onClick = navigator(name)
         })
     }
 }
 
 @Composable
 private fun SliderPage() {
-    Column(Modifier.verticalScroll(rememberScrollState())) {
+    RegularScaffold(title = TITLE) {
         SettingsSlider(object : SettingsSliderModel {
             override val title = "Slider"
             override val initValue = 40
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SwitchPreferencePage.kt
index b6f7258..e9e5d35 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SwitchPreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SwitchPreferencePage.kt
@@ -17,15 +17,11 @@
 package com.android.settingslib.spa.gallery.page
 
 import android.os.Bundle
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.produceState
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
 import com.android.settingslib.spa.framework.api.SettingsPageProvider
 import com.android.settingslib.spa.framework.compose.navigator
@@ -35,10 +31,13 @@
 import com.android.settingslib.spa.widget.preference.PreferenceModel
 import com.android.settingslib.spa.widget.preference.SwitchPreference
 import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 import kotlinx.coroutines.delay
 
+private const val TITLE = "Sample SwitchPreference"
+
 object SwitchPreferencePageProvider : SettingsPageProvider {
-    override val name = Destinations.SwitchPreference
+    override val name = "SwitchPreference"
 
     @Composable
     override fun Page(arguments: Bundle?) {
@@ -48,15 +47,15 @@
     @Composable
     fun EntryItem() {
         Preference(object : PreferenceModel {
-            override val title = "Sample SwitchPreference"
-            override val onClick = navigator(Destinations.SwitchPreference)
+            override val title = TITLE
+            override val onClick = navigator(name)
         })
     }
 }
 
 @Composable
 private fun SwitchPreferencePage() {
-    Column(Modifier.verticalScroll(rememberScrollState())) {
+    RegularScaffold(title = TITLE) {
         SampleSwitchPreference()
         SampleSwitchPreferenceWithSummary()
         SampleSwitchPreferenceWithAsyncSummary()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
index e1ca69b..7d3e107 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt
@@ -31,4 +31,5 @@
         end = itemPaddingEnd,
         bottom = itemPaddingVertical,
     )
+    val itemPaddingAround = 8.dp
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt
similarity index 73%
copy from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
copy to packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt
index 8dde897..20020d0 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spaprivileged.framework.app
+package com.android.settingslib.spa.framework.theme
 
-import android.content.pm.ApplicationInfo
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.ui.unit.dp
 
-interface AppRecord {
-    val app: ApplicationInfo
+object SettingsShape {
+    val CornerMedium = RoundedCornerShape(12.dp)
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
index 1a80ed2..0e6f53a 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.clickable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import com.android.settingslib.spa.framework.compose.stateOf
 
@@ -38,6 +39,14 @@
         get() = stateOf("")
 
     /**
+     * The icon of this [Preference].
+     *
+     * Default is `null` which means no icon.
+     */
+    val icon: (@Composable () -> Unit)?
+        get() = null
+
+    /**
      * Indicates whether this [Preference] is enabled.
      *
      * Disabled [Preference] will be displayed in disabled style.
@@ -61,13 +70,16 @@
  */
 @Composable
 fun Preference(model: PreferenceModel) {
-    val modifier = model.onClick?.let { onClick ->
-        Modifier.clickable(enabled = model.enabled.value) { onClick() }
-    } ?: Modifier
+    val modifier = remember(model.enabled.value, model.onClick) {
+      model.onClick?.let { onClick ->
+        Modifier.clickable(enabled = model.enabled.value, onClick = onClick)
+      } ?: Modifier
+    }
     BasePreference(
         title = model.title,
         summary = model.summary,
         modifier = modifier,
+        icon = model.icon,
         enabled = model.enabled,
     )
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
index 0dab0df..b6d6936 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/SwitchPreference.kt
@@ -100,7 +100,7 @@
 ) {
     val checkedValue = checked.value
     val indication = LocalIndication.current
-    val modifier = remember(checkedValue) {
+    val modifier = remember(checkedValue, changeable.value) {
         if (checkedValue != null && onCheckedChange != null) {
             Modifier.toggleable(
                 value = checkedValue,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
new file mode 100644
index 0000000..0a41a1a
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.scaffold
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.android.settingslib.spa.framework.compose.LocalNavController
+
+@Composable
+internal fun NavigateUp() {
+    val navController = LocalNavController.current
+    val contentDescription = stringResource(
+        id = androidx.appcompat.R.string.abc_action_bar_up_description,
+    )
+    BackAction(contentDescription) {
+        navController.navigateUp()
+    }
+}
+
+@Composable
+private fun BackAction(contentDescription: String, onClick: () -> Unit) {
+    IconButton(onClick) {
+        Icon(
+            imageVector = Icons.Outlined.ArrowBack,
+            contentDescription = contentDescription,
+        )
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt
new file mode 100644
index 0000000..3325d53
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/RegularScaffold.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.scaffold
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SmallTopAppBar
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+/**
+ * A [Scaffold] which content is scrollable and wrapped in a [Column].
+ *
+ * For example, this is for the pages with some preferences and is scrollable when the items out of
+ * the screen.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun RegularScaffold(
+    title: String,
+    actions: @Composable RowScope.() -> Unit = {},
+    content: @Composable () -> Unit,
+) {
+    Scaffold(
+        topBar = {
+            SmallTopAppBar(
+                title = {
+                    Text(
+                        text = title,
+                        modifier = Modifier.padding(SettingsDimension.itemPaddingAround),
+                    )
+                },
+                navigationIcon = { NavigateUp() },
+                actions = actions,
+                colors = settingsTopAppBarColors(),
+            )
+        },
+    ) { paddingValues ->
+        Column(Modifier.verticalScroll(rememberScrollState())) {
+            Spacer(Modifier.padding(paddingValues))
+            content()
+        }
+    }
+}
+
+@Composable
+internal fun settingsTopAppBarColors() = TopAppBarDefaults.largeTopAppBarColors(
+    containerColor = SettingsTheme.colorScheme.surfaceHeader,
+    scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader,
+)
+
+@Preview
+@Composable
+private fun RegularScaffoldPreview() {
+    SettingsTheme {
+        RegularScaffold(title = "Display") {}
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt
new file mode 100644
index 0000000..1ec2390
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsPager.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.scaffold
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.TabRow
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+
+@Composable
+fun SettingsPager(titles: List<String>, content: @Composable (page: Int) -> Unit) {
+    check(titles.isNotEmpty())
+    if (titles.size == 1) {
+        content(0)
+        return
+    }
+
+    Column {
+        var currentPage by rememberSaveable { mutableStateOf(0) }
+
+        TabRow(
+            selectedTabIndex = currentPage,
+            modifier = Modifier.padding(horizontal = SettingsDimension.itemPaddingEnd),
+            containerColor = Color.Transparent,
+            indicator = {},
+            divider = {},
+        ) {
+            titles.forEachIndexed { page, title ->
+                SettingsTab(
+                    title = title,
+                    selected = currentPage == page,
+                    onClick = { currentPage = page },
+                )
+            }
+        }
+
+        content(currentPage)
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt
new file mode 100644
index 0000000..16d8dbc
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.scaffold
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Tab
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.theme.SettingsShape
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+@Composable
+internal fun SettingsTab(
+    title: String,
+    selected: Boolean,
+    onClick: () -> Unit,
+) {
+    Tab(
+        selected = selected,
+        onClick = onClick,
+        modifier = Modifier
+            .height(48.dp)
+            .padding(horizontal = 4.dp, vertical = 6.dp)
+            .clip(SettingsShape.CornerMedium)
+            .background(color = when {
+                selected -> SettingsTheme.colorScheme.primaryContainer
+                else -> SettingsTheme.colorScheme.surface
+            }),
+    ) {
+        Text(
+            text = title,
+            style = MaterialTheme.typography.labelLarge,
+            color = when {
+                selected -> SettingsTheme.colorScheme.onPrimaryContainer
+                else -> SettingsTheme.colorScheme.secondaryText
+            },
+        )
+    }
+}
+
+@Preview
+@Composable
+private fun SettingsTabPreview() {
+    SettingsTheme {
+        Column {
+            SettingsTab(title = "Personal", selected = true) {}
+            SettingsTab(title = "Work", selected = false) {}
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Footer.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Footer.kt
index 41fd03b..296cf3b 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Footer.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Footer.kt
@@ -20,8 +20,11 @@
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.Info
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
@@ -32,7 +35,12 @@
 fun Footer(footerText: String) {
     if (footerText.isEmpty()) return
     Column(Modifier.padding(SettingsDimension.itemPadding)) {
-        SettingsIcon(imageVector = Icons.Outlined.Info, contentDescription = null)
+        Icon(
+            imageVector = Icons.Outlined.Info,
+            contentDescription = null,
+            modifier = Modifier.size(SettingsDimension.itemIconSize),
+            tint = MaterialTheme.colorScheme.onSurfaceVariant,
+        )
         Spacer(modifier = Modifier.height(SettingsDimension.itemPaddingVertical))
         SettingsBody(footerText)
     }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Icon.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Icon.kt
index cb08cdb..4f28e37 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Icon.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Icon.kt
@@ -25,13 +25,10 @@
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 
 @Composable
-fun SettingsIcon(
-    imageVector: ImageVector,
-    contentDescription: String?,
-) {
+fun SettingsIcon(imageVector: ImageVector) {
     Icon(
         imageVector = imageVector,
-        contentDescription = contentDescription,
+        contentDescription = null,
         modifier = Modifier.size(SettingsDimension.itemIconSize),
         tint = MaterialTheme.colorScheme.onSurface,
     )
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt
new file mode 100644
index 0000000..f608e10
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/scaffold/SettingsPagerKtTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.scaffold
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotSelected
+import androidx.compose.ui.test.assertIsSelected
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.widget.ui.SettingsTitle
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsPagerKtTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun twoPage_initialState() {
+        composeTestRule.setContent {
+            TestTwoPage()
+        }
+
+        composeTestRule.onNodeWithText("Personal").assertIsSelected()
+        composeTestRule.onNodeWithText("Page 0").assertIsDisplayed()
+        composeTestRule.onNodeWithText("Work").assertIsNotSelected()
+        composeTestRule.onNodeWithText("Page 1").assertDoesNotExist()
+    }
+
+    @Test
+    fun twoPage_afterSwitch() {
+        composeTestRule.setContent {
+            TestTwoPage()
+        }
+
+        composeTestRule.onNodeWithText("Work").performClick()
+
+        composeTestRule.onNodeWithText("Personal").assertIsNotSelected()
+        composeTestRule.onNodeWithText("Page 0").assertDoesNotExist()
+        composeTestRule.onNodeWithText("Work").assertIsSelected()
+        composeTestRule.onNodeWithText("Page 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun onePage_initialState() {
+        composeTestRule.setContent {
+            SettingsPager(listOf("Personal")) {
+                SettingsTitle(title = "Page $it")
+            }
+        }
+
+        composeTestRule.onNodeWithText("Personal").assertDoesNotExist()
+        composeTestRule.onNodeWithText("Page 0").assertIsDisplayed()
+        composeTestRule.onNodeWithText("Page 1").assertDoesNotExist()
+    }
+}
+
+@Composable
+private fun TestTwoPage() {
+    SettingsPager(listOf("Personal", "Work")) {
+        SettingsTitle(title = "Page $it")
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp
index 48f7ff2..ecbb219 100644
--- a/packages/SettingsLib/SpaPrivileged/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/Android.bp
@@ -29,5 +29,4 @@
         "androidx.compose.runtime_runtime",
     ],
     kotlincflags: ["-Xjvm-default=all"],
-    min_sdk_version: "31",
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppOpsController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
similarity index 96%
rename from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppOpsController.kt
rename to packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
index 3808f64..c2d85a5 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppOpsController.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spaprivileged.framework.app
+package com.android.settingslib.spaprivileged.model.app
 
 import android.app.AppOpsManager
 import android.app.AppOpsManager.MODE_ALLOWED
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRecord.kt
similarity index 92%
rename from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
rename to packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRecord.kt
index 8dde897..2978688 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRecord.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spaprivileged.framework.app
+package com.android.settingslib.spaprivileged.model.app
 
 import android.content.pm.ApplicationInfo
 
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
similarity index 96%
rename from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.kt
rename to packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
index a6378ef..7ffa938 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spaprivileged.framework.app
+package com.android.settingslib.spaprivileged.model.app
 
 import android.content.Context
 import android.content.pm.ApplicationInfo
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/Apps.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/Apps.kt
similarity index 92%
rename from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/Apps.kt
rename to packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/Apps.kt
index f675545..6e1afd9 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/Apps.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/Apps.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spaprivileged.framework.app
+package com.android.settingslib.spaprivileged.model.app
 
 import android.content.pm.ApplicationInfo
 import android.os.UserHandle
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
similarity index 94%
rename from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt
rename to packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
index 66b05da..0cc497a 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spaprivileged.framework.app
+package com.android.settingslib.spaprivileged.model.app
 
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageInfo
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/EnterpriseRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/EnterpriseRepository.kt
new file mode 100644
index 0000000..fab3ae8
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/EnterpriseRepository.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.model.enterprise
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Settings.PERSONAL_CATEGORY_HEADER
+import android.app.admin.DevicePolicyResources.Strings.Settings.WORK_CATEGORY_HEADER
+import android.content.Context
+import com.android.settingslib.spaprivileged.R
+
+class EnterpriseRepository(private val context: Context) {
+    private val resources by lazy {
+        checkNotNull(context.getSystemService(DevicePolicyManager::class.java)).resources
+    }
+
+    fun getEnterpriseString(updatableStringId: String, resId: Int): String =
+        resources.getString(updatableStringId) { context.getString(resId) }
+
+    fun getProfileTitle(isManagedProfile: Boolean): String = if (isManagedProfile) {
+        getEnterpriseString(WORK_CATEGORY_HEADER, R.string.category_work)
+    } else {
+        getEnterpriseString(PERSONAL_CATEGORY_HEADER, R.string.category_personal)
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
index 5ae514c..d802b04 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
@@ -33,8 +33,8 @@
 import com.android.settingslib.spa.framework.compose.rememberDrawablePainter
 import com.android.settingslib.spa.widget.ui.SettingsBody
 import com.android.settingslib.spa.widget.ui.SettingsTitle
-import com.android.settingslib.spaprivileged.framework.app.PackageManagers
-import com.android.settingslib.spaprivileged.framework.app.rememberAppRepository
+import com.android.settingslib.spaprivileged.model.app.PackageManagers
+import com.android.settingslib.spaprivileged.model.app.rememberAppRepository
 
 @Composable
 fun AppInfo(packageName: String, userId: Int) {
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
index 57e9e9a..430ac54 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
@@ -31,8 +31,8 @@
 import com.android.settingslib.spa.framework.compose.rememberContext
 import com.android.settingslib.spa.widget.preference.SwitchPreference
 import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
-import com.android.settingslib.spaprivileged.framework.app.AppRecord
-import com.android.settingslib.spaprivileged.framework.app.PackageManagers
+import com.android.settingslib.spaprivileged.model.app.AppRecord
+import com.android.settingslib.spaprivileged.model.app.PackageManagers
 import kotlinx.coroutines.Dispatchers
 
 private const val PERMISSION = "permission"
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListModel.kt
index 88ad9da..3782f7c 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListModel.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListModel.kt
@@ -20,7 +20,7 @@
 import android.content.pm.ApplicationInfo
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
-import com.android.settingslib.spaprivileged.framework.app.AppRecord
+import com.android.settingslib.spaprivileged.model.app.AppRecord
 
 interface TogglePermissionAppListModel<T : AppRecord> {
     val pageTitleResId: Int
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/WorkProfilePager.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/WorkProfilePager.kt
new file mode 100644
index 0000000..aa5ccf1
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/WorkProfilePager.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.settingslib.spaprivileged.template.common
+
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import com.android.settingslib.spa.widget.scaffold.SettingsPager
+import com.android.settingslib.spaprivileged.model.enterprise.EnterpriseRepository
+
+@Composable
+fun WorkProfilePager(content: @Composable (userInfo: UserInfo) -> Unit) {
+    val context = LocalContext.current
+    val profiles = remember {
+        val userManager = checkNotNull(context.getSystemService(UserManager::class.java))
+        userManager.getProfiles(UserHandle.myUserId())
+    }
+    val titles = remember {
+        val enterpriseRepository = EnterpriseRepository(context)
+        profiles.map {
+            enterpriseRepository.getProfileTitle(isManagedProfile = it.isManagedProfile)
+        }
+    }
+
+    SettingsPager(titles) { page ->
+        content(profiles[page])
+    }
+}
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 65696253..aab0d3a 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1125,7 +1125,7 @@
     <!-- [CHAR_LIMIT=40] Label for battery level chart when charging with duration -->
     <string name="power_charging_duration"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="time">%2$s</xliff:g> left until full</string>
     <!-- [CHAR_LIMIT=80] Label for battery level chart when charge been limited -->
-    <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging temporarily limited</string>
+    <string name="power_charging_limited"><xliff:g id="level">%1$s</xliff:g> - Charging is paused</string>
 
     <!-- Battery Info screen. Value for a status item.  Used for diagnostic info screens, precise translation isn't needed -->
     <string name="battery_info_status_unknown">Unknown</string>
@@ -1138,7 +1138,7 @@
     <!-- [CHAR_LIMIT=20] Battery use screen.  Battery status shown in chart label when charging wirelessly.  -->
     <string name="battery_info_status_charging_wireless">Charging wirelessly</string>
     <!-- [CHAR_LIMIT=20] Battery use screen.  Battery status shown in chart label when the device is dock charging.  -->
-    <string name="battery_info_status_charging_dock">Charging Dock</string>
+    <string name="battery_info_status_charging_dock">Charging</string>
     <!-- Battery Info screen. Value for a status item.  Used for diagnostic info screens, precise translation isn't needed -->
     <string name="battery_info_status_discharging">Not charging</string>
     <!-- Battery Info screen. Value for a status item. A state which device is connected with any charger(e.g. USB, Adapter or Wireless) but not charging yet. Used for diagnostic info screens, precise translation isn't needed -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/OWNERS b/packages/SettingsLib/src/com/android/settingslib/media/OWNERS
new file mode 100644
index 0000000..d40f322
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/media/OWNERS
@@ -0,0 +1,2 @@
+# Default reviewers for this and subdirectories.
+shaoweishen@google.com
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 42b992f..5aae1ce 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -182,6 +182,7 @@
         Settings.Secure.PEOPLE_STRIP,
         Settings.Secure.MEDIA_CONTROLS_RESUME,
         Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
+        Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
         Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
         Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
         Settings.Secure.ACCESSIBILITY_MAGNIFICATION_CAPABILITY,
@@ -212,6 +213,8 @@
         Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
         Settings.Secure.WEAR_TALKBACK_ENABLED,
         Settings.Secure.HBM_SETTING_KEY,
-        Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED
+        Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED,
+        Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_TRIGGER_HINTS_ENABLED,
+        Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_KEYBOARD_SHIFT_ENABLED,
     };
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 14b5855..67ea024 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -277,6 +277,7 @@
         VALIDATORS.put(Secure.PEOPLE_STRIP, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.MEDIA_CONTROLS_RESUME, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.MEDIA_CONTROLS_RECOMMENDATION, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.MEDIA_CONTROLS_LOCK_SCREEN, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_MAGNIFICATION_MODE,
                 new InclusiveIntegerRangeValidator(
                         Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN,
@@ -346,5 +347,9 @@
         VALIDATORS.put(Secure.WEAR_TALKBACK_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.HBM_SETTING_KEY, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(
+                Secure.ACCESSIBILITY_SOFTWARE_CURSOR_TRIGGER_HINTS_ENABLED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(
+                Secure.ACCESSIBILITY_SOFTWARE_CURSOR_KEYBOARD_SHIFT_ENABLED, BOOLEAN_VALIDATOR);
     }
 }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index c3b645e..a2ffcf3 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1828,6 +1828,12 @@
         dumpSetting(s, p,
                 Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED,
                 SecureSettingsProto.Accessibility.ACCESSIBILITY_SOFTWARE_CURSOR_ENABLED);
+        dumpSetting(s, p,
+                Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_TRIGGER_HINTS_ENABLED,
+                SecureSettingsProto.Accessibility.SoftwareCursorSettings.TRIGGER_HINTS_ENABLED);
+        dumpSetting(s, p,
+                Settings.Secure.ACCESSIBILITY_SOFTWARE_CURSOR_KEYBOARD_SHIFT_ENABLED,
+                SecureSettingsProto.Accessibility.SoftwareCursorSettings.KEYBOARD_SHIFT_ENABLED);
         p.end(accessibilityToken);
 
         final long adaptiveSleepToken = p.start(SecureSettingsProto.ADAPTIVE_SLEEP);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index fd7554f..528af2e 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -376,9 +376,11 @@
             Setting newSetting = new Setting(name, oldSetting.getValue(), null,
                     oldSetting.getPackageName(), oldSetting.getTag(), false,
                     oldSetting.getId());
-            mSettings.put(name, newSetting);
-            updateMemoryUsagePerPackageLocked(newSetting.getPackageName(), oldValue,
+            int newSize = getNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), oldValue,
                     newSetting.getValue(), oldDefaultValue, newSetting.getDefaultValue());
+            checkNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize);
+            mSettings.put(name, newSetting);
+            updateMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize);
             scheduleWriteIfNeededLocked();
         }
     }
@@ -410,6 +412,12 @@
         Setting oldState = mSettings.get(name);
         String oldValue = (oldState != null) ? oldState.value : null;
         String oldDefaultValue = (oldState != null) ? oldState.defaultValue : null;
+        String newDefaultValue = makeDefault ? value : oldDefaultValue;
+
+        int newSize = getNewMemoryUsagePerPackageLocked(packageName, oldValue, value,
+                oldDefaultValue, newDefaultValue);
+        checkNewMemoryUsagePerPackageLocked(packageName, newSize);
+
         Setting newState;
 
         if (oldState != null) {
@@ -430,8 +438,7 @@
 
         addHistoricalOperationLocked(HISTORICAL_OPERATION_UPDATE, newState);
 
-        updateMemoryUsagePerPackageLocked(packageName, oldValue, value,
-                oldDefaultValue, newState.getDefaultValue());
+        updateMemoryUsagePerPackageLocked(packageName, newSize);
 
         scheduleWriteIfNeededLocked();
 
@@ -552,13 +559,14 @@
         }
 
         Setting oldState = mSettings.remove(name);
+        int newSize = getNewMemoryUsagePerPackageLocked(oldState.packageName, oldState.value,
+                null, oldState.defaultValue, null);
 
         FrameworkStatsLog.write(FrameworkStatsLog.SETTING_CHANGED, name, /* value= */ "",
                 /* newValue= */ "", oldState.value, /* tag */ "", false, getUserIdFromKey(mKey),
                 FrameworkStatsLog.SETTING_CHANGED__REASON__DELETED);
 
-        updateMemoryUsagePerPackageLocked(oldState.packageName, oldState.value,
-                null, oldState.defaultValue, null);
+        updateMemoryUsagePerPackageLocked(oldState.packageName, newSize);
 
         addHistoricalOperationLocked(HISTORICAL_OPERATION_DELETE, oldState);
 
@@ -579,16 +587,18 @@
         Setting oldSetting = new Setting(setting);
         String oldValue = setting.getValue();
         String oldDefaultValue = setting.getDefaultValue();
+        String newValue = oldDefaultValue;
+        String newDefaultValue = oldDefaultValue;
+
+        int newSize = getNewMemoryUsagePerPackageLocked(setting.packageName, oldValue,
+                newValue, oldDefaultValue, newDefaultValue);
+        checkNewMemoryUsagePerPackageLocked(setting.packageName, newSize);
 
         if (!setting.reset()) {
             return false;
         }
 
-        String newValue = setting.getValue();
-        String newDefaultValue = setting.getDefaultValue();
-
-        updateMemoryUsagePerPackageLocked(setting.packageName, oldValue,
-                newValue, oldDefaultValue, newDefaultValue);
+        updateMemoryUsagePerPackageLocked(setting.packageName, newSize);
 
         addHistoricalOperationLocked(HISTORICAL_OPERATION_RESET, oldSetting);
 
@@ -696,38 +706,49 @@
     }
 
     @GuardedBy("mLock")
-    private void updateMemoryUsagePerPackageLocked(String packageName, String oldValue,
+    private boolean isExemptFromMemoryUsageCap(String packageName) {
+        return mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED
+                || SYSTEM_PACKAGE_NAME.equals(packageName);
+    }
+
+    @GuardedBy("mLock")
+    private void checkNewMemoryUsagePerPackageLocked(String packageName, int newSize)
+            throws IllegalStateException {
+        if (isExemptFromMemoryUsageCap(packageName)) {
+            return;
+        }
+        if (newSize > mMaxBytesPerAppPackage) {
+            throw new IllegalStateException("You are adding too many system settings. "
+                    + "You should stop using system settings for app specific data"
+                    + " package: " + packageName);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private int getNewMemoryUsagePerPackageLocked(String packageName, String oldValue,
             String newValue, String oldDefaultValue, String newDefaultValue) {
-        if (mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED) {
-            return;
+        if (isExemptFromMemoryUsageCap(packageName)) {
+            return 0;
         }
-
-        if (SYSTEM_PACKAGE_NAME.equals(packageName)) {
-            return;
-        }
-
+        final Integer currentSize = mPackageToMemoryUsage.get(packageName);
         final int oldValueSize = (oldValue != null) ? oldValue.length() : 0;
         final int newValueSize = (newValue != null) ? newValue.length() : 0;
         final int oldDefaultValueSize = (oldDefaultValue != null) ? oldDefaultValue.length() : 0;
         final int newDefaultValueSize = (newDefaultValue != null) ? newDefaultValue.length() : 0;
         final int deltaSize = newValueSize + newDefaultValueSize
                 - oldValueSize - oldDefaultValueSize;
+        return Math.max((currentSize != null) ? currentSize + deltaSize : deltaSize, 0);
+    }
 
-        Integer currentSize = mPackageToMemoryUsage.get(packageName);
-        final int newSize = Math.max((currentSize != null)
-                ? currentSize + deltaSize : deltaSize, 0);
-
-        if (newSize > mMaxBytesPerAppPackage) {
-            throw new IllegalStateException("You are adding too many system settings. "
-                    + "You should stop using system settings for app specific data"
-                    + " package: " + packageName);
+    @GuardedBy("mLock")
+    private void updateMemoryUsagePerPackageLocked(String packageName, int newSize) {
+        if (isExemptFromMemoryUsageCap(packageName)) {
+            return;
         }
-
         if (DEBUG) {
             Slog.i(LOG_TAG, "Settings for package: " + packageName
                     + " size: " + newSize + " bytes.");
         }
-
         mPackageToMemoryUsage.put(packageName, newSize);
     }
 
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index 69eb713..a637efa 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -20,6 +20,8 @@
 import android.util.TypedXmlSerializer;
 import android.util.Xml;
 
+import com.google.common.base.Strings;
+
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
@@ -276,4 +278,39 @@
         settingsState.setVersionLocked(SettingsState.SETTINGS_VERSION_NEW_ENCODING);
         return settingsState;
     }
+
+    public void testInsertSetting_memoryUsage() {
+        SettingsState settingsState = getSettingStateObject();
+        // No exception should be thrown when there is no cap
+        settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001),
+                null, false, "p1");
+        settingsState.deleteSettingLocked(SETTING_NAME);
+
+        settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1,
+                SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper());
+        // System package doesn't have memory usage limit
+        settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001),
+                null, false, SYSTEM_PACKAGE);
+        settingsState.deleteSettingLocked(SETTING_NAME);
+
+        // Should not throw if usage is under the cap
+        settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 19999),
+                null, false, "p1");
+        settingsState.deleteSettingLocked(SETTING_NAME);
+        try {
+            settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001),
+                    null, false, "p1");
+            fail("Should throw because it exceeded per package memory usage");
+        } catch (IllegalStateException ex) {
+            assertTrue(ex.getMessage().contains("p1"));
+        }
+        try {
+            settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001),
+                    null, false, "p1");
+            fail("Should throw because it exceeded per package memory usage");
+        } catch (IllegalStateException ex) {
+            assertTrue(ex.getMessage().contains("p1"));
+        }
+        assertTrue(settingsState.getSettingLocked(SETTING_NAME).isNull());
+    }
 }
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 634df39..a25f567 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -238,6 +238,9 @@
         "com.android.systemui",
     ],
     plugins: ["dagger2-compiler"],
+    lint: {
+        test: true,
+    },
 }
 
 // Opt-in config for optimizing the SystemUI target using R8.
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 6edf13a..652e281 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -394,6 +394,11 @@
                   android:label="@string/screenshot_scroll_label"
                   android:finishOnTaskLaunch="true" />
 
+        <service android:name=".screenshot.ScreenshotProxyService"
+                 android:permission="com.android.systemui.permission.SELF"
+                 android:exported="false" />
+
+
         <service android:name=".screenrecord.RecordingService" />
 
         <receiver android:name=".SysuiRestartReceiver"
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index 61fac29..4a6164c 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -14,11 +14,10 @@
 brycelee@google.com
 ccassidy@google.com
 chrisgollner@google.com
-cinek@google.com
-cwren@google.com
 dupin@google.com
 ethibodeau@google.com
 evanlaird@google.com
+florenceyang@google.com
 gwasserman@google.com
 hwwang@google.com
 hyunyoungs@google.com
@@ -27,36 +26,36 @@
 jbolinger@google.com
 jdemeulenaere@google.com
 jeffdq@google.com
+jernej@google.com
+jglazier@google.com
 jjaggi@google.com
 jonmiranda@google.com
 joshtrask@google.com
 juliacr@google.com
 juliatuttle@google.com
 justinkoh@google.com
-kchyn@google.com
 kozynski@google.com
 kprevas@google.com
 lynhan@google.com
 madym@google.com
 mankoff@google.com
-mett@google.com
 mkephart@google.com
 mpietal@google.com
 mrcasey@google.com
 mrenouf@google.com
-nesciosquid@google.com
 nickchameyev@google.com
 nicomazz@google.com
 ogunwale@google.com
 peanutbutter@google.com
+peskal@google.com
 pinyaoting@google.com
 pixel@google.com
+pomini@google.com
 rahulbanerjee@google.com
 roosa@google.com
 santie@google.com
 shanh@google.com
 snoeberger@google.com
-sreyasr@google.com
 steell@google.com
 sfufa@google.com
 stwu@google.com
@@ -70,15 +69,10 @@
 victortulias@google.com
 winsonc@google.com
 wleshner@google.com
-yurilin@google.com
 xuqiu@google.com
+yuandizhou@google.com
+yurilin@google.com
 zakcohen@google.com
-jernej@google.com
-jglazier@google.com
-peskal@google.com
-
-#Android Auto
-hseog@google.com
 
 #Android TV
 rgl@google.com
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
index cc7d23e..dc2c6356 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt
@@ -165,6 +165,8 @@
          * @param includeFadeIn true if the animator should also fade in the view and child views.
          * @param fadeInInterpolator the interpolator to use when fading in the view. Unused if
          *     [includeFadeIn] is false.
+         * @param onAnimationEnd an optional runnable that will be run once the animation
+         *    finishes successfully. Will not be run if the animation is cancelled.
          */
         @JvmOverloads
         fun animateAddition(
@@ -174,7 +176,8 @@
             duration: Long = DEFAULT_DURATION,
             includeMargins: Boolean = false,
             includeFadeIn: Boolean = false,
-            fadeInInterpolator: Interpolator = DEFAULT_FADE_IN_INTERPOLATOR
+            fadeInInterpolator: Interpolator = DEFAULT_FADE_IN_INTERPOLATOR,
+            onAnimationEnd: Runnable? = null,
         ): Boolean {
             if (
                 occupiesSpace(
@@ -193,7 +196,8 @@
                     origin,
                     interpolator,
                     duration,
-                    ignorePreviousValues = !includeMargins
+                    ignorePreviousValues = !includeMargins,
+                    onAnimationEnd,
                 )
             addListener(rootView, listener, recursive = true)
 
@@ -246,14 +250,16 @@
             origin: Hotspot,
             interpolator: Interpolator,
             duration: Long,
-            ignorePreviousValues: Boolean
+            ignorePreviousValues: Boolean,
+            onAnimationEnd: Runnable? = null,
         ): View.OnLayoutChangeListener {
             return createListener(
                 interpolator,
                 duration,
                 ephemeral = true,
                 origin = origin,
-                ignorePreviousValues = ignorePreviousValues
+                ignorePreviousValues = ignorePreviousValues,
+                onAnimationEnd,
             )
         }
 
@@ -272,7 +278,8 @@
             duration: Long,
             ephemeral: Boolean,
             origin: Hotspot? = null,
-            ignorePreviousValues: Boolean = false
+            ignorePreviousValues: Boolean = false,
+            onAnimationEnd: Runnable? = null,
         ): View.OnLayoutChangeListener {
             return object : View.OnLayoutChangeListener {
                 override fun onLayoutChange(
@@ -340,7 +347,8 @@
                             endValues,
                             interpolator,
                             duration,
-                            ephemeral
+                            ephemeral,
+                            onAnimationEnd,
                         )
                     }
                 }
@@ -903,7 +911,8 @@
             endValues: Map<Bound, Int>,
             interpolator: Interpolator,
             duration: Long,
-            ephemeral: Boolean
+            ephemeral: Boolean,
+            onAnimationEnd: Runnable? = null,
         ) {
             val propertyValuesHolders =
                 buildList {
@@ -941,6 +950,9 @@
                             // listener.
                             recursivelyRemoveListener(view)
                         }
+                        if (!cancelled) {
+                            onAnimationEnd?.run()
+                        }
                     }
 
                     override fun onAnimationCancel(animation: Animator?) {
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp
index 40218de..325ede6 100644
--- a/packages/SystemUI/compose/features/Android.bp
+++ b/packages/SystemUI/compose/features/Android.bp
@@ -30,6 +30,7 @@
     ],
 
     static_libs: [
+        "SystemUI-core",
         "SystemUIComposeCore",
 
         "androidx.compose.runtime_runtime",
diff --git a/packages/SystemUI/compose/features/AndroidManifest.xml b/packages/SystemUI/compose/features/AndroidManifest.xml
index 0aea99d..eada40e 100644
--- a/packages/SystemUI/compose/features/AndroidManifest.xml
+++ b/packages/SystemUI/compose/features/AndroidManifest.xml
@@ -16,7 +16,38 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+
     package="com.android.systemui.compose.features">
-
-
+    <application
+        android:name="android.app.Application"
+        android:appComponentFactory="androidx.core.app.AppComponentFactory"
+        tools:replace="android:name,android:appComponentFactory">
+        <!-- Disable providers from SystemUI -->
+        <provider android:name="com.android.systemui.keyguard.KeyguardSliceProvider"
+            android:authorities="com.android.systemui.test.keyguard.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove" />
+        <provider android:name="com.google.android.systemui.keyguard.KeyguardSliceProviderGoogle"
+            android:authorities="com.android.systemui.test.keyguard.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove" />
+        <provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
+            android:authorities="com.android.systemui.test.keyguard.clock.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove" />
+        <provider android:name="com.android.systemui.people.PeopleProvider"
+            android:authorities="com.android.systemui.test.people.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove" />
+        <provider android:name="androidx.core.content.FileProvider"
+            android:authorities="com.android.systemui.test.fileprovider.disabled"
+            android:enabled="false"
+            tools:replace="android:authorities"
+            tools:node="remove"/>
+    </application>
 </manifest>
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
new file mode 100644
index 0000000..2bf1937
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.people.ui.compose
+
+import android.annotation.StringRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.compose.theme.LocalAndroidColorScheme
+import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import kotlinx.coroutines.flow.collect
+
+/**
+ * Compose the screen associated to a [PeopleViewModel].
+ *
+ * @param viewModel the [PeopleViewModel] that should be composed.
+ * @param onResult the callback called with the result of this screen. Callers should usually finish
+ * the Activity/Fragment/View hosting this Composable once a result is available.
+ */
+@Composable
+fun PeopleScreen(
+    viewModel: PeopleViewModel,
+    onResult: (PeopleViewModel.Result) -> Unit,
+) {
+    val priorityTiles by viewModel.priorityTiles.collectAsState()
+    val recentTiles by viewModel.recentTiles.collectAsState()
+
+    // Make sure to refresh the tiles/conversations when the lifecycle is resumed, so that it
+    // updates them when going back to the Activity after leaving it.
+    val lifecycleOwner = LocalLifecycleOwner.current
+    LaunchedEffect(lifecycleOwner, viewModel) {
+        lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+            viewModel.onTileRefreshRequested()
+        }
+    }
+
+    // Call [onResult] this activity when the ViewModel tells us so.
+    LaunchedEffect(viewModel.result) {
+        viewModel.result.collect { result ->
+            if (result != null) {
+                viewModel.clearResult()
+                onResult(result)
+            }
+        }
+    }
+
+    // Make sure to use the Android colors and not the default Material3 colors to have the exact
+    // same colors as the View implementation.
+    val androidColors = LocalAndroidColorScheme.current
+    Surface(
+        color = androidColors.colorBackground,
+        contentColor = androidColors.textColorPrimary,
+        modifier = Modifier.fillMaxSize(),
+    ) {
+        if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
+            PeopleScreenWithConversations(priorityTiles, recentTiles, viewModel::onTileClicked)
+        } else {
+            PeopleScreenEmpty(viewModel::onUserJourneyCancelled)
+        }
+    }
+}
+
+@Composable
+private fun PeopleScreenWithConversations(
+    priorityTiles: List<PeopleTileViewModel>,
+    recentTiles: List<PeopleTileViewModel>,
+    onTileClicked: (PeopleTileViewModel) -> Unit,
+) {
+    Column {
+        Column(
+            Modifier.fillMaxWidth().padding(PeopleSpacePadding),
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Text(
+                stringResource(R.string.select_conversation_title),
+                style = MaterialTheme.typography.headlineSmall,
+                textAlign = TextAlign.Center,
+            )
+
+            Spacer(Modifier.height(24.dp))
+
+            Text(
+                stringResource(R.string.select_conversation_text),
+                Modifier.padding(horizontal = 24.dp),
+                style = MaterialTheme.typography.bodyLarge,
+                textAlign = TextAlign.Center,
+            )
+        }
+
+        LazyColumn(
+            Modifier.fillMaxWidth(),
+            contentPadding =
+                PaddingValues(
+                    top = 16.dp,
+                    bottom = PeopleSpacePadding,
+                    start = 8.dp,
+                    end = 8.dp,
+                )
+        ) {
+            ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked)
+            item { Spacer(Modifier.height(35.dp)) }
+            ConversationList(R.string.recent_conversations, recentTiles, onTileClicked)
+        }
+    }
+}
+
+private fun LazyListScope.ConversationList(
+    @StringRes headerTextResource: Int,
+    tiles: List<PeopleTileViewModel>,
+    onTileClicked: (PeopleTileViewModel) -> Unit
+) {
+    item {
+        Text(
+            stringResource(headerTextResource),
+            Modifier.padding(start = 16.dp),
+            style = MaterialTheme.typography.labelLarge,
+            color = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
+        )
+
+        Spacer(Modifier.height(10.dp))
+    }
+
+    tiles.forEachIndexed { index, tile ->
+        if (index > 0) {
+            item {
+                Divider(
+                    color = LocalAndroidColorScheme.current.colorBackground,
+                    thickness = 2.dp,
+                )
+            }
+        }
+
+        item(tile.key.toString()) {
+            Tile(
+                tile,
+                onTileClicked,
+                withTopCornerRadius = index == 0,
+                withBottomCornerRadius = index == tiles.lastIndex,
+            )
+        }
+    }
+}
+
+@Composable
+private fun Tile(
+    tile: PeopleTileViewModel,
+    onTileClicked: (PeopleTileViewModel) -> Unit,
+    withTopCornerRadius: Boolean,
+    withBottomCornerRadius: Boolean,
+) {
+    val androidColors = LocalAndroidColorScheme.current
+    val cornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
+    val topCornerRadius = if (withTopCornerRadius) cornerRadius else 0.dp
+    val bottomCornerRadius = if (withBottomCornerRadius) cornerRadius else 0.dp
+
+    Surface(
+        color = androidColors.colorSurface,
+        contentColor = androidColors.textColorPrimary,
+        shape =
+            RoundedCornerShape(
+                topStart = topCornerRadius,
+                topEnd = topCornerRadius,
+                bottomStart = bottomCornerRadius,
+                bottomEnd = bottomCornerRadius,
+            ),
+    ) {
+        Row(
+            Modifier.fillMaxWidth().clickable { onTileClicked(tile) }.padding(12.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            Image(
+                tile.icon.asImageBitmap(),
+                // TODO(b/238993727): Add a content description.
+                contentDescription = null,
+                Modifier.size(dimensionResource(R.dimen.avatar_size_for_medium)),
+            )
+
+            Text(
+                tile.username ?: "",
+                Modifier.padding(horizontal = 16.dp),
+                style = MaterialTheme.typography.titleLarge,
+            )
+        }
+    }
+}
+
+/** The padding applied to the PeopleSpace screen. */
+internal val PeopleSpacePadding = 24.dp
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
new file mode 100644
index 0000000..5c9358f
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.people.ui.compose
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.android.systemui.R
+import com.android.systemui.compose.theme.LocalAndroidColorScheme
+
+@Composable
+internal fun PeopleScreenEmpty(
+    onGotItClicked: () -> Unit,
+) {
+    Column(
+        Modifier.fillMaxSize().padding(PeopleSpacePadding),
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        Text(
+            stringResource(R.string.select_conversation_title),
+            style = MaterialTheme.typography.headlineSmall,
+            textAlign = TextAlign.Center,
+        )
+
+        Spacer(Modifier.height(50.dp))
+
+        Text(
+            stringResource(R.string.no_conversations_text),
+            style = MaterialTheme.typography.bodyLarge,
+            textAlign = TextAlign.Center,
+        )
+
+        Spacer(Modifier.weight(1f))
+        ExampleTile()
+        Spacer(Modifier.weight(1f))
+
+        val androidColors = LocalAndroidColorScheme.current
+        Button(
+            onGotItClicked,
+            Modifier.fillMaxWidth().defaultMinSize(minHeight = 56.dp),
+            colors =
+                ButtonDefaults.buttonColors(
+                    containerColor = androidColors.colorAccentPrimary,
+                    contentColor = androidColors.textColorOnAccent,
+                )
+        ) { Text(stringResource(R.string.got_it)) }
+    }
+}
+
+@Composable
+private fun ExampleTile() {
+    val androidColors = LocalAndroidColorScheme.current
+    Surface(
+        shape = RoundedCornerShape(28.dp),
+        color = androidColors.colorSurface,
+        contentColor = androidColors.textColorPrimary,
+    ) {
+        Row(
+            Modifier.padding(vertical = 20.dp, horizontal = 16.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            Column(horizontalAlignment = Alignment.CenterHorizontally) {
+                // TODO(b/238993727): Add a content description.
+                Image(
+                    painterResource(R.drawable.ic_avatar_with_badge),
+                    contentDescription = null,
+                    Modifier.size(40.dp),
+                )
+                Spacer(Modifier.height(2.dp))
+                Text(
+                    stringResource(R.string.empty_user_name),
+                    style = MaterialTheme.typography.labelMedium,
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis,
+                )
+            }
+
+            Spacer(Modifier.width(24.dp))
+
+            Text(
+                stringResource(R.string.empty_status),
+                style = MaterialTheme.typography.labelMedium,
+                maxLines = 1,
+                overflow = TextOverflow.Ellipsis,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/gallery/Android.bp b/packages/SystemUI/compose/gallery/Android.bp
index 40504dc..b0f5cc1 100644
--- a/packages/SystemUI/compose/gallery/Android.bp
+++ b/packages/SystemUI/compose/gallery/Android.bp
@@ -27,6 +27,7 @@
 
     srcs: [
         "src/**/*.kt",
+        ":SystemUI-tests-utils",
     ],
 
     resource_dirs: [
@@ -45,6 +46,14 @@
         "androidx.navigation_navigation-compose",
 
         "androidx.appcompat_appcompat",
+
+        // TODO(b/240431193): Remove the dependencies and depend on
+        // SystemUI-test-utils directly.
+        "androidx.test.runner",
+        "mockito-target-extended-minus-junit4",
+        "testables",
+        "truth-prebuilt",
+        "androidx.test.uiautomator",
     ],
 
     kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
index c341867..bb98fb3 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
@@ -13,7 +13,7 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
@@ -33,6 +33,28 @@
     val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
     val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() }
 
+    val PeopleEmpty =
+        ChildScreen("people_empty") { navController ->
+            EmptyPeopleScreen(onResult = { navController.popBackStack() })
+        }
+    val PeopleFew =
+        ChildScreen("people_few") { navController ->
+            FewPeopleScreen(onResult = { navController.popBackStack() })
+        }
+    val PeopleFull =
+        ChildScreen("people_full") { navController ->
+            FullPeopleScreen(onResult = { navController.popBackStack() })
+        }
+    val People =
+        ParentScreen(
+            "people",
+            mapOf(
+                "Empty" to PeopleEmpty,
+                "Few" to PeopleFew,
+                "Full" to PeopleFull,
+            )
+        )
+
     val Home =
         ParentScreen(
             "home",
@@ -41,20 +63,21 @@
                 "Material colors" to MaterialColors,
                 "Android colors" to AndroidColors,
                 "Example feature" to ExampleFeature,
+                "People" to People,
             )
         )
 }
 
 /** The main content of the app, that shows [GalleryAppScreens.Home] by default. */
 @Composable
-private fun MainContent() {
+private fun MainContent(onControlToggleRequested: () -> Unit) {
     Box(Modifier.fillMaxSize()) {
         val navController = rememberNavController()
         NavHost(
             navController = navController,
             startDestination = GalleryAppScreens.Home.identifier,
         ) {
-            screen(GalleryAppScreens.Home, navController)
+            screen(GalleryAppScreens.Home, navController, onControlToggleRequested)
         }
     }
 }
@@ -69,7 +92,7 @@
     onChangeTheme: () -> Unit,
 ) {
     val systemFontScale = LocalDensity.current.fontScale
-    var fontScale: FontScale by remember {
+    var fontScale: FontScale by rememberSaveable {
         mutableStateOf(
             FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal
         )
@@ -87,7 +110,7 @@
     }
 
     val systemLayoutDirection = LocalLayoutDirection.current
-    var layoutDirection by remember { mutableStateOf(systemLayoutDirection) }
+    var layoutDirection by rememberSaveable { mutableStateOf(systemLayoutDirection) }
     val onChangeLayoutDirection = {
         layoutDirection =
             when (layoutDirection) {
@@ -105,19 +128,24 @@
                 Modifier.fillMaxSize(),
                 color = MaterialTheme.colorScheme.background,
             ) {
-                Column(Modifier.fillMaxSize().systemBarsPadding().padding(16.dp)) {
-                    ConfigurationControls(
-                        theme,
-                        fontScale,
-                        layoutDirection,
-                        onChangeTheme,
-                        onChangeLayoutDirection,
-                        onChangeFontScale,
-                    )
+                Column(Modifier.fillMaxSize().systemBarsPadding()) {
+                    var showControls by rememberSaveable { mutableStateOf(true) }
 
-                    Spacer(Modifier.height(4.dp))
+                    if (showControls) {
+                        ConfigurationControls(
+                            theme,
+                            fontScale,
+                            layoutDirection,
+                            onChangeTheme,
+                            onChangeLayoutDirection,
+                            onChangeFontScale,
+                            Modifier.padding(horizontal = 16.dp),
+                        )
 
-                    MainContent()
+                        Spacer(Modifier.height(4.dp))
+                    }
+
+                    MainContent(onControlToggleRequested = { showControls = !showControls })
                 }
             }
         }
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt
new file mode 100644
index 0000000..2f0df77
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.compose.gallery
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import com.android.systemui.people.emptyPeopleSpaceViewModel
+import com.android.systemui.people.fewPeopleSpaceViewModel
+import com.android.systemui.people.fullPeopleSpaceViewModel
+import com.android.systemui.people.ui.compose.PeopleScreen
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+@Composable
+fun EmptyPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
+    val context = LocalContext.current.applicationContext
+    val viewModel = emptyPeopleSpaceViewModel(context)
+    PeopleScreen(viewModel, onResult)
+}
+
+@Composable
+fun FewPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
+    val context = LocalContext.current.applicationContext
+    val viewModel = fewPeopleSpaceViewModel(context)
+    PeopleScreen(viewModel, onResult)
+}
+
+@Composable
+fun FullPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
+    val context = LocalContext.current.applicationContext
+    val viewModel = fullPeopleSpaceViewModel(context)
+    PeopleScreen(viewModel, onResult)
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
index 467dac04..d7d0d72 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
@@ -52,17 +52,29 @@
 ) : Screen(identifier)
 
 /** Create the navigation graph for [screen]. */
-fun NavGraphBuilder.screen(screen: Screen, navController: NavController) {
+fun NavGraphBuilder.screen(
+    screen: Screen,
+    navController: NavController,
+    onControlToggleRequested: () -> Unit,
+) {
     when (screen) {
         is ChildScreen -> composable(screen.identifier) { screen.content(navController) }
         is ParentScreen -> {
             val menuRoute = "${screen.identifier}_menu"
             navigation(startDestination = menuRoute, route = screen.identifier) {
                 // The menu to navigate to one of the children screens.
-                composable(menuRoute) { ScreenMenu(screen, navController) }
+                composable(menuRoute) {
+                    ScreenMenu(screen, navController, onControlToggleRequested)
+                }
 
                 // The content of the child screens.
-                screen.children.forEach { (_, child) -> screen(child, navController) }
+                screen.children.forEach { (_, child) ->
+                    screen(
+                        child,
+                        navController,
+                        onControlToggleRequested,
+                    )
+                }
             }
         }
     }
@@ -72,8 +84,27 @@
 private fun ScreenMenu(
     screen: ParentScreen,
     navController: NavController,
+    onControlToggleRequested: () -> Unit,
 ) {
-    LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+    LazyColumn(
+        Modifier.padding(horizontal = 16.dp),
+        verticalArrangement = Arrangement.spacedBy(8.dp),
+    ) {
+        item {
+            Surface(
+                Modifier.fillMaxWidth(),
+                color = MaterialTheme.colorScheme.tertiaryContainer,
+                shape = CircleShape,
+            ) {
+                Column(
+                    Modifier.clickable(onClick = onControlToggleRequested).padding(16.dp),
+                    horizontalAlignment = Alignment.CenterHorizontally,
+                ) {
+                    Text("Toggle controls")
+                }
+            }
+        }
+
         screen.children.forEach { (name, child) ->
             item {
                 Surface(
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt
new file mode 100644
index 0000000..0966c32
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.people
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.drawable.Icon
+import androidx.core.graphics.drawable.toIcon
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.people.data.model.PeopleTileModel
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import com.android.systemui.people.widget.PeopleTileKey
+
+/** A [PeopleViewModel] that does not have any conversations. */
+fun emptyPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
+    return fakePeopleSpaceViewModel(context, emptyList(), emptyList())
+}
+
+/** A [PeopleViewModel] that has a few conversations. */
+fun fewPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
+    return fakePeopleSpaceViewModel(
+        context,
+        priorityTiles =
+            listOf(
+                fakeTile(context, id = "0", Color.RED, "Priority"),
+                fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
+            ),
+        recentTiles =
+            listOf(
+                fakeTile(context, id = "2", Color.GREEN, "Recent Important", isImportant = true),
+                fakeTile(context, id = "3", Color.CYAN, "Recent DndBlocking", isDndBlocking = true),
+            ),
+    )
+}
+
+/** A [PeopleViewModel] that has a lot of conversations. */
+fun fullPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
+    return fakePeopleSpaceViewModel(
+        context,
+        priorityTiles =
+            listOf(
+                fakeTile(context, id = "0", Color.RED, "Priority"),
+                fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
+                fakeTile(context, id = "2", Color.GREEN, "Priority Important", isImportant = true),
+                fakeTile(
+                    context,
+                    id = "3",
+                    Color.CYAN,
+                    "Priority DndBlocking",
+                    isDndBlocking = true,
+                ),
+                fakeTile(
+                    context,
+                    id = "4",
+                    Color.MAGENTA,
+                    "Priority NewStory Important",
+                    hasNewStory = true,
+                    isImportant = true,
+                ),
+            ),
+        recentTiles =
+            listOf(
+                fakeTile(
+                    context,
+                    id = "5",
+                    Color.RED,
+                    "Recent NewStory DndBlocking",
+                    hasNewStory = true,
+                    isDndBlocking = true,
+                ),
+                fakeTile(
+                    context,
+                    id = "6",
+                    Color.BLUE,
+                    "Recent Important DndBlocking",
+                    isImportant = true,
+                    isDndBlocking = true,
+                ),
+                fakeTile(
+                    context,
+                    id = "7",
+                    Color.GREEN,
+                    "Recent NewStory Important DndBlocking",
+                    hasNewStory = true,
+                    isImportant = true,
+                    isDndBlocking = true,
+                ),
+                fakeTile(context, id = "8", Color.CYAN, "Recent"),
+                fakeTile(context, id = "9", Color.MAGENTA, "Recent"),
+            ),
+    )
+}
+
+private fun fakePeopleSpaceViewModel(
+    @Application context: Context,
+    priorityTiles: List<PeopleTileModel>,
+    recentTiles: List<PeopleTileModel>,
+): PeopleViewModel {
+    return PeopleViewModel(
+        context,
+        FakePeopleTileRepository(priorityTiles, recentTiles),
+        FakePeopleWidgetRepository(),
+    )
+}
+
+private fun fakeTile(
+    @Application context: Context,
+    id: String,
+    iconColor: Int,
+    username: String,
+    hasNewStory: Boolean = false,
+    isImportant: Boolean = false,
+    isDndBlocking: Boolean = false
+): PeopleTileModel {
+    return PeopleTileModel(
+        PeopleTileKey(id, /* userId= */ 0, /* packageName */ ""),
+        username,
+        fakeUserIcon(context, iconColor),
+        hasNewStory,
+        isImportant,
+        isDndBlocking,
+    )
+}
+
+private fun fakeUserIcon(@Application context: Context, color: Int): Icon {
+    val size = context.resources.getDimensionPixelSize(R.dimen.avatar_size_for_medium)
+    val bitmap =
+        Bitmap.createBitmap(
+            size,
+            size,
+            Bitmap.Config.ARGB_8888,
+        )
+    val canvas = Canvas(bitmap)
+    val paint = Paint().apply { this.color = color }
+    val radius = size / 2f
+    canvas.drawCircle(/* cx= */ radius, /* cy= */ radius, /* radius= */ radius, paint)
+    return bitmap.toIcon()
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index 1008481..1ac1e3a 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -17,6 +17,7 @@
 import android.graphics.drawable.Drawable
 import android.view.View
 import com.android.systemui.plugins.annotations.ProvidesInterface
+import com.android.systemui.shared.regionsampling.RegionDarkness
 import java.io.PrintWriter
 import java.util.Locale
 import java.util.TimeZone
@@ -116,12 +117,3 @@
     val clockId: ClockId,
     val name: String
 )
-
-/**
- * Enum for whether clock region is dark or light.
- */
-enum class RegionDarkness(val isDark: Boolean) {
-    DEFAULT(false),
-    DARK(true),
-    LIGHT(false)
-}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/shared/regionsampling/RegionDarkness.kt b/packages/SystemUI/plugin/src/com/android/systemui/shared/regionsampling/RegionDarkness.kt
new file mode 100644
index 0000000..344fdb8
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/shared/regionsampling/RegionDarkness.kt
@@ -0,0 +1,10 @@
+package com.android.systemui.shared.regionsampling
+
+/**
+ * Enum for whether clock region is dark or light.
+ */
+enum class RegionDarkness(val isDark: Boolean) {
+    DEFAULT(false),
+    DARK(true),
+    LIGHT(false)
+}
diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml
index babe924..8fa2204 100644
--- a/packages/SystemUI/res-keyguard/values/strings.xml
+++ b/packages/SystemUI/res-keyguard/values/strings.xml
@@ -37,8 +37,8 @@
     <!-- When the lock screen is showing and the phone plugged in, and the battery is not fully charged, say that it's wirelessly charging. [CHAR LIMIT=50]  -->
     <string name="keyguard_plugged_in_wireless"><xliff:g id="percentage" example="20%">%s</xliff:g> • Charging wirelessly</string>
 
-    <!-- When the lock screen is showing and the phone plugged in, and the battery is not fully charged, say that it's dock charging. [CHAR LIMIT=50]  -->
-    <string name="keyguard_plugged_in_dock"><xliff:g id="percentage" example="20%">%s</xliff:g> • Charging Dock</string>
+    <!-- When the lock screen is showing and the phone plugged in, and the battery is not fully charged, say that it's charging. [CHAR LIMIT=50]  -->
+    <string name="keyguard_plugged_in_dock"><xliff:g id="percentage" example="20%">%s</xliff:g> • Charging</string>
 
     <!-- When the lock screen is showing and the phone plugged in, and the battery
          is not fully charged, say that it's charging.  -->
@@ -53,7 +53,7 @@
     <string name="keyguard_plugged_in_charging_slowly"><xliff:g id="percentage">%s</xliff:g> • Charging slowly</string>
 
     <!-- When the lock screen is showing and the phone plugged in, and the defend mode is triggered, say that charging is temporarily limited.  -->
-    <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging temporarily limited</string>
+    <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging is paused to protect battery</string>
 
     <!-- On the keyguard screen, when pattern lock is disabled, only tell them to press menu to unlock.  This is shown in small font at the bottom. -->
     <string name="keyguard_instructions_when_pattern_disabled">Press Menu to unlock.</string>
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
index 5f4e310..8ab3e45 100644
--- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
@@ -20,10 +20,11 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:fontFamily="@*android:string/config_clockFontFamily"
-    android:includeFontPadding="false"
     android:textColor="@android:color/white"
     android:format12Hour="@string/dream_time_complication_12_hr_time_format"
     android:format24Hour="@string/dream_time_complication_24_hr_time_format"
     android:shadowColor="@color/keyguard_shadow_color"
     android:shadowRadius="?attr/shadowRadius"
+    android:fontFeatureSettings="pnum, lnum"
+    android:letterSpacing="0.02"
     android:textSize="@dimen/dream_overlay_complication_clock_time_text_size"/>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 1168378..7c1fdd5 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1461,7 +1461,7 @@
     <dimen name="dream_overlay_status_bar_extra_margin">16dp</dimen>
 
     <!-- Dream overlay complications related dimensions -->
-    <dimen name="dream_overlay_complication_clock_time_text_size">100sp</dimen>
+    <dimen name="dream_overlay_complication_clock_time_text_size">86sp</dimen>
     <dimen name="dream_overlay_complication_clock_subtitle_text_size">24sp</dimen>
     <dimen name="dream_overlay_complication_preview_text_size">36sp</dimen>
     <dimen name="dream_overlay_complication_preview_icon_padding">28dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index b844f11..bfdb170 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -884,7 +884,7 @@
     <string name="keyguard_indication_charging_time_slowly"><xliff:g id="percentage">%2$s</xliff:g> • Charging slowly • Full in <xliff:g id="charging_time_left" example="4 hr, 2 min">%1$s</xliff:g></string>
 
     <!-- Indication on the keyguard that is shown when the device is dock charging. [CHAR LIMIT=80]-->
-    <string name="keyguard_indication_charging_time_dock"><xliff:g id="percentage" example="20%">%2$s</xliff:g> • Charging Dock • Full in <xliff:g id="charging_time_left" example="4 hr, 2 min">%1$s</xliff:g></string>
+    <string name="keyguard_indication_charging_time_dock"><xliff:g id="percentage" example="20%">%2$s</xliff:g> • Charging • Full in <xliff:g id="charging_time_left" example="4 hr, 2 min">%1$s</xliff:g></string>
 
     <!-- Related to user switcher --><skip/>
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 8f1959e..a21a78b 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -50,7 +50,12 @@
     defStyleRes: Int = 0
 ) : TextView(context, attrs, defStyleAttr, defStyleRes) {
 
-    private var lastMeasureCall: CharSequence = ""
+    private var lastMeasureCall: CharSequence? = null
+    private var lastDraw: CharSequence? = null
+    private var lastTextUpdate: CharSequence? = null
+    private var lastOnTextChanged: CharSequence? = null
+    private var lastInvalidate: CharSequence? = null
+    private var lastTimeZoneChange: CharSequence? = null
 
     private val time = Calendar.getInstance()
 
@@ -142,7 +147,6 @@
         // relayout if the text didn't actually change.
         if (!TextUtils.equals(text, formattedText)) {
             text = formattedText
-
             // Because the TextLayout may mutate under the hood as a result of the new text, we
             // notify the TextAnimator that it may have changed and request a measure/layout. A
             // crash will occur on the next invocation of setTextStyle if the layout is mutated
@@ -151,18 +155,19 @@
                 textAnimator?.updateLayout(layout)
             }
             requestLayout()
+            lastTextUpdate = getTimestamp()
         }
     }
 
     fun onTimeZoneChanged(timeZone: TimeZone?) {
         time.timeZone = timeZone
         refreshFormat()
+        lastTimeZoneChange = "${getTimestamp()} timeZone=${time.timeZone}"
     }
 
     @SuppressLint("DrawAllocation")
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
-        lastMeasureCall = DateFormat.format(descFormat, System.currentTimeMillis())
         val animator = textAnimator
         if (animator == null) {
             textAnimator = TextAnimator(layout) { invalidate() }
@@ -171,13 +176,34 @@
         } else {
             animator.updateLayout(layout)
         }
+        lastMeasureCall = getTimestamp()
     }
 
     override fun onDraw(canvas: Canvas) {
+        lastDraw = getTimestamp()
         // intentionally doesn't call super.onDraw here or else the text will be rendered twice
         textAnimator?.draw(canvas)
     }
 
+    override fun invalidate() {
+        super.invalidate()
+        lastInvalidate = getTimestamp()
+    }
+
+    private fun getTimestamp(): CharSequence {
+        return "${DateFormat.format("HH:mm:ss", System.currentTimeMillis())} text=$text"
+    }
+
+    override fun onTextChanged(
+            text: CharSequence,
+            start: Int,
+            lengthBefore: Int,
+            lengthAfter: Int
+    ) {
+        super.onTextChanged(text, start, lengthBefore, lengthAfter)
+        lastOnTextChanged = "${getTimestamp()}"
+    }
+
     fun setLineSpacingScale(scale: Float) {
         lineSpacingScale = scale
         setLineSpacing(0f, lineSpacingScale)
@@ -370,7 +396,12 @@
         pw.println("    measuredWidth=$measuredWidth")
         pw.println("    measuredHeight=$measuredHeight")
         pw.println("    singleLineInternal=$isSingleLineInternal")
+        pw.println("    lastTextUpdate=$lastTextUpdate")
+        pw.println("    lastOnTextChanged=$lastOnTextChanged")
+        pw.println("    lastInvalidate=$lastInvalidate")
         pw.println("    lastMeasureCall=$lastMeasureCall")
+        pw.println("    lastDraw=$lastDraw")
+        pw.println("    lastTimeZoneChange=$lastTimeZoneChange")
         pw.println("    currText=$text")
         pw.println("    currTimeContextDesc=$contentDescription")
         pw.println("    time=$time")
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 3d72f15..6c49186 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -26,8 +26,8 @@
 import com.android.systemui.plugins.ClockId
 import com.android.systemui.plugins.ClockMetadata
 import com.android.systemui.plugins.ClockProvider
-import com.android.systemui.plugins.RegionDarkness
 import com.android.systemui.shared.R
+import com.android.systemui.shared.regionsampling.RegionDarkness
 import java.io.PrintWriter
 import java.util.Locale
 import java.util.TimeZone
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
index deabc27..0146795 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSamplingInstance.kt
@@ -18,7 +18,6 @@
 import android.graphics.Rect
 import android.view.View
 import androidx.annotation.VisibleForTesting
-import com.android.systemui.plugins.RegionDarkness
 import com.android.systemui.shared.navigationbar.RegionSamplingHelper
 import com.android.systemui.shared.navigationbar.RegionSamplingHelper.SamplingCallback
 import java.io.PrintWriter
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 2f9bc1c..d752852 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -54,6 +54,7 @@
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
 import android.hardware.SensorPrivacyManager;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricSourceType;
@@ -2018,12 +2019,13 @@
         // in case authenticators aren't registered yet at this point:
         mAuthController.addCallback(new AuthController.Callback() {
             @Override
-            public void onAllAuthenticatorsRegistered() {
+            public void onAllAuthenticatorsRegistered(
+                    @BiometricAuthenticator.Modality int modality) {
                 mainExecutor.execute(() -> updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE));
             }
 
             @Override
-            public void onEnrollmentsChanged() {
+            public void onEnrollmentsChanged(@BiometricAuthenticator.Modality int modality) {
                 mainExecutor.execute(() -> updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE));
             }
         });
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 06e1828..d6974df 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -16,6 +16,7 @@
 
 package com.android.keyguard;
 
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT;
 
 import static com.android.keyguard.LockIconView.ICON_FINGERPRINT;
@@ -29,6 +30,7 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.AnimatedStateListDrawable;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.biometrics.BiometricSourceType;
 import android.os.Process;
 import android.os.VibrationAttributes;
@@ -701,13 +703,17 @@
 
     private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
         @Override
-        public void onAllAuthenticatorsRegistered() {
-            updateUdfpsConfig();
+        public void onAllAuthenticatorsRegistered(@BiometricAuthenticator.Modality int modality) {
+            if (modality == TYPE_FINGERPRINT) {
+                updateUdfpsConfig();
+            }
         }
 
         @Override
-        public void onEnrollmentsChanged() {
-            updateUdfpsConfig();
+        public void onEnrollmentsChanged(@BiometricAuthenticator.Modality int modality) {
+            if (modality == TYPE_FINGERPRINT) {
+                updateUdfpsConfig();
+            }
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 47ff59c..282f251 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -46,6 +46,7 @@
 import android.hardware.display.DisplayManager;
 import android.hardware.face.FaceManager;
 import android.hardware.face.FaceSensorPropertiesInternal;
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
@@ -156,25 +157,6 @@
         }
     };
 
-    private final IFingerprintAuthenticatorsRegisteredCallback
-            mFingerprintAuthenticatorsRegisteredCallback =
-            new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
-                @Override
-                public void onAllAuthenticatorsRegistered(
-                        List<FingerprintSensorPropertiesInternal> sensors) {
-                    mHandler.post(() -> handleAllFingerprintAuthenticatorsRegistered(sensors));
-                }
-            };
-
-    private final BiometricStateListener mBiometricStateListener =
-            new BiometricStateListener() {
-                @Override
-                public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) {
-                    mHandler.post(
-                            () -> handleEnrollmentsChanged(userId, sensorId, hasEnrollments));
-                }
-            };
-
     @VisibleForTesting
     final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
         @Override
@@ -249,8 +231,8 @@
             List<FingerprintSensorPropertiesInternal> sensors) {
         mExecution.assertIsMainThread();
         if (DEBUG) {
-            Log.d(TAG, "handleAllAuthenticatorsRegistered | sensors: " + Arrays.toString(
-                    sensors.toArray()));
+            Log.d(TAG, "handleAllFingerprintAuthenticatorsRegistered | sensors: "
+                    + Arrays.toString(sensors.toArray()));
         }
         mAllFingerprintAuthenticatorsRegistered = true;
         mFpProps = sensors;
@@ -292,15 +274,42 @@
             mSidefpsController = mSidefpsControllerFactory.get();
         }
 
-        mFingerprintManager.registerBiometricStateListener(mBiometricStateListener);
+        mFingerprintManager.registerBiometricStateListener(new BiometricStateListener() {
+            @Override
+            public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) {
+                mHandler.post(() -> handleEnrollmentsChanged(
+                        TYPE_FINGERPRINT, userId, sensorId, hasEnrollments));
+            }
+        });
         updateFingerprintLocation();
 
         for (Callback cb : mCallbacks) {
-            cb.onAllAuthenticatorsRegistered();
+            cb.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT);
         }
     }
 
-    private void handleEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) {
+    private void handleAllFaceAuthenticatorsRegistered(List<FaceSensorPropertiesInternal> sensors) {
+        mExecution.assertIsMainThread();
+        if (DEBUG) {
+            Log.d(TAG, "handleAllFaceAuthenticatorsRegistered | sensors: " + Arrays.toString(
+                    sensors.toArray()));
+        }
+
+        mFaceManager.registerBiometricStateListener(new BiometricStateListener() {
+            @Override
+            public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) {
+                mHandler.post(() -> handleEnrollmentsChanged(
+                        TYPE_FACE, userId, sensorId, hasEnrollments));
+            }
+        });
+
+        for (Callback cb : mCallbacks) {
+            cb.onAllAuthenticatorsRegistered(TYPE_FACE);
+        }
+    }
+
+    private void handleEnrollmentsChanged(@Modality int modality, int userId, int sensorId,
+            boolean hasEnrollments) {
         mExecution.assertIsMainThread();
         Log.d(TAG, "handleEnrollmentsChanged, userId: " + userId + ", sensorId: " + sensorId
                 + ", hasEnrollments: " + hasEnrollments);
@@ -314,7 +323,7 @@
             }
         }
         for (Callback cb : mCallbacks) {
-            cb.onEnrollmentsChanged();
+            cb.onEnrollmentsChanged(modality);
         }
     }
 
@@ -700,7 +709,26 @@
 
         if (mFingerprintManager != null) {
             mFingerprintManager.addAuthenticatorsRegisteredCallback(
-                    mFingerprintAuthenticatorsRegisteredCallback);
+                    new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
+                        @Override
+                        public void onAllAuthenticatorsRegistered(
+                                List<FingerprintSensorPropertiesInternal> sensors) {
+                            mHandler.post(() ->
+                                    handleAllFingerprintAuthenticatorsRegistered(sensors));
+                        }
+                    });
+        }
+        if (mFaceManager != null) {
+            mFaceManager.addAuthenticatorsRegisteredCallback(
+                    new IFaceAuthenticatorsRegisteredCallback.Stub() {
+                        @Override
+                        public void onAllAuthenticatorsRegistered(
+                                List<FaceSensorPropertiesInternal> sensors) {
+                            mHandler.post(() ->
+                                    handleAllFaceAuthenticatorsRegistered(sensors));
+                        }
+                    }
+            );
         }
 
         mStableDisplaySize = mDisplayManager.getStableDisplaySize();
@@ -1116,13 +1144,13 @@
          * Called when authenticators are registered. If authenticators are already
          * registered before this call, this callback will never be triggered.
          */
-        default void onAllAuthenticatorsRegistered() {}
+        default void onAllAuthenticatorsRegistered(@Modality int modality) {}
 
         /**
-         * Called when UDFPS enrollments have changed. This is called after boot and on changes to
+         * Called when enrollments have changed. This is called after boot and on changes to
          * enrollment.
          */
-        default void onEnrollmentsChanged() {}
+        default void onEnrollmentsChanged(@Modality int modality) {}
 
         /**
          * Called when the biometric prompt starts showing.
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index 38fab8f..fd3f600 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -308,7 +308,7 @@
 
     private val authControllerCallback =
         object : AuthController.Callback {
-            override fun onAllAuthenticatorsRegistered() {
+            override fun onAllAuthenticatorsRegistered(modality: Int) {
                 updateUdfpsDependentParams()
                 updateSensorLocation()
             }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index cf50f7f..d1589b2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -517,8 +517,6 @@
                                     scaledMajor);
                             Log.v(TAG, "onTouch | finger down: " + touchInfo);
                             mTouchLogTime = mSystemClock.elapsedRealtime();
-                            mPowerManager.userActivity(mSystemClock.uptimeMillis(),
-                                    PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0);
                             handled = true;
                         } else if (sinceLastLog >= MIN_TOUCH_LOG_INTERVAL) {
                             Log.v(TAG, "onTouch | finger move: " + touchInfo);
@@ -846,6 +844,9 @@
             return;
         }
         mLatencyTracker.onActionStart(LatencyTracker.ACTION_UDFPS_ILLUMINATE);
+        // Refresh screen timeout and boost process priority if possible.
+        mPowerManager.userActivity(mSystemClock.uptimeMillis(),
+                PowerManager.USER_ACTIVITY_EVENT_TOUCH, 0);
 
         if (!mOnFingerDown) {
             playStartHaptic();
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
index 49e378e..d96476f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
@@ -99,12 +99,11 @@
         mProgressColor = context.getColor(R.color.udfps_enroll_progress);
         final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
         mIsAccessibilityEnabled = am.isTouchExplorationEnabled();
+        mOnFirstBucketFailedColor = context.getColor(R.color.udfps_moving_target_fill_error);
         if (!mIsAccessibilityEnabled) {
             mHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
-            mOnFirstBucketFailedColor = context.getColor(R.color.udfps_moving_target_fill_error);
         } else {
             mHelpColor = context.getColor(R.color.udfps_enroll_progress_help_with_talkback);
-            mOnFirstBucketFailedColor = mHelpColor;
         }
         mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark);
         mCheckmarkDrawable.mutate();
@@ -167,7 +166,8 @@
     }
 
     private void updateProgress(int remainingSteps, int totalSteps, boolean showingHelp) {
-        if (mRemainingSteps == remainingSteps && mTotalSteps == totalSteps) {
+        if (mRemainingSteps == remainingSteps && mTotalSteps == totalSteps
+                && mShowingHelp == showingHelp) {
             return;
         }
 
@@ -197,6 +197,7 @@
             }
         }
 
+        mShowingHelp = showingHelp;
         mRemainingSteps = remainingSteps;
         mTotalSteps = totalSteps;
 
diff --git a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
index 47ea27f..0839338 100644
--- a/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/charging/WirelessChargingLayout.java
@@ -30,6 +30,8 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import androidx.core.graphics.ColorUtils;
+
 import com.android.settingslib.Utils;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
@@ -42,7 +44,8 @@
  * @hide
  */
 final class WirelessChargingLayout extends FrameLayout {
-    private static final long RIPPLE_ANIMATION_DURATION = 1500;
+    private static final long CIRCLE_RIPPLE_ANIMATION_DURATION = 1500;
+    private static final long ROUNDED_BOX_RIPPLE_ANIMATION_DURATION = 1750;
     private static final int SCRIM_COLOR = 0x4C000000;
     private static final int SCRIM_FADE_DURATION = 300;
     private RippleView mRippleView;
@@ -131,17 +134,30 @@
                 "backgroundColor", SCRIM_COLOR, Color.TRANSPARENT);
         scrimFadeOutAnimator.setDuration(SCRIM_FADE_DURATION);
         scrimFadeOutAnimator.setInterpolator(Interpolators.LINEAR);
-        scrimFadeOutAnimator.setStartDelay(RIPPLE_ANIMATION_DURATION - SCRIM_FADE_DURATION);
+        scrimFadeOutAnimator.setStartDelay((rippleShape == RippleShape.CIRCLE
+                ? CIRCLE_RIPPLE_ANIMATION_DURATION : ROUNDED_BOX_RIPPLE_ANIMATION_DURATION)
+                - SCRIM_FADE_DURATION);
         AnimatorSet animatorSetScrim = new AnimatorSet();
         animatorSetScrim.playTogether(scrimFadeInAnimator, scrimFadeOutAnimator);
         animatorSetScrim.start();
 
         mRippleView = findViewById(R.id.wireless_charging_ripple);
         mRippleView.setupShader(rippleShape);
+        if (mRippleView.getRippleShape() == RippleShape.ROUNDED_BOX) {
+            mRippleView.setDuration(ROUNDED_BOX_RIPPLE_ANIMATION_DURATION);
+            mRippleView.setSparkleStrength(0.22f);
+            int color = Utils.getColorAttr(mRippleView.getContext(),
+                    android.R.attr.colorAccent).getDefaultColor();
+            mRippleView.setColor(ColorUtils.setAlphaComponent(color, 28));
+        } else {
+            mRippleView.setDuration(CIRCLE_RIPPLE_ANIMATION_DURATION);
+            mRippleView.setColor(Utils.getColorAttr(mRippleView.getContext(),
+                    android.R.attr.colorAccent).getDefaultColor());
+        }
+
         OnAttachStateChangeListener listener = new OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View view) {
-                mRippleView.setDuration(RIPPLE_ANIMATION_DURATION);
                 mRippleView.startRipple();
                 mRippleView.removeOnAttachStateChangeListener(this);
             }
@@ -232,13 +248,13 @@
             int height = getMeasuredHeight();
             mRippleView.setCenter(width * 0.5f, height * 0.5f);
             if (mRippleView.getRippleShape() == RippleShape.ROUNDED_BOX) {
-                mRippleView.setMaxSize(width * 1.5f, height * 1.5f);
+                // Those magic numbers are introduced for visual polish. This aspect ratio maps with
+                // the tablet's docking station.
+                mRippleView.setMaxSize(width * 1.36f, height * 1.46f);
             } else {
                 float maxSize = Math.max(width, height);
                 mRippleView.setMaxSize(maxSize, maxSize);
             }
-            mRippleView.setColor(Utils.getColorAttr(mRippleView.getContext(),
-                    android.R.attr.colorAccent).getDefaultColor());
         }
 
         super.onLayout(changed, left, top, right, bottom);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 5dff4a5..4157728 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -26,6 +26,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.internal.logging.UiEventLogger;
 import com.android.keyguard.KeyguardViewController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -61,6 +62,7 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.KeyguardEnvironmentImpl;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BatteryControllerImpl;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -191,7 +193,9 @@
             GroupMembershipManager groupManager,
             VisualStabilityProvider visualStabilityProvider,
             ConfigurationController configurationController,
-            @Main Handler handler) {
+            @Main Handler handler,
+            AccessibilityManagerWrapper accessibilityManagerWrapper,
+            UiEventLogger uiEventLogger) {
         return new HeadsUpManagerPhone(
                 context,
                 headsUpManagerLogger,
@@ -200,7 +204,9 @@
                 groupManager,
                 visualStabilityProvider,
                 configurationController,
-                handler
+                handler,
+                accessibilityManagerWrapper,
+                uiEventLogger
         );
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index f32ea35..2dadf57 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -18,6 +18,7 @@
 
 import android.app.INotificationManager;
 import android.content.Context;
+import android.service.dreams.IDreamManager;
 
 import androidx.annotation.Nullable;
 
@@ -213,6 +214,7 @@
             ShadeController shadeController,
             @Nullable IStatusBarService statusBarService,
             INotificationManager notificationManager,
+            IDreamManager dreamManager,
             NotificationVisibilityProvider visibilityProvider,
             NotificationInterruptStateProvider interruptionStateProvider,
             ZenModeController zenModeController,
@@ -230,6 +232,7 @@
                 shadeController,
                 statusBarService,
                 notificationManager,
+                dreamManager,
                 visibilityProvider,
                 interruptionStateProvider,
                 zenModeController,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java
index a9e310d..7da2cf1 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenState.java
@@ -16,12 +16,15 @@
 
 package com.android.systemui.doze;
 
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
 import static com.android.systemui.doze.DozeMachine.State.DOZE;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_PAUSED;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_PAUSING;
 import static com.android.systemui.doze.DozeMachine.State.DOZE_PULSE_DONE;
 
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.os.Handler;
 import android.util.Log;
 import android.view.Display;
@@ -232,13 +235,17 @@
 
     private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
         @Override
-        public void onAllAuthenticatorsRegistered() {
-            updateUdfpsController();
+        public void onAllAuthenticatorsRegistered(@BiometricAuthenticator.Modality int modality) {
+            if (modality == TYPE_FINGERPRINT) {
+                updateUdfpsController();
+            }
         }
 
         @Override
-        public void onEnrollmentsChanged() {
-            updateUdfpsController();
+        public void onEnrollmentsChanged(@BiometricAuthenticator.Modality int modality) {
+            if (modality == TYPE_FINGERPRINT) {
+                updateUdfpsController();
+            }
         }
     };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index da6c163..997a6e5 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.doze;
 
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
 import static com.android.systemui.doze.DozeLog.REASON_SENSOR_QUICK_PICKUP;
 import static com.android.systemui.doze.DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS;
 import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_DISPLAY;
@@ -29,6 +31,7 @@
 import android.hardware.SensorManager;
 import android.hardware.TriggerEvent;
 import android.hardware.TriggerEventListener;
+import android.hardware.biometrics.BiometricAuthenticator;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.net.Uri;
 import android.os.Handler;
@@ -835,13 +838,17 @@
 
     private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
         @Override
-        public void onAllAuthenticatorsRegistered() {
-            updateUdfpsEnrolled();
+        public void onAllAuthenticatorsRegistered(@BiometricAuthenticator.Modality int modality) {
+            if (modality == TYPE_FINGERPRINT) {
+                updateUdfpsEnrolled();
+            }
         }
 
         @Override
-        public void onEnrollmentsChanged() {
-            updateUdfpsEnrolled();
+        public void onEnrollmentsChanged(@BiometricAuthenticator.Modality int modality) {
+            if (modality == TYPE_FINGERPRINT) {
+                updateUdfpsEnrolled();
+            }
         }
 
         private void updateUdfpsEnrolled() {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java
index 5250d44..7d9f105 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java
@@ -37,7 +37,7 @@
 public interface DreamClockTimeComplicationModule {
     String DREAM_CLOCK_TIME_COMPLICATION_VIEW = "clock_time_complication_view";
     String TAG_WEIGHT = "'wght' ";
-    int WEIGHT = 200;
+    int WEIGHT = 400;
 
     /**
      * Provides the complication view.
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 6eb77bd..1f356cb 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -92,7 +92,7 @@
      * Whether the KeyguardBottomArea(View|Controller) should use the modern architecture or the old
      * one.
      */
-    public static final UnreleasedFlag MODERN_BOTTOM_AREA = new UnreleasedFlag(206, true);
+    public static final ReleasedFlag MODERN_BOTTOM_AREA = new ReleasedFlag(206, true);
 
     public static final UnreleasedFlag LOCKSCREEN_CUSTOM_CLOCKS = new UnreleasedFlag(207);
   
@@ -151,8 +151,8 @@
     public static final ResourceBooleanFlag STATUS_BAR_USER_SWITCHER =
             new ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip);
 
-    public static final UnreleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE =
-            new UnreleasedFlag(603, false);
+    public static final ReleasedFlag STATUS_BAR_LETTERBOX_APPEARANCE =
+            new ReleasedFlag(603, false);
 
     public static final UnreleasedFlag NEW_STATUS_BAR_PIPELINE = new UnreleasedFlag(604, true);
 
@@ -182,6 +182,7 @@
     public static final UnreleasedFlag MEDIA_SESSION_ACTIONS = new UnreleasedFlag(901);
     public static final ReleasedFlag MEDIA_NEARBY_DEVICES = new ReleasedFlag(903);
     public static final ReleasedFlag MEDIA_MUTE_AWAIT = new ReleasedFlag(904);
+    public static final UnreleasedFlag MEDIA_DREAM_COMPLICATION = new UnreleasedFlag(905);
 
     // 1000 - dock
     public static final ReleasedFlag SIMULATE_DOCK_THROUGH_CHARGING =
@@ -236,6 +237,10 @@
     // 1300 - screenshots
 
     public static final UnreleasedFlag SCREENSHOT_REQUEST_PROCESSOR = new UnreleasedFlag(1300);
+    public static final UnreleasedFlag SCREENSHOT_WORK_PROFILE_POLICY = new UnreleasedFlag(1301);
+
+    // 1400 - columbus, b/242800729
+    public static final UnreleasedFlag QUICK_TAP_IN_PCC = new UnreleasedFlag(1400);
 
     // Pay no attention to the reflection behind the curtain.
     // ========================== Curtain ==========================
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index ca65d12..da5819a 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -90,6 +90,8 @@
 import android.widget.LinearLayout;
 import android.widget.ListPopupWindow;
 import android.widget.TextView;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
 import androidx.lifecycle.Lifecycle;
@@ -155,6 +157,8 @@
     public static final String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions";
     public static final String SYSTEM_DIALOG_REASON_DREAM = "dream";
 
+    private static final boolean DEBUG = false;
+
     private static final String TAG = "GlobalActionsDialogLite";
 
     private static final String INTERACTION_JANK_TAG = "global_actions";
@@ -2177,6 +2181,11 @@
 
         protected ViewGroup mContainer;
 
+        private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
+            logOnBackInvocation();
+            dismiss();
+        };
+
         @VisibleForTesting
         protected GestureDetector.SimpleOnGestureListener mGestureListener =
                 new GestureDetector.SimpleOnGestureListener() {
@@ -2221,6 +2230,16 @@
                     }
                 };
 
+
+        // this exists so that we can point it to a mock during Unit Testing
+        private OnBackInvokedDispatcher mOverriddenBackDispatcher;
+
+        // the following method exists so that a Unit Test can supply a `OnBackInvokedDispatcher`
+        @VisibleForTesting
+        void setBackDispatcherOverride(OnBackInvokedDispatcher mockDispatcher) {
+            mOverriddenBackDispatcher = mockDispatcher;
+        }
+
         ActionsDialogLite(Context context, int themeRes, MyAdapter adapter,
                 MyOverflowAdapter overflowAdapter,
                 SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService,
@@ -2254,6 +2273,22 @@
             super.onCreate(savedInstanceState);
             initializeLayout();
             mWindowDimAmount = getWindow().getAttributes().dimAmount;
+            getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                    OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback);
+            if (DEBUG) Log.d(TAG, "OnBackInvokedCallback handler registered");
+        }
+
+        @VisibleForTesting
+        @Override
+        public OnBackInvokedDispatcher getOnBackInvokedDispatcher() {
+            if (mOverriddenBackDispatcher != null) return mOverriddenBackDispatcher;
+            else return super.getOnBackInvokedDispatcher();
+        }
+
+        @Override
+        public void onDetachedFromWindow() {
+            getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mOnBackInvokedCallback);
+            if (DEBUG) Log.d(TAG, "OnBackInvokedCallback handler unregistered");
         }
 
         @Override
@@ -2453,7 +2488,12 @@
         @Override
         public void onBackPressed() {
             super.onBackPressed();
+            logOnBackInvocation();
+        }
+
+        private void logOnBackInvocation() {
             mUiEventLogger.log(GlobalActionsEvent.GA_CLOSE_BACK);
+            if (DEBUG) Log.d(TAG, "onBack invoked");
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 3eb3c80..6dfbd42 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -323,6 +323,8 @@
         if (sEnableRemoteKeyguardOccludeAnimation) {
             Slog.d(TAG, "KeyguardService registerRemote: TRANSIT_KEYGUARD_(UN)OCCLUDE");
             // Register for occluding
+            final RemoteTransition occludeTransition = new RemoteTransition(
+                    mOccludeAnimation, getIApplicationThread());
             TransitionFilter f = new TransitionFilter();
             f.mFlags = TRANSIT_FLAG_KEYGUARD_LOCKED;
             f.mRequirements = new TransitionFilter.Requirement[]{
@@ -337,10 +339,11 @@
             f.mRequirements[1].mMustBeIndependent = false;
             f.mRequirements[1].mFlags = FLAG_OCCLUDES_KEYGUARD;
             f.mRequirements[1].mModes = new int[]{TRANSIT_CLOSE, TRANSIT_TO_BACK};
-            mShellTransitions.registerRemote(f,
-                    new RemoteTransition(mOccludeAnimation, getIApplicationThread()));
+            mShellTransitions.registerRemote(f, occludeTransition);
 
             // Now register for un-occlude.
+            final RemoteTransition unoccludeTransition = new RemoteTransition(
+                    mUnoccludeAnimation, getIApplicationThread());
             f = new TransitionFilter();
             f.mFlags = TRANSIT_FLAG_KEYGUARD_LOCKED;
             f.mRequirements = new TransitionFilter.Requirement[]{
@@ -358,8 +361,23 @@
             f.mRequirements[0].mMustBeIndependent = false;
             f.mRequirements[0].mFlags = FLAG_OCCLUDES_KEYGUARD;
             f.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
-            mShellTransitions.registerRemote(f,
-                    new RemoteTransition(mUnoccludeAnimation, getIApplicationThread()));
+            mShellTransitions.registerRemote(f, unoccludeTransition);
+
+            // Register for specific transition type.
+            // Above filter cannot fulfill all conditions.
+            // E.g. close top activity while screen off but next activity is occluded, this should
+            // an occluded transition, but since the activity is invisible, the condition would
+            // match unoccluded transition.
+            // But on the contrary, if we add above condition in occluded transition, then when user
+            // trying to dismiss occluded activity when unlock keyguard, the condition would match
+            // occluded transition.
+            f = new TransitionFilter();
+            f.mTypeSet = new int[]{TRANSIT_KEYGUARD_OCCLUDE};
+            mShellTransitions.registerRemote(f, occludeTransition);
+
+            f = new TransitionFilter();
+            f.mTypeSet = new int[]{TRANSIT_KEYGUARD_UNOCCLUDE};
+            mShellTransitions.registerRemote(f, unoccludeTransition);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index b4f40e2..4a7346e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2377,10 +2377,10 @@
     private void handleHide() {
         Trace.beginSection("KeyguardViewMediator#handleHide");
 
-        // It's possible that the device was unlocked in a dream state. It's time to wake up.
-        if (mAodShowing || mDreamOverlayShowing) {
-            PowerManager pm = mContext.getSystemService(PowerManager.class);
-            pm.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+        // It's possible that the device was unlocked (via BOUNCER) while dozing. It's time to
+        // wake up.
+        if (mAodShowing) {
+            mPM.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
                     "com.android.systemui:BOUNCER_DOZING");
         }
 
@@ -2409,6 +2409,13 @@
                             null /* nonApps */, null /* finishedCallback */);
                 });
             }
+
+            // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while
+            // dreaming. It's time to wake up.
+            if (mDreamOverlayShowing) {
+                mPM.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+                        "com.android.systemui:UNLOCK_DREAMING");
+            }
         }
         Trace.endSection();
     }
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 c858bc3..c2a8764 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -84,6 +84,14 @@
         return factory.create("LSShadeTransitionLog", 50);
     }
 
+    /** Provides a logging buffer for Shade messages. */
+    @Provides
+    @SysUISingleton
+    @ShadeLog
+    public static LogBuffer provideShadeLogBuffer(LogBufferFactory factory) {
+        return factory.create("ShadeLog", 500, false);
+    }
+
     /** Provides a logging buffer for all logs related to managing notification sections. */
     @Provides
     @SysUISingleton
@@ -262,7 +270,7 @@
     @SysUISingleton
     @StatusBarConnectivityLog
     public static LogBuffer provideStatusBarConnectivityBuffer(LogBufferFactory factory) {
-        return factory.create("StatusBarConnectivityLog", 64);
+        return factory.create("SbConnectivity", 64);
     }
 
     /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
similarity index 60%
copy from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
copy to packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
index 8dde897..bd0d298 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/ShadeLog.java
@@ -14,10 +14,20 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spaprivileged.framework.app
+package com.android.systemui.log.dagger;
 
-import android.content.pm.ApplicationInfo
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-interface AppRecord {
-    val app: ApplicationInfo
+import com.android.systemui.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/** A {@link LogBuffer} for Shade touch handling messages. */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface ShadeLog {
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
index 237b505..32600fb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
@@ -18,19 +18,25 @@
 
 import android.content.Context
 import android.content.res.Configuration
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
 import android.view.View
 import android.view.ViewGroup
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.dagger.MediaModule.KEYGUARD
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.stack.MediaContainerView
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.LargeScreenUtils
+import com.android.systemui.util.settings.SecureSettings
 import javax.inject.Inject
 import javax.inject.Named
 
@@ -43,9 +49,10 @@
     @param:Named(KEYGUARD) private val mediaHost: MediaHost,
     private val bypassController: KeyguardBypassController,
     private val statusBarStateController: SysuiStatusBarStateController,
-    private val notifLockscreenUserManager: NotificationLockscreenUserManager,
     private val context: Context,
-    configurationController: ConfigurationController
+    private val secureSettings: SecureSettings,
+    @Main private val handler: Handler,
+    configurationController: ConfigurationController,
 ) {
 
     init {
@@ -60,6 +67,24 @@
             }
         })
 
+        val settingsObserver: ContentObserver = object : ContentObserver(handler) {
+            override fun onChange(selfChange: Boolean, uri: Uri?) {
+                if (uri == lockScreenMediaPlayerUri) {
+                    allowMediaPlayerOnLockScreen =
+                            secureSettings.getBoolForUser(
+                                    Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                                    true,
+                                    UserHandle.USER_CURRENT
+                            )
+                    refreshMediaPosition()
+                }
+            }
+        }
+        secureSettings.registerContentObserverForUser(
+                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                settingsObserver,
+                UserHandle.USER_ALL)
+
         // First let's set the desired state that we want for this host
         mediaHost.expansion = MediaHostState.EXPANDED
         mediaHost.showsOnlyActiveMedia = true
@@ -101,6 +126,13 @@
     private var splitShadeContainer: ViewGroup? = null
 
     /**
+     * Track the media player setting status on lock screen.
+     */
+    private var allowMediaPlayerOnLockScreen: Boolean = true
+    private val lockScreenMediaPlayerUri =
+            secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
+
+    /**
      * Attaches media container in single pane mode, situated at the top of the notifications list
      */
     fun attachSinglePaneContainer(mediaView: MediaContainerView?) {
@@ -164,7 +196,7 @@
         visible = mediaHost.visible &&
                 !bypassController.bypassEnabled &&
                 keyguardOrUserSwitcher &&
-                notifLockscreenUserManager.shouldShowLockscreenNotifications()
+                allowMediaPlayerOnLockScreen
         if (visible) {
             showMediaPlayer()
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 88a1b17..7b497ad 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media
 
 import android.app.Notification
+import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME
 import android.app.PendingIntent
 import android.app.smartspace.SmartspaceConfig
 import android.app.smartspace.SmartspaceManager
@@ -27,6 +28,7 @@
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.graphics.Bitmap
 import android.graphics.ImageDecoder
@@ -57,8 +59,8 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
-import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
 import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
+import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
 import com.android.systemui.statusbar.notification.row.HybridGroupManager
 import com.android.systemui.tuner.TunerService
 import com.android.systemui.util.Assert
@@ -633,9 +635,14 @@
         }
         val mediaController = mediaControllerFactory.create(token)
         val metadata = mediaController.metadata
+        val notif: Notification = sbn.notification
+
+        val appInfo = notif.extras.getParcelable(
+            Notification.EXTRA_BUILDER_APPLICATION_INFO,
+            ApplicationInfo::class.java
+        ) ?: getAppInfoFromPackage(sbn.packageName)
 
         // Album art
-        val notif: Notification = sbn.notification
         var artworkBitmap = metadata?.let { loadBitmapFromUri(it) }
         if (artworkBitmap == null) {
             artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART)
@@ -650,8 +657,7 @@
         }
 
         // App name
-        val builder = Notification.Builder.recoverBuilder(context, notif)
-        val app = builder.loadHeaderAppName()
+        val appName = getAppName(sbn, appInfo)
 
         // App Icon
         val smallIcon = sbn.notification.smallIcon
@@ -712,12 +718,7 @@
 
         val currentEntry = mediaEntries.get(key)
         val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
-        val appUid = try {
-            context.packageManager.getApplicationInfo(sbn.packageName, 0)?.uid!!
-        } catch (e: PackageManager.NameNotFoundException) {
-            Log.w(TAG, "Could not get app UID for ${sbn.packageName}", e)
-            Process.INVALID_UID
-        }
+        val appUid = appInfo?.uid ?: Process.INVALID_UID
 
         if (logEvent) {
             logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
@@ -730,7 +731,7 @@
             val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
             val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
             val active = mediaEntries[key]?.active ?: true
-            onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, app,
+            onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, appName,
                     smallIcon, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed,
                     semanticActions, sbn.packageName, token, notif.contentIntent, device,
                     active, resumeAction = resumeAction, playbackLocation = playbackLocation,
@@ -740,6 +741,28 @@
         }
     }
 
+    private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? {
+        try {
+            return context.packageManager.getApplicationInfo(packageName, 0)
+        } catch (e: PackageManager.NameNotFoundException) {
+            Log.w(TAG, "Could not get app info for $packageName", e)
+        }
+        return null
+    }
+
+    private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String {
+        val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME)
+        if (name != null) {
+            return name
+        }
+
+        return if (appInfo != null) {
+            context.packageManager.getApplicationLabel(appInfo).toString()
+        } else {
+            sbn.packageName
+        }
+    }
+
     /**
      * Generate action buttons based on notification actions
      */
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index 458ed40..ae4c7c7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -22,7 +22,12 @@
 import android.annotation.IntDef
 import android.content.Context
 import android.content.res.Configuration
+import android.database.ContentObserver
 import android.graphics.Rect
+import android.net.Uri
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings
 import android.util.Log
 import android.util.MathUtils
 import android.view.View
@@ -33,11 +38,12 @@
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dreams.DreamOverlayStateController
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.NotifPanelEvents
 import com.android.systemui.statusbar.CrossFadeHelper
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
@@ -46,6 +52,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.settings.SecureSettings
 import com.android.systemui.util.traceSection
 import javax.inject.Inject
 
@@ -84,14 +91,31 @@
     private val keyguardStateController: KeyguardStateController,
     private val bypassController: KeyguardBypassController,
     private val mediaCarouselController: MediaCarouselController,
-    private val notifLockscreenUserManager: NotificationLockscreenUserManager,
+    private val keyguardViewController: KeyguardViewController,
+    private val dreamOverlayStateController: DreamOverlayStateController,
     configurationController: ConfigurationController,
     wakefulnessLifecycle: WakefulnessLifecycle,
-    private val keyguardViewController: KeyguardViewController,
-    private val dreamOverlayStateController: DreamOverlayStateController
+    panelEventsEvents: NotifPanelEvents,
+    private val secureSettings: SecureSettings,
+    @Main private val handler: Handler,
 ) {
 
     /**
+     * Track the media player setting status on lock screen.
+     */
+    private var allowMediaPlayerOnLockScreen: Boolean = true
+    private val lockScreenMediaPlayerUri =
+            secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN)
+
+    /**
+     * Whether we "skip" QQS during panel expansion.
+     *
+     * This means that when expanding the panel we go directly to QS. Also when we are on QS and
+     * start closing the panel, it fully collapses instead of going to QQS.
+     */
+    private var skipQqsOnExpansion: Boolean = false
+
+    /**
      * The root overlay of the hierarchy. This is where the media notification is attached to
      * whenever the view is transitioning from one host to another. It also make sure that the
      * view is always in its final state when it is attached to a view host.
@@ -504,6 +528,30 @@
         mediaCarouselController.updateUserVisibility = {
             mediaCarouselController.mediaCarouselScrollHandler.visibleToUser = isVisibleToUser()
         }
+
+        panelEventsEvents.registerListener(object : NotifPanelEvents.Listener {
+            override fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {
+                skipQqsOnExpansion = isExpandImmediateEnabled
+                updateDesiredLocation()
+            }
+        })
+
+        val settingsObserver: ContentObserver = object : ContentObserver(handler) {
+            override fun onChange(selfChange: Boolean, uri: Uri?) {
+                if (uri == lockScreenMediaPlayerUri) {
+                    allowMediaPlayerOnLockScreen =
+                            secureSettings.getBoolForUser(
+                                    Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                                    true,
+                                    UserHandle.USER_CURRENT
+                            )
+                }
+            }
+        }
+        secureSettings.registerContentObserverForUser(
+                Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN,
+                settingsObserver,
+                UserHandle.USER_ALL)
     }
 
     private fun updateConfiguration() {
@@ -701,6 +749,9 @@
         if (isCurrentlyInGuidedTransformation()) {
             return false
         }
+        if (skipQqsOnExpansion) {
+            return false
+        }
         // This is an invalid transition, and can happen when using the camera gesture from the
         // lock screen. Disallow.
         if (previousLocation == LOCATION_LOCKSCREEN &&
@@ -852,6 +903,9 @@
      * otherwise
      */
     private fun getTransformationProgress(): Float {
+        if (skipQqsOnExpansion) {
+            return -1.0f
+        }
         val progress = getQSTransformationProgress()
         if (statusbarState != StatusBarState.KEYGUARD && progress >= 0) {
             return progress
@@ -1013,7 +1067,6 @@
         }
         val onLockscreen = (!bypassController.bypassEnabled &&
             (statusbarState == StatusBarState.KEYGUARD))
-        val allowedOnLockscreen = notifLockscreenUserManager.shouldShowLockscreenNotifications()
         val location = when {
             dreamOverlayActive -> LOCATION_DREAM_OVERLAY
             (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
@@ -1021,7 +1074,7 @@
             !hasActiveMedia -> LOCATION_QS
             onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
             onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
-            onLockscreen && allowedOnLockscreen -> LOCATION_LOCKSCREEN
+            onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN
             else -> LOCATION_QQS
         }
         // When we're on lock screen and the player is not active, we should keep it in QS.
@@ -1042,6 +1095,10 @@
             // reattach it without an animation
             return LOCATION_LOCKSCREEN
         }
+        if (skipQqsOnExpansion) {
+            // When doing an immediate expand or collapse, we want to keep it in QS.
+            return LOCATION_QS
+        }
         return location
     }
 
@@ -1089,7 +1146,7 @@
         return !statusBarStateController.isDozing &&
                 !keyguardViewController.isBouncerShowing &&
                 statusBarStateController.state == StatusBarState.KEYGUARD &&
-                notifLockscreenUserManager.shouldShowLockscreenNotifications() &&
+                allowMediaPlayerOnLockScreen &&
                 statusBarStateController.isExpanded &&
                 !qsExpanded
     }
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 7e263d8..08cf57c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -296,27 +296,17 @@
         mMediaOutputController.setRefreshing(true);
         // Update header icon
         final int iconRes = getHeaderIconRes();
-        final IconCompat iconCompat = getHeaderIcon();
-        final Drawable appSourceDrawable = getAppSourceIcon();
+        final IconCompat headerIcon = getHeaderIcon();
+        final IconCompat appSourceIcon = getAppSourceIcon();
         boolean colorSetUpdated = false;
         mCastAppLayout.setVisibility(
                 mMediaOutputController.shouldShowLaunchSection()
                         ? View.VISIBLE : View.GONE);
-        if (appSourceDrawable != null) {
-            mAppResourceIcon.setImageDrawable(appSourceDrawable);
-            mAppButton.setCompoundDrawablesWithIntrinsicBounds(resizeDrawable(appSourceDrawable,
-                            mContext.getResources().getDimensionPixelSize(
-                                    R.dimen.media_output_dialog_app_tier_icon_size
-                            )),
-                    null, null, null);
-        } else {
-            mAppResourceIcon.setVisibility(View.GONE);
-        }
         if (iconRes != 0) {
             mHeaderIcon.setVisibility(View.VISIBLE);
             mHeaderIcon.setImageResource(iconRes);
-        } else if (iconCompat != null) {
-            Icon icon = iconCompat.toIcon(mContext);
+        } else if (headerIcon != null) {
+            Icon icon = headerIcon.toIcon(mContext);
             if (icon.getType() != Icon.TYPE_BITMAP && icon.getType() != Icon.TYPE_ADAPTIVE_BITMAP) {
                 // icon doesn't support getBitmap, use default value for color scheme
                 updateButtonBackgroundColorFilter();
@@ -336,6 +326,18 @@
         } else {
             mHeaderIcon.setVisibility(View.GONE);
         }
+        if (appSourceIcon != null) {
+            Icon appIcon = appSourceIcon.toIcon(mContext);
+            mAppResourceIcon.setColorFilter(mMediaOutputController.getColorItemContent());
+            mAppResourceIcon.setImageIcon(appIcon);
+        } else {
+            Drawable appIconDrawable = mMediaOutputController.getAppSourceIconFromPackage();
+            if (appIconDrawable != null) {
+                mAppResourceIcon.setImageDrawable(appIconDrawable);
+            } else {
+                mAppResourceIcon.setVisibility(View.GONE);
+            }
+        }
         if (mHeaderIcon.getVisibility() == View.VISIBLE) {
             final int size = getHeaderIconSize();
             final int padding = mContext.getResources().getDimensionPixelSize(
@@ -480,7 +482,7 @@
         }
     }
 
-    abstract Drawable getAppSourceIcon();
+    abstract IconCompat getAppSourceIcon();
 
     abstract int getHeaderIconRes();
 
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 310469d..35baf013 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -19,7 +19,6 @@
 import android.app.AlertDialog;
 import android.content.Context;
 import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.text.method.HideReturnsTransformationMethod;
 import android.text.method.PasswordTransformationMethod;
@@ -116,8 +115,8 @@
     }
 
     @Override
-    Drawable getAppSourceIcon() {
-        return mMediaOutputController.getAppSourceIcon();
+    IconCompat getAppSourceIcon() {
+        return mMediaOutputController.getNotificationSmallIcon();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
index 0fa3265..2b5d6fd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.dialog
 
+import android.app.KeyguardManager
 import android.content.Context
 import android.media.AudioManager
 import android.media.session.MediaSessionManager
@@ -45,7 +46,8 @@
     private val dialogLaunchAnimator: DialogLaunchAnimator,
     private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>,
     private val audioManager: AudioManager,
-    private val powerExemptionManager: PowerExemptionManager
+    private val powerExemptionManager: PowerExemptionManager,
+    private val keyGuardManager: KeyguardManager
 ) {
     var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
 
@@ -57,7 +59,7 @@
         val controller = MediaOutputController(context, packageName,
                 mediaSessionManager, lbm, starter, notifCollection,
                 dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager,
-                powerExemptionManager)
+                powerExemptionManager, keyGuardManager)
         val dialog =
                 MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
         mediaOutputBroadcastDialog = dialog
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 27095b3..f7d80e0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -20,6 +20,7 @@
 
 import android.annotation.CallbackExecutor;
 import android.app.AlertDialog;
+import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.WallpaperColors;
 import android.bluetooth.BluetoothLeBroadcast;
@@ -120,6 +121,7 @@
     final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
     private final AudioManager mAudioManager;
     private final PowerExemptionManager mPowerExemptionManager;
+    private final KeyguardManager mKeyGuardManager;
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
     private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>();
 
@@ -154,7 +156,8 @@
             DialogLaunchAnimator dialogLaunchAnimator,
             Optional<NearbyMediaDevicesManager> nearbyMediaDevicesManagerOptional,
             AudioManager audioManager,
-            PowerExemptionManager powerExemptionManager) {
+            PowerExemptionManager powerExemptionManager,
+            KeyguardManager keyGuardManager) {
         mContext = context;
         mPackageName = packageName;
         mMediaSessionManager = mediaSessionManager;
@@ -163,6 +166,7 @@
         mNotifCollection = notifCollection;
         mAudioManager = audioManager;
         mPowerExemptionManager = powerExemptionManager;
+        mKeyGuardManager = keyGuardManager;
         InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm);
         mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
         mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
@@ -300,7 +304,7 @@
         }
     }
 
-    Drawable getAppSourceIcon() {
+    Drawable getAppSourceIconFromPackage() {
         if (mPackageName.isEmpty()) {
             return null;
         }
@@ -411,6 +415,24 @@
                 || isSelectedDeviceInGroup;
     }
 
+    IconCompat getNotificationSmallIcon() {
+        if (TextUtils.isEmpty(mPackageName)) {
+            return null;
+        }
+        for (NotificationEntry entry : mNotifCollection.getAllNotifs()) {
+            final Notification notification = entry.getSbn().getNotification();
+            if (notification.isMediaNotification()
+                    && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) {
+                final Icon icon = notification.getSmallIcon();
+                if (icon == null) {
+                    break;
+                }
+                return IconCompat.createFromIcon(icon);
+            }
+        }
+        return null;
+    }
+
     IconCompat getNotificationIcon() {
         if (TextUtils.isEmpty(mPackageName)) {
             return null;
@@ -701,7 +723,8 @@
         ActivityLaunchAnimator.Controller controller =
                 mDialogLaunchAnimator.createActivityLaunchController(view);
 
-        if (controller == null) {
+        if (controller == null || (mKeyGuardManager != null
+                && mKeyGuardManager.isKeyguardLocked())) {
             mCallback.dismissDialog();
         }
 
@@ -753,7 +776,7 @@
         MediaOutputController controller = new MediaOutputController(mContext, mPackageName,
                 mMediaSessionManager, mLocalBluetoothManager, mActivityStarter,
                 mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager),
-                mAudioManager, mPowerExemptionManager);
+                mAudioManager, mPowerExemptionManager, mKeyGuardManager);
         MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true,
                 broadcastSender, controller);
         mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 9fb96b5..cb6f5a7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -17,7 +17,6 @@
 package com.android.systemui.media.dialog;
 
 import android.content.Context;
-import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.view.View;
 import android.view.WindowManager;
@@ -81,8 +80,8 @@
     }
 
     @Override
-    Drawable getAppSourceIcon() {
-        return mMediaOutputController.getAppSourceIcon();
+    IconCompat getAppSourceIcon() {
+        return mMediaOutputController.getNotificationSmallIcon();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
index 8249a7c..543efed 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media.dialog
 
+import android.app.KeyguardManager
 import android.content.Context
 import android.media.AudioManager
 import android.media.session.MediaSessionManager
@@ -47,7 +48,8 @@
     private val dialogLaunchAnimator: DialogLaunchAnimator,
     private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>,
     private val audioManager: AudioManager,
-    private val powerExemptionManager: PowerExemptionManager
+    private val powerExemptionManager: PowerExemptionManager,
+    private val keyGuardManager: KeyguardManager
 ) {
     companion object {
         private const val INTERACTION_JANK_TAG = "media_output"
@@ -63,7 +65,7 @@
             context, packageName,
             mediaSessionManager, lbm, starter, notifCollection,
             dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager,
-            powerExemptionManager)
+            powerExemptionManager, keyGuardManager)
         val dialog =
             MediaOutputDialog(context, aboveStatusBar, broadcastSender, controller, uiEventLogger)
         mediaOutputDialog = dialog
diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
index e077fed..c544871 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.media.dream;
 
+import static com.android.systemui.flags.Flags.MEDIA_DREAM_COMPLICATION;
+
 import android.content.Context;
 
 import androidx.annotation.NonNull;
@@ -23,6 +25,7 @@
 
 import com.android.systemui.CoreStartable;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.MediaData;
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.media.SmartspaceMediaData;
@@ -34,7 +37,7 @@
  * the media complication as appropriate
  */
 public class MediaDreamSentinel extends CoreStartable {
-    private MediaDataManager.Listener mListener = new MediaDataManager.Listener() {
+    private final MediaDataManager.Listener mListener = new MediaDataManager.Listener() {
         private boolean mAdded;
         @Override
         public void onSmartspaceMediaDataRemoved(@NonNull String key, boolean immediately) {
@@ -63,6 +66,10 @@
         public void onMediaDataLoaded(@NonNull String key, @Nullable String oldKey,
                 @NonNull MediaData data, boolean immediately, int receivedSmartspaceCardLatency,
                 boolean isSsReactivated) {
+            if (!mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)) {
+                return;
+            }
+
             if (mAdded) {
                 return;
             }
@@ -79,15 +86,18 @@
     private final MediaDataManager mMediaDataManager;
     private final DreamOverlayStateController mDreamOverlayStateController;
     private final MediaDreamComplication mComplication;
+    private final FeatureFlags mFeatureFlags;
 
     @Inject
     public MediaDreamSentinel(Context context, MediaDataManager mediaDataManager,
             DreamOverlayStateController dreamOverlayStateController,
-            MediaDreamComplication complication) {
+            MediaDreamComplication complication,
+            FeatureFlags featureFlags) {
         super(context);
         mMediaDataManager = mediaDataManager;
         mDreamOverlayStateController = dreamOverlayStateController;
         mComplication = complication;
+        mFeatureFlags = featureFlags;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
index 9ab83b8..2278938 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
@@ -56,7 +56,7 @@
         internal val logger: MediaTttLogger,
         internal val windowManager: WindowManager,
         private val viewUtil: ViewUtil,
-        @Main internal val mainExecutor: DelayableExecutor,
+        @Main private val mainExecutor: DelayableExecutor,
         private val accessibilityManager: AccessibilityManager,
         private val configurationController: ConfigurationController,
         private val powerManager: PowerManager,
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
index d3b5bc6..aa10f7e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel
-import com.android.systemui.log.dagger.MediaTttSenderLogBuffer
 
 /**
  * A logger for media tap-to-transfer events.
@@ -27,7 +26,7 @@
  */
 class MediaTttLogger(
     private val deviceTypeTag: String,
-    @MediaTttSenderLogBuffer private val buffer: LogBuffer
+    private val buffer: LogBuffer
 ){
     /** Logs a change in the chip state for the given [mediaRouteId]. */
     fun logStateChange(stateName: String, mediaRouteId: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index 92d9ea8..9335489 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -160,12 +160,8 @@
             duration = ANIMATION_DURATION,
             includeMargins = true,
             includeFadeIn = true,
-        )
-
-        // We can only request focus once the animation finishes.
-        mainExecutor.executeDelayed(
-                { chipInnerView.requestAccessibilityFocus() },
-                ANIMATION_DURATION
+            // We can only request focus once the animation finishes.
+            onAnimationEnd = { chipInnerView.requestAccessibilityFocus() },
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
index 0834a5a..e27bfb3 100644
--- a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
@@ -31,7 +31,6 @@
 import com.android.systemui.people.data.repository.PeopleTileRepository
 import com.android.systemui.people.data.repository.PeopleWidgetRepository
 import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -52,7 +51,7 @@
      * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
      */
     private val _priorityTiles = MutableStateFlow(priorityTiles())
-    val priorityTiles: Flow<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow()
+    val priorityTiles: StateFlow<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow()
 
     /**
      * The list of the priority tiles/conversations.
@@ -61,7 +60,7 @@
      * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
      */
     private val _recentTiles = MutableStateFlow(recentTiles())
-    val recentTiles: Flow<List<PeopleTileViewModel>> = _recentTiles.asStateFlow()
+    val recentTiles: StateFlow<List<PeopleTileViewModel>> = _recentTiles.asStateFlow()
 
     /** The ID of the widget currently being edited/added. */
     private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index 833573d..be44202 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -39,6 +39,7 @@
 
 import javax.inject.Inject;
 import javax.inject.Named;
+import javax.inject.Provider;
 
 /** Controller for {@link QuickQSPanel}. */
 @QSScope
@@ -52,20 +53,21 @@
                 }
             };
 
-    private final boolean mUsingCollapsedLandscapeMedia;
+    private final Provider<Boolean> mUsingCollapsedLandscapeMediaProvider;
 
     @Inject
     QuickQSPanelController(QuickQSPanel view, QSTileHost qsTileHost,
             QSCustomizerController qsCustomizerController,
             @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer,
             @Named(QUICK_QS_PANEL) MediaHost mediaHost,
-            @Named(QS_USING_COLLAPSED_LANDSCAPE_MEDIA) boolean usingCollapsedLandscapeMedia,
+            @Named(QS_USING_COLLAPSED_LANDSCAPE_MEDIA)
+                    Provider<Boolean> usingCollapsedLandscapeMediaProvider,
             MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
             DumpManager dumpManager
     ) {
         super(view, qsTileHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger,
                 uiEventLogger, qsLogger, dumpManager);
-        mUsingCollapsedLandscapeMedia = usingCollapsedLandscapeMedia;
+        mUsingCollapsedLandscapeMediaProvider = usingCollapsedLandscapeMediaProvider;
     }
 
     @Override
@@ -80,7 +82,8 @@
         int rotation = getRotation();
         boolean isLandscape = rotation == RotationUtils.ROTATION_LANDSCAPE
                 || rotation == RotationUtils.ROTATION_SEASCAPE;
-        if (!mUsingCollapsedLandscapeMedia || !isLandscape) {
+        boolean usingCollapsedLandscapeMedia = mUsingCollapsedLandscapeMediaProvider.get();
+        if (!usingCollapsedLandscapeMedia || !isLandscape) {
             mMediaHost.setExpansion(MediaHost.EXPANDED);
         } else {
             mMediaHost.setExpansion(MediaHost.COLLAPSED);
@@ -126,7 +129,6 @@
         super.setTiles(tiles, /* collapsedView */ true);
     }
 
-    /** */
     public void setContentMargins(int marginStart, int marginEnd) {
         mView.setContentMargins(marginStart, marginEnd, mMediaHost.getHostView());
     }
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
index 56a1874..db7c1fd 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
+++ b/packages/SystemUI/src/com/android/systemui/ripple/RippleShader.kt
@@ -67,7 +67,7 @@
 
                 float rippleInsideAlpha = (1.-inside) * in_fadeFill;
                 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
-                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * 0.45;
+                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
                 vec4 ripple = in_color * rippleAlpha;
                 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
             }
@@ -83,7 +83,7 @@
 
                 float rippleInsideAlpha = (1.-inside) * in_fadeFill;
                 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
-                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * 0.45;
+                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
                 vec4 ripple = in_color * rippleAlpha;
                 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
             }
@@ -99,7 +99,7 @@
 
                 float rippleInsideAlpha = (1.-inside) * in_fadeFill;
                 float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
-                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * 0.45;
+                float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
                 vec4 ripple = in_color * rippleAlpha;
                 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt b/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
index 8b01201..60c8f37 100644
--- a/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/ripple/RippleView.kt
@@ -81,6 +81,7 @@
         rippleShader.color = RIPPLE_DEFAULT_COLOR
         rippleShader.progress = 0f
         rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
+        rippleShader.pixelDensity = resources.displayMetrics.density
 
         ripplePaint.shader = rippleShader
     }
@@ -124,6 +125,13 @@
         rippleShader.rippleFill = rippleFill
     }
 
+    /**
+     * Set the intensity of the sparkles.
+     */
+    fun setSparkleStrength(strength: Float) {
+        rippleShader.sparkleStrength = strength
+    }
+
     override fun onDraw(canvas: Canvas?) {
         if (canvas == null || !canvas.isHardwareAccelerated) {
             // Drawing with the ripple shader requires hardware acceleration, so skip
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt b/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl
similarity index 60%
copy from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
copy to packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl
index 8dde897..f7c4dad 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/IScreenshotProxy.aidl
@@ -1,11 +1,11 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
+/**
+ * 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
+ *     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,
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spaprivileged.framework.app
+package com.android.systemui.screenshot;
 
-import android.content.pm.ApplicationInfo
+/** Interface implemented by ScreenshotProxyService */
+interface IScreenshotProxy {
 
-interface AppRecord {
-    val app: ApplicationInfo
-}
+    /** Is the notification shade currently exanded? */
+    boolean isNotificationShadeExpanded();
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageCapture.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ImageCapture.kt
index 39f35a5..7779760 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageCapture.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageCapture.kt
@@ -22,5 +22,5 @@
 
     fun captureDisplay(displayId: Int, crop: Rect? = null): Bitmap?
 
-    fun captureTask(taskId: Int): Bitmap?
+    suspend fun captureTask(taskId: Int): Bitmap?
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageCaptureImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ImageCaptureImpl.kt
index 258c436..246265b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageCaptureImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageCaptureImpl.kt
@@ -27,13 +27,19 @@
 import android.view.SurfaceControl.DisplayCaptureArgs
 import android.view.SurfaceControl.ScreenshotHardwareBuffer
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
 
 private const val TAG = "ImageCaptureImpl"
 
+@SysUISingleton
 open class ImageCaptureImpl @Inject constructor(
     private val displayManager: DisplayManager,
-    private val atmService: IActivityTaskManager
+    private val atmService: IActivityTaskManager,
+    @Background private val bgContext: CoroutineDispatcher
 ) : ImageCapture {
 
     override fun captureDisplay(displayId: Int, crop: Rect?): Bitmap? {
@@ -46,8 +52,8 @@
         return buffer?.asBitmap()
     }
 
-    override fun captureTask(taskId: Int): Bitmap? {
-        val snapshot = atmService.takeTaskSnapshot(taskId)
+    override suspend fun captureTask(taskId: Int): Bitmap? {
+        val snapshot = withContext(bgContext) { atmService.takeTaskSnapshot(taskId) } ?: return null
         return Bitmap.wrapHardwareBuffer(snapshot.hardwareBuffer, snapshot.colorSpace)
     }
 
@@ -67,12 +73,17 @@
     }
 
     @VisibleForTesting
-    open fun captureDisplay(displayToken: IBinder, width: Int, height: Int, crop: Rect): ScreenshotHardwareBuffer? {
-        val captureArgs = DisplayCaptureArgs.Builder(displayToken)
-            .setSize(width, height)
-            .setSourceCrop(crop)
-            .build()
+    open fun captureDisplay(
+        displayToken: IBinder,
+        width: Int,
+        height: Int,
+        crop: Rect
+    ): ScreenshotHardwareBuffer? {
+        val captureArgs =
+            DisplayCaptureArgs.Builder(displayToken)
+                .setSize(width, height)
+                .setSourceCrop(crop)
+                .build()
         return SurfaceControl.captureDisplay(captureArgs)
     }
-
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
index 4397d3d..a918e5d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
@@ -16,51 +16,84 @@
 
 package com.android.systemui.screenshot
 
-import android.net.Uri
-import android.util.Log
-import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
+import android.graphics.Insets
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-import android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION
 import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler
 import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY
 import java.util.function.Consumer
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
 /**
  * Processes a screenshot request sent from {@link ScreenshotHelper}.
  */
 @SysUISingleton
-internal class RequestProcessor @Inject constructor(
-    private val controller: ScreenshotController,
+class RequestProcessor @Inject constructor(
+    private val capture: ImageCapture,
+    private val policy: ScreenshotPolicy,
+    private val flags: FeatureFlags,
+    /** For the Java Async version, to invoke the callback. */
+    @Application private val mainScope: CoroutineScope
 ) {
-    fun processRequest(
-        request: ScreenshotRequest,
-        onSavedListener: Consumer<Uri>,
-        callback: RequestCallback
-    ) {
+    /**
+     * Inspects the incoming request, returning a potentially modified request depending on policy.
+     *
+     * @param request the request to process
+     */
+    suspend fun process(request: ScreenshotRequest): ScreenshotRequest {
+        var result = request
 
-        if (request.type == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
-            val image = HardwareBitmapBundler.bundleToHardwareBitmap(request.bitmapBundle)
+        // Apply work profile screenshots policy:
+        //
+        // If the focused app belongs to a work profile, transforms a full screen
+        // (or partial) screenshot request to a task snapshot (provided image) screenshot.
 
-            controller.handleImageAsScreenshot(
-                image, request.boundsInScreen, request.insets,
-                request.taskId, request.userId, request.topComponent, onSavedListener, callback
-            )
-            return
+        // Whenever displayContentInfo is fetched, the topComponent is also populated
+        // regardless of the managed profile status.
+
+        if (request.type != TAKE_SCREENSHOT_PROVIDED_IMAGE &&
+            flags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)
+        ) {
+
+            val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
+
+            result = if (policy.isManagedProfile(info.userId)) {
+                val image = capture.captureTask(info.taskId)
+                    ?: error("Task snapshot returned a null Bitmap!")
+
+                // Provide the task snapshot as the screenshot
+                ScreenshotRequest(
+                    TAKE_SCREENSHOT_PROVIDED_IMAGE, request.source,
+                    HardwareBitmapBundler.hardwareBitmapToBundle(image),
+                    info.bounds, Insets.NONE, info.taskId, info.userId, info.component
+                )
+            } else {
+                // Create a new request of the same type which includes the top component
+                ScreenshotRequest(request.source, request.type, info.component)
+            }
         }
 
-        when (request.type) {
-            TAKE_SCREENSHOT_FULLSCREEN ->
-                controller.takeScreenshotFullscreen(null, onSavedListener, callback)
-            TAKE_SCREENSHOT_SELECTED_REGION ->
-                controller.takeScreenshotPartial(null, onSavedListener, callback)
-            else -> Log.w(TAG, "Invalid screenshot option: ${request.type}")
-        }
+        return result
     }
 
-    companion object {
-        const val TAG: String = "RequestProcessor"
+    /**
+     * Note: This is for compatibility with existing Java. Prefer the suspending function when
+     * calling from a Coroutine context.
+     *
+     * @param request the request to process
+     * @param callback the callback to provide the processed request, invoked from the main thread
+     */
+    fun processAsync(request: ScreenshotRequest, callback: Consumer<ScreenshotRequest>) {
+        mainScope.launch {
+            val result = process(request)
+            callback.accept(result)
+        }
     }
 }
+
+private const val TAG = "RequestProcessor"
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt
new file mode 100644
index 0000000..3580010
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.screenshot
+
+import android.annotation.UserIdInt
+import android.content.ComponentName
+import android.graphics.Rect
+import android.view.Display
+
+/**
+ * Provides policy decision-making information to screenshot request handling.
+ */
+interface ScreenshotPolicy {
+
+    /** @return true if the user is a managed profile (a.k.a. work profile) */
+    suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean
+
+    /**
+     * Requests information about the owner of display content which occupies a majority of the
+     * screenshot and/or has most recently been interacted with at the time the screenshot was
+     * requested.
+     *
+     * @param displayId the id of the display to inspect
+     * @return content info for the primary content on the display
+     */
+    suspend fun findPrimaryContent(displayId: Int): DisplayContentInfo
+
+    data class DisplayContentInfo(
+        val component: ComponentName,
+        val bounds: Rect,
+        @UserIdInt val userId: Int,
+        val taskId: Int,
+    )
+
+    fun getDefaultDisplayId(): Int = Display.DEFAULT_DISPLAY
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
new file mode 100644
index 0000000..ba809f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
@@ -0,0 +1,178 @@
+/*
+ * 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.screenshot
+
+import android.annotation.UserIdInt
+import android.app.ActivityTaskManager
+import android.app.ActivityTaskManager.RootTaskInfo
+import android.app.IActivityTaskManager
+import android.app.WindowConfiguration
+import android.app.WindowConfiguration.activityTypeToString
+import android.app.WindowConfiguration.windowingModeToString
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Process
+import android.os.RemoteException
+import android.os.UserManager
+import android.util.Log
+import android.view.Display.DEFAULT_DISPLAY
+import com.android.internal.infra.ServiceConnector
+import com.android.systemui.SystemUIService
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
+import java.util.Arrays
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+internal class ScreenshotPolicyImpl @Inject constructor(
+    context: Context,
+    private val userMgr: UserManager,
+    private val atmService: IActivityTaskManager,
+    @Background val bgDispatcher: CoroutineDispatcher,
+) : ScreenshotPolicy {
+
+    private val systemUiContent =
+        DisplayContentInfo(
+            ComponentName(context, SystemUIService::class.java),
+            Rect(),
+            ActivityTaskManager.INVALID_TASK_ID,
+            Process.myUserHandle().identifier,
+        )
+
+    private val proxyConnector: ServiceConnector<IScreenshotProxy> =
+        ServiceConnector.Impl(
+            context,
+            Intent(context, ScreenshotProxyService::class.java),
+            Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
+            context.userId,
+            IScreenshotProxy.Stub::asInterface
+        )
+
+    override fun getDefaultDisplayId(): Int {
+        return DEFAULT_DISPLAY
+    }
+
+    override suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean {
+        return withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
+    }
+
+    private fun nonPipVisibleTask(info: RootTaskInfo): Boolean {
+        return info.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED &&
+            info.isVisible &&
+            info.isRunning &&
+            info.numActivities > 0 &&
+            info.topActivity != null &&
+            info.childTaskIds.isNotEmpty()
+    }
+
+    /**
+     * Uses RootTaskInfo from ActivityTaskManager to guess at the primary focused task within a
+     * display. If no task is visible or the top task is covered by a system window, the info
+     * reported will reference a SystemUI component instead.
+     */
+    override suspend fun findPrimaryContent(displayId: Int): DisplayContentInfo {
+        // Determine if the notification shade is expanded. If so, task windows are not
+        // visible behind it, so the screenshot should instead be associated with SystemUI.
+        if (isNotificationShadeExpanded()) {
+            return systemUiContent
+        }
+
+        val taskInfoList = getAllRootTaskInfosOnDisplay(displayId)
+        if (DEBUG) {
+            debugLogRootTaskInfos(taskInfoList)
+        }
+
+        // If no visible task is located, then report SystemUI as the foreground content
+        val target = taskInfoList.firstOrNull(::nonPipVisibleTask) ?: return systemUiContent
+
+        val topActivity: ComponentName = target.topActivity ?: error("should not be null")
+        val topChildTask = target.childTaskIds.size - 1
+        val childTaskId = target.childTaskIds[topChildTask]
+        val childTaskUserId = target.childTaskUserIds[topChildTask]
+        val childTaskBounds = target.childTaskBounds[topChildTask]
+
+        return DisplayContentInfo(topActivity, childTaskBounds, childTaskId, childTaskUserId)
+    }
+
+    private fun debugLogRootTaskInfos(taskInfoList: List<RootTaskInfo>) {
+        for (info in taskInfoList) {
+            Log.d(
+                TAG,
+                "[root task info] " +
+                    "taskId=${info.taskId} " +
+                    "parentTaskId=${info.parentTaskId} " +
+                    "position=${info.position} " +
+                    "positionInParent=${info.positionInParent} " +
+                    "isVisible=${info.isVisible()} " +
+                    "visible=${info.visible} " +
+                    "isFocused=${info.isFocused} " +
+                    "isSleeping=${info.isSleeping} " +
+                    "isRunning=${info.isRunning} " +
+                    "windowMode=${windowingModeToString(info.windowingMode)} " +
+                    "activityType=${activityTypeToString(info.activityType)} " +
+                    "topActivity=${info.topActivity} " +
+                    "topActivityInfo=${info.topActivityInfo} " +
+                    "numActivities=${info.numActivities} " +
+                    "childTaskIds=${Arrays.toString(info.childTaskIds)} " +
+                    "childUserIds=${Arrays.toString(info.childTaskUserIds)} " +
+                    "childTaskBounds=${Arrays.toString(info.childTaskBounds)} " +
+                    "childTaskNames=${Arrays.toString(info.childTaskNames)}"
+            )
+
+            for (j in 0 until info.childTaskIds.size) {
+                Log.d(TAG, "    *** [$j] ******")
+                Log.d(TAG, "        ***  childTaskIds[$j]: ${info.childTaskIds[j]}")
+                Log.d(TAG, "        ***  childTaskUserIds[$j]: ${info.childTaskUserIds[j]}")
+                Log.d(TAG, "        ***  childTaskBounds[$j]: ${info.childTaskBounds[j]}")
+                Log.d(TAG, "        ***  childTaskNames[$j]: ${info.childTaskNames[j]}")
+            }
+        }
+    }
+
+    private suspend fun getAllRootTaskInfosOnDisplay(displayId: Int): List<RootTaskInfo> =
+        withContext(bgDispatcher) {
+            try {
+                atmService.getAllRootTaskInfosOnDisplay(displayId)
+            } catch (e: RemoteException) {
+                Log.e(TAG, "getAllRootTaskInfosOnDisplay", e)
+                listOf()
+            }
+        }
+
+    private suspend fun isNotificationShadeExpanded(): Boolean = suspendCoroutine { k ->
+        proxyConnector
+            .postForResult { it.isNotificationShadeExpanded }
+            .whenComplete { expanded, error ->
+                if (error != null) {
+                    Log.e(TAG, "isNotificationShadeExpanded", error)
+                }
+                k.resume(expanded ?: false)
+            }
+    }
+
+    companion object {
+        const val TAG: String = "ScreenshotPolicyImpl"
+        const val DEBUG: Boolean = false
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
new file mode 100644
index 0000000..9654e03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.screenshot
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import android.util.Log
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
+import javax.inject.Inject
+
+/**
+ * Provides state from the main SystemUI process on behalf of the Screenshot process.
+ */
+internal class ScreenshotProxyService @Inject constructor(
+    private val mExpansionMgr: PanelExpansionStateManager
+) : Service() {
+
+    private val mBinder: IBinder = object : IScreenshotProxy.Stub() {
+        /**
+         * @return true when the notification shade is partially or fully expanded.
+         */
+        override fun isNotificationShadeExpanded(): Boolean {
+            val expanded = !mExpansionMgr.isClosed()
+            Log.d(TAG, "isNotificationShadeExpanded(): $expanded")
+            return expanded
+        }
+    }
+
+    override fun onBind(intent: Intent): IBinder? {
+        Log.d(TAG, "onBind: $intent")
+        return mBinder
+    }
+
+    companion object {
+        const val TAG = "ScreenshotProxyService"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index 7bf3217..a8993bc 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -22,6 +22,7 @@
 import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE;
 import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_URI;
 import static com.android.systemui.flags.Flags.SCREENSHOT_REQUEST_PROCESSOR;
+import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_SERVICE;
@@ -97,7 +98,7 @@
     };
 
     /** Informs about coarse grained state of the Controller. */
-    interface RequestCallback {
+    public interface RequestCallback {
         /** Respond to the current request indicating the screenshot request failed. */
         void reportError();
 
@@ -124,6 +125,7 @@
         mBgExecutor = bgExecutor;
         mFeatureFlags = featureFlags;
         mFeatureFlags.addListener(SCREENSHOT_REQUEST_PROCESSOR, FlagEvent::requestNoRestart);
+        mFeatureFlags.addListener(SCREENSHOT_WORK_PROFILE_POLICY, FlagEvent::requestNoRestart);
         mProcessor = processor;
     }
 
@@ -229,49 +231,57 @@
 
         if (mFeatureFlags.isEnabled(SCREENSHOT_REQUEST_PROCESSOR)) {
             Log.d(TAG, "handleMessage: Using request processor");
-            mProcessor.processRequest(screenshotRequest, uriConsumer, requestCallback);
+            mProcessor.processAsync(screenshotRequest,
+                    (request) -> dispatchToController(request, uriConsumer, requestCallback));
             return true;
         }
 
-        switch (screenshotRequest.getType()) {
+        dispatchToController(screenshotRequest, uriConsumer, requestCallback);
+        return true;
+    }
+
+    private void dispatchToController(ScreenshotHelper.ScreenshotRequest request,
+            Consumer<Uri> uriConsumer, RequestCallback callback) {
+
+        ComponentName topComponent = request.getTopComponent();
+
+        switch (request.getType()) {
             case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
                 if (DEBUG_SERVICE) {
                     Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_FULLSCREEN");
                 }
-                mScreenshot.takeScreenshotFullscreen(topComponent, uriConsumer, requestCallback);
+                mScreenshot.takeScreenshotFullscreen(topComponent, uriConsumer, callback);
                 break;
             case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION:
                 if (DEBUG_SERVICE) {
                     Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_SELECTED_REGION");
                 }
-                mScreenshot.takeScreenshotPartial(topComponent, uriConsumer, requestCallback);
+                mScreenshot.takeScreenshotPartial(topComponent, uriConsumer, callback);
                 break;
             case WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE:
                 if (DEBUG_SERVICE) {
                     Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_PROVIDED_IMAGE");
                 }
                 Bitmap screenshot = ScreenshotHelper.HardwareBitmapBundler.bundleToHardwareBitmap(
-                        screenshotRequest.getBitmapBundle());
-                Rect screenBounds = screenshotRequest.getBoundsInScreen();
-                Insets insets = screenshotRequest.getInsets();
-                int taskId = screenshotRequest.getTaskId();
-                int userId = screenshotRequest.getUserId();
+                        request.getBitmapBundle());
+                Rect screenBounds = request.getBoundsInScreen();
+                Insets insets = request.getInsets();
+                int taskId = request.getTaskId();
+                int userId = request.getUserId();
 
                 if (screenshot == null) {
                     Log.e(TAG, "Got null bitmap from screenshot message");
                     mNotificationsController.notifyScreenshotError(
                             R.string.screenshot_failed_to_capture_text);
-                    requestCallback.reportError();
+                    callback.reportError();
                 } else {
                     mScreenshot.handleImageAsScreenshot(screenshot, screenBounds, insets,
-                            taskId, userId, topComponent, uriConsumer, requestCallback);
+                            taskId, userId, topComponent, uriConsumer, callback);
                 }
                 break;
             default:
-                Log.w(TAG, "Invalid screenshot option: " + msg.what);
-                return false;
+                Log.w(TAG, "Invalid screenshot option: " + request.getType());
         }
-        return true;
     }
 
     private static void sendComplete(Messenger target) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index 3e442587..fdb0100 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 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.
@@ -20,6 +20,9 @@
 
 import com.android.systemui.screenshot.ImageCapture;
 import com.android.systemui.screenshot.ImageCaptureImpl;
+import com.android.systemui.screenshot.ScreenshotPolicy;
+import com.android.systemui.screenshot.ScreenshotPolicyImpl;
+import com.android.systemui.screenshot.ScreenshotProxyService;
 import com.android.systemui.screenshot.TakeScreenshotService;
 
 import dagger.Binds;
@@ -33,12 +36,20 @@
 @Module
 public abstract class ScreenshotModule {
 
-    /** */
     @Binds
     @IntoMap
     @ClassKey(TakeScreenshotService.class)
-    public abstract Service bindTakeScreenshotService(TakeScreenshotService service);
+    abstract Service bindTakeScreenshotService(TakeScreenshotService service);
 
     @Binds
-    public abstract ImageCapture bindImageCapture(ImageCaptureImpl capture);
+    @IntoMap
+    @ClassKey(ScreenshotProxyService.class)
+    abstract Service bindScreenshotProxyService(ScreenshotProxyService service);
+
+    @Binds
+    abstract ScreenshotPolicy bindScreenshotPolicyImpl(ScreenshotPolicyImpl impl);
+
+    @Binds
+    abstract ImageCapture bindImageCaptureImpl(ImageCaptureImpl capture);
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt b/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt
index ce9d89f..4558061 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotifPanelEvents.kt
@@ -29,11 +29,25 @@
     interface Listener {
 
         /** Invoked when the notification panel starts or stops collapsing. */
-        fun onPanelCollapsingChanged(isCollapsing: Boolean)
+        @JvmDefault
+        fun onPanelCollapsingChanged(isCollapsing: Boolean) {}
 
         /**
          * Invoked when the notification panel starts or stops launching an [android.app.Activity].
          */
-        fun onLaunchingActivityChanged(isLaunchingActivity: Boolean)
+        @JvmDefault
+        fun onLaunchingActivityChanged(isLaunchingActivity: Boolean) {}
+
+        /**
+         * Invoked when the "expand immediate" attribute changes.
+         *
+         * An example of expanding immediately is when swiping down from the top with two fingers.
+         * Instead of going to QQS, we immediately expand to full QS.
+         *
+         * Another example is when full QS is showing, and we swipe up from the bottom. Instead of
+         * going to QQS, the panel fully collapses.
+         */
+        @JvmDefault
+        fun onExpandImmediateChanged(isExpandImmediateEnabled: Boolean) {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 37a948d..7a4c877 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -442,7 +442,6 @@
      */
     private boolean mQsAnimatorExpand;
     private boolean mIsLaunchTransitionFinished;
-    private boolean mOnlyAffordanceInThisMotion;
     private ValueAnimator mQsSizeChangeAnimator;
 
     private boolean mQsScrimEnabled = true;
@@ -720,6 +719,7 @@
             AccessibilityManager accessibilityManager, @DisplayId int displayId,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             MetricsLogger metricsLogger,
+            ShadeLogger shadeLogger,
             ConfigurationController configurationController,
             Provider<FlingAnimationUtils.Builder> flingAnimationUtilsBuilder,
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
@@ -786,6 +786,7 @@
                 panelExpansionStateManager,
                 ambientState,
                 interactionJankMonitor,
+                shadeLogger,
                 systemClock);
         mView = view;
         mVibratorHelper = vibratorHelper;
@@ -1690,12 +1691,17 @@
         }
 
         if (mQsExpanded) {
-            mQsExpandImmediate = true;
+            setQsExpandImmediate(true);
             setShowShelfOnly(true);
         }
         super.collapse(delayed, speedUpFactor);
     }
 
+    private void setQsExpandImmediate(boolean expandImmediate) {
+        mQsExpandImmediate = expandImmediate;
+        mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate);
+    }
+
     private void setShowShelfOnly(boolean shelfOnly) {
         mNotificationStackScrollLayoutController.setShouldShowShelfOnly(
                 shelfOnly && !mSplitShadeEnabled);
@@ -1748,7 +1754,7 @@
 
     public void expandWithQs() {
         if (isQsExpansionEnabled()) {
-            mQsExpandImmediate = true;
+            setQsExpandImmediate(true);
             setShowShelfOnly(true);
         }
         if (mSplitShadeEnabled && isOnKeyguard()) {
@@ -1827,6 +1833,8 @@
                 }
                 if (mQsExpansionAnimator != null) {
                     mInitialHeightOnTouch = mQsExpansionHeight;
+                    mShadeLog.logMotionEvent(event,
+                            "onQsIntercept: down action, QS tracking enabled");
                     mQsTracking = true;
                     traceQsJank(true /* startTracing */, false /* wasCancelled */);
                     mNotificationStackScrollLayoutController.cancelLongPress();
@@ -1854,12 +1862,16 @@
                     setQsExpansion(h + mInitialHeightOnTouch);
                     trackMovement(event);
                     return true;
+                } else {
+                    mShadeLog.logMotionEvent(event,
+                            "onQsIntercept: move ignored because qs tracking disabled");
                 }
                 if ((h > getTouchSlop(event) || (h < -getTouchSlop(event) && mQsExpanded))
                         && Math.abs(h) > Math.abs(x - mInitialTouchX)
                         && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
                     if (DEBUG_LOGCAT) Log.d(TAG, "onQsIntercept - start tracking expansion");
                     mView.getParent().requestDisallowInterceptTouchEvent(true);
+                    mShadeLog.onQsInterceptMoveQsTrackingEnabled(h);
                     mQsTracking = true;
                     traceQsJank(true /* startTracing */, false /* wasCancelled */);
                     onQsExpansionStarted();
@@ -1875,6 +1887,7 @@
             case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
                 trackMovement(event);
+                mShadeLog.logMotionEvent(event, "onQsIntercept: up action, QS tracking disabled");
                 mQsTracking = false;
                 break;
         }
@@ -1912,7 +1925,6 @@
 
     private void initDownStates(MotionEvent event) {
         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
-            mOnlyAffordanceInThisMotion = false;
             mQsTouchAboveFalsingThreshold = mQsFullyExpanded;
             mDozingOnDown = isDozing();
             mDownX = event.getX();
@@ -2051,6 +2063,7 @@
                 && collapsedQs && isQsExpansionEnabled();
         if (action == MotionEvent.ACTION_DOWN && expandedShadeCollapsedQs) {
             // Down in the empty area while fully expanded - go to QS.
+            mShadeLog.logMotionEvent(event, "handleQsTouch: down action, QS tracking enabled");
             mQsTracking = true;
             traceQsJank(true /* startTracing */, false /* wasCancelled */);
             mConflictingQsExpansionGesture = true;
@@ -2065,6 +2078,8 @@
         if (!mQsExpandImmediate && mQsTracking) {
             onQsTouch(event);
             if (!mConflictingQsExpansionGesture && !mSplitShadeEnabled) {
+                mShadeLog.logMotionEvent(event,
+                        "handleQsTouch: not immediate expand or conflicting gesture");
                 return true;
             }
         }
@@ -2077,7 +2092,7 @@
         if (mTwoFingerQsExpandPossible && isOpenQsEvent(event) && event.getY(event.getActionIndex())
                 < mStatusBarMinHeight) {
             mMetricsLogger.count(COUNTER_PANEL_OPEN_QS, 1);
-            mQsExpandImmediate = true;
+            setQsExpandImmediate(true);
             setShowShelfOnly(true);
             requestPanelHeightUpdate();
 
@@ -2132,6 +2147,7 @@
                 event.getX(), event.getY(), -1)) {
             if (DEBUG_LOGCAT) Log.d(TAG, "handleQsDown");
             mFalsingCollector.onQsDown();
+            mShadeLog.logMotionEvent(event, "handleQsDown: down action, QS tracking enabled");
             mQsTracking = true;
             onQsExpansionStarted();
             mInitialHeightOnTouch = mQsExpansionHeight;
@@ -2214,6 +2230,7 @@
 
         switch (event.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
+                mShadeLog.logMotionEvent(event, "onQsTouch: down action, QS tracking enabled");
                 mQsTracking = true;
                 traceQsJank(true /* startTracing */, false /* wasCancelled */);
                 mInitialTouchY = y;
@@ -2240,6 +2257,7 @@
 
             case MotionEvent.ACTION_MOVE:
                 if (DEBUG_LOGCAT) Log.d(TAG, "onQSTouch move");
+                mShadeLog.logMotionEvent(event, "onQsTouch: move action, setting QS expansion");
                 setQsExpansion(h + mInitialHeightOnTouch);
                 if (h >= getFalsingThreshold()) {
                     mQsTouchAboveFalsingThreshold = true;
@@ -2249,6 +2267,8 @@
 
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
+                mShadeLog.logMotionEvent(event,
+                        "onQsTouch: up/cancel action, QS tracking disabled");
                 mQsTracking = false;
                 mTrackingPointer = -1;
                 trackMovement(event);
@@ -3071,8 +3091,8 @@
                 positionClockAndNotifications();
             }
         }
-        if (mQsExpandImmediate || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
-                && !mQsExpansionFromOverscroll) {
+        if (mQsExpandImmediate || (mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
+                && !mQsExpansionFromOverscroll)) {
             float t;
             if (mKeyguardShowing) {
 
@@ -3279,7 +3299,7 @@
         } else {
             setListening(true);
         }
-        mQsExpandImmediate = false;
+        setQsExpandImmediate(false);
         setShowShelfOnly(false);
         mTwoFingerQsExpandPossible = false;
         updateTrackingHeadsUp(null);
@@ -3337,7 +3357,7 @@
         super.onTrackingStarted();
         mScrimController.onTrackingStarted();
         if (mQsFullyExpanded) {
-            mQsExpandImmediate = true;
+            setQsExpandImmediate(true);
             setShowShelfOnly(true);
         }
         mNotificationStackScrollLayoutController.onPanelTrackingStarted();
@@ -4162,6 +4182,7 @@
                         || mPulseExpansionHandler.isExpanding();
                 if (pulseShouldGetTouch && mPulseExpansionHandler.onTouchEvent(event)) {
                     // We're expanding all the other ones shouldn't get this anymore
+                    mShadeLog.logMotionEvent(event, "onTouch: PulseExpansionHandler handled event");
                     return true;
                 }
                 if (mListenForHeadsUp && !mHeadsUpTouchHelper.isTrackingHeadsUp()
@@ -4169,14 +4190,10 @@
                         && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
                     mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
                 }
-                boolean handled = false;
-                if (mOnlyAffordanceInThisMotion) {
-                    return true;
-                }
-                handled |= mHeadsUpTouchHelper.onTouchEvent(event);
+                boolean handled = mHeadsUpTouchHelper.onTouchEvent(event);
 
                 if (!mHeadsUpTouchHelper.isTrackingHeadsUp() && handleQsTouch(event)) {
-                    if (DEBUG_LOGCAT) Log.d(TAG, "handleQsTouch true");
+                    mShadeLog.logMotionEvent(event, "onTouch: handleQsTouch handled event");
                     return true;
                 }
                 if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
@@ -4744,6 +4761,8 @@
                 }
             } else if (!mQsExpanded && mQsExpansionAnimator == null) {
                 setQsExpansion(mQsMinExpansionHeight + mLastOverscroll);
+            } else {
+                mShadeLog.v("onLayoutChange: qs expansion not set");
             }
             updateExpandedHeight(getExpandedHeight());
             updateHeader();
@@ -4898,7 +4917,7 @@
             // to locked will trigger this event and we're not actually in the process of opening
             // the shade, lockscreen is just always expanded
             if (mSplitShadeEnabled && !isOnKeyguard()) {
-                mQsExpandImmediate = true;
+                setQsExpandImmediate(true);
             }
             mCentralSurfaces.makeExpandedVisible(false);
         }
@@ -4965,5 +4984,11 @@
                 cb.onPanelCollapsingChanged(isCollapsing);
             }
         }
+
+        private void notifyExpandImmediateChange(boolean expandImmediateEnabled) {
+            for (NotifPanelEvents.Listener cb : mListeners) {
+                cb.onExpandImmediateChanged(expandImmediateEnabled);
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
index 4aad245..73eaa85 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/PanelViewController.java
@@ -202,6 +202,8 @@
     private final InteractionJankMonitor mInteractionJankMonitor;
     protected final SystemClock mSystemClock;
 
+    protected final ShadeLogger mShadeLog;
+
     protected abstract void onExpandingFinished();
 
     protected void onExpandingStarted() {
@@ -242,6 +244,7 @@
             PanelExpansionStateManager panelExpansionStateManager,
             AmbientState ambientState,
             InteractionJankMonitor interactionJankMonitor,
+            ShadeLogger shadeLogger,
             SystemClock systemClock) {
         keyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
@@ -254,6 +257,7 @@
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mLockscreenGestureLogger = lockscreenGestureLogger;
         mPanelExpansionStateManager = panelExpansionStateManager;
+        mShadeLog = shadeLogger;
         TouchHandler touchHandler = createTouchHandler();
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
@@ -1275,9 +1279,16 @@
 
         @Override
         public boolean onTouch(View v, MotionEvent event) {
-            if (mInstantExpanding || (mTouchDisabled
-                    && event.getActionMasked() != MotionEvent.ACTION_CANCEL) || (mMotionAborted
-                    && event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+            if (mInstantExpanding) {
+                mShadeLog.logMotionEvent(event, "onTouch: touch ignored due to instant expanding");
+                return false;
+            }
+            if (mTouchDisabled  && event.getActionMasked() != MotionEvent.ACTION_CANCEL) {
+                mShadeLog.logMotionEvent(event, "onTouch: non-cancel action, touch disabled");
+                return false;
+            }
+            if (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN) {
+                mShadeLog.logMotionEvent(event, "onTouch: non-down action, motion was aborted");
                 return false;
             }
 
@@ -1287,6 +1298,7 @@
                     // Turn off tracking if it's on or the shade can get stuck in the down position.
                     onTrackingStopped(true /* expand */);
                 }
+                mShadeLog.logMotionEvent(event, "onTouch: drag not enabled");
                 return false;
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
new file mode 100644
index 0000000..f1e44ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -0,0 +1,48 @@
+package com.android.systemui.shade
+
+import android.view.MotionEvent
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.LogMessage
+import com.android.systemui.log.dagger.ShadeLog
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "systemui.shade"
+
+/** Lightweight logging utility for the Shade. */
+class ShadeLogger @Inject constructor(
+    @ShadeLog
+    private val buffer: LogBuffer
+) {
+    fun v(@CompileTimeConstant msg: String) {
+        buffer.log(TAG, LogLevel.VERBOSE, msg)
+    }
+
+    private inline fun log(
+        logLevel: LogLevel,
+        initializer: LogMessage.() -> Unit,
+        noinline printer: LogMessage.() -> String
+    ) {
+        buffer.log(TAG, logLevel, initializer, printer)
+    }
+
+    fun onQsInterceptMoveQsTrackingEnabled(h: Float) {
+        log(LogLevel.VERBOSE,
+            { double1 = h.toDouble() },
+            { "onQsIn[tercept: move action, QS tracking enabled. h = $double1" })
+    }
+
+    fun logMotionEvent(event: MotionEvent, message: String) {
+        log(LogLevel.VERBOSE, {
+            str1 = message
+            long1 = event.eventTime
+            long2 = event.downTime
+            int1 = event.action
+            int2 = event.classification
+            double1 = event.y.toDouble()
+        }, {
+            "$str1\neventTime=$long1,downTime=$long2,y=$double1,action=$int1,classification=$int2"
+        })
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 65ba5ad..e754d5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -512,7 +512,8 @@
                     Trace.beginSection("MODE_WAKE_AND_UNLOCK");
                 } else {
                     Trace.beginSection("MODE_WAKE_AND_UNLOCK_FROM_DREAM");
-                    mUpdateMonitor.awakenFromDream();
+                    // Don't call awaken from Dream here. In order to avoid flickering, wait until
+                    // later to awaken.
                 }
                 mNotificationShadeWindowController.setNotificationShadeFocusable(false);
                 mKeyguardViewMediator.onWakeAndUnlocking();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 4c9c75b..103e4f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -27,6 +27,7 @@
 import androidx.collection.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.UiEventLogger;
 import com.android.internal.policy.SystemBarUtils;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
@@ -39,6 +40,7 @@
 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
@@ -107,8 +109,10 @@
             GroupMembershipManager groupMembershipManager,
             VisualStabilityProvider visualStabilityProvider,
             ConfigurationController configurationController,
-            @Main Handler handler) {
-        super(context, logger, handler);
+            @Main Handler handler,
+            AccessibilityManagerWrapper accessibilityManagerWrapper,
+            UiEventLogger uiEventLogger) {
+        super(context, logger, handler, accessibilityManagerWrapper, uiEventLogger);
         Resources resources = mContext.getResources();
         mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time);
         statusBarStateController.addCallback(mStatusBarStateListener);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index d058b75..53e08ea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -10,6 +10,7 @@
 import android.provider.Settings
 import android.view.Surface
 import android.view.View
+import android.view.WindowManager.fixScale
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
@@ -138,8 +139,8 @@
     }
 
     fun updateAnimatorDurationScale() {
-        animatorDurationScale =
-                globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f)
+        animatorDurationScale = fixScale(
+                globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f))
     }
 
     override fun shouldDelayKeyguardShow(): Boolean =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt
index 6c02b0d..780a02d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollector.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline
 
-import com.android.systemui.statusbar.pipeline.repository.NetworkCapabilityInfo
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo
 import kotlinx.coroutines.flow.StateFlow
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt
index 8d69422..99798f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoCollectorImpl.kt
@@ -18,9 +18,9 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.repository.NetworkCapabilitiesRepo
-import kotlinx.coroutines.CoroutineScope
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilitiesRepo
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
index 1aae250..64c47f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
@@ -20,14 +20,18 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.repository.NetworkCapabilityInfo
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
 import javax.inject.Inject
+import javax.inject.Provider
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
+import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /**
  * A processor that transforms raw connectivity information that we get from callbacks and turns it
@@ -42,12 +46,16 @@
 class ConnectivityInfoProcessor @Inject constructor(
         connectivityInfoCollector: ConnectivityInfoCollector,
         context: Context,
+        // TODO(b/238425913): Don't use the application scope; instead, use the status bar view's
+        // scope so we only do work when there's UI that cares about it.
         @Application private val scope: CoroutineScope,
-        statusBarPipelineFlags: StatusBarPipelineFlags,
+        private val statusBarPipelineFlags: StatusBarPipelineFlags,
+        private val wifiViewModelProvider: Provider<WifiViewModel>,
 ) : CoreStartable(context) {
     // Note: This flow will not start running until a client calls `collect` on it, which means that
     // [connectivityInfoCollector]'s flow will also not start anything until that `collect` call
     // happens.
+    // TODO(b/238425913): Delete this.
     val processedInfoFlow: Flow<ProcessedConnectivityInfo> =
             if (!statusBarPipelineFlags.isNewPipelineEnabled())
                 emptyFlow()
@@ -60,6 +68,14 @@
                     )
 
     override fun start() {
+        if (!statusBarPipelineFlags.isNewPipelineEnabled()) {
+            return
+        }
+        // TODO(b/238425913): The view binder should do this instead. For now, do it here so we can
+        // see the logs.
+        scope.launch {
+            wifiViewModelProvider.get().isActivityInVisible.collect { }
+        }
     }
 
     private fun RawConnectivityInfo.process(): ProcessedConnectivityInfo {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLogger.kt
deleted file mode 100644
index f88e9d6..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLogger.kt
+++ /dev/null
@@ -1,59 +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.pipeline
-
-import android.net.Network
-import android.net.NetworkCapabilities
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.LogLevel
-import com.android.systemui.log.dagger.StatusBarConnectivityLog
-import javax.inject.Inject
-
-@SysUISingleton
-class ConnectivityPipelineLogger @Inject constructor(
-    @StatusBarConnectivityLog private val buffer: LogBuffer,
-) {
-    fun logOnCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
-        buffer.log(
-            TAG,
-            LogLevel.INFO,
-            {
-                int1 = network.getNetId()
-                str1 = networkCapabilities.toString()
-            },
-            {
-                "onCapabilitiesChanged: net=$int1 capabilities=$str1"
-            }
-        )
-    }
-
-    fun logOnLost(network: Network) {
-        buffer.log(
-            TAG,
-            LogLevel.INFO,
-            {
-                int1 = network.getNetId()
-            },
-            {
-                "onLost: net=$int1"
-            }
-        )
-    }
-}
-
-private const val TAG = "SbConnectivityPipeline"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index c4e2b73..7abe19e7b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -20,6 +20,8 @@
 import com.android.systemui.statusbar.pipeline.ConnectivityInfoCollector
 import com.android.systemui.statusbar.pipeline.ConnectivityInfoCollectorImpl
 import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.ClassKey
@@ -37,4 +39,7 @@
     abstract fun provideConnectivityInfoCollector(
             impl: ConnectivityInfoCollectorImpl
     ): ConnectivityInfoCollector
+
+    @Binds
+    abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
new file mode 100644
index 0000000..a5fff5e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.pipeline.shared
+
+import android.net.Network
+import android.net.NetworkCapabilities
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.dagger.StatusBarConnectivityLog
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onEach
+
+@SysUISingleton
+class ConnectivityPipelineLogger @Inject constructor(
+    @StatusBarConnectivityLog private val buffer: LogBuffer,
+) {
+    fun logInputChange(callbackName: String, changeInfo: String) {
+        buffer.log(
+                SB_LOGGING_TAG,
+                LogLevel.INFO,
+                {
+                    str1 = callbackName
+                    str2 = changeInfo
+                },
+                {
+                    "Input: $str1: $str2"
+                }
+        )
+    }
+
+    fun logOutputChange(outputParamName: String, changeInfo: String) {
+        buffer.log(
+                SB_LOGGING_TAG,
+                LogLevel.INFO,
+                {
+                    str1 = outputParamName
+                    str2 = changeInfo
+                },
+                {
+                    "Output: $str1: $str2"
+                }
+        )
+    }
+
+    fun logOnCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
+        buffer.log(
+            SB_LOGGING_TAG,
+            LogLevel.INFO,
+            {
+                int1 = network.getNetId()
+                str1 = networkCapabilities.toString()
+            },
+            {
+                "onCapabilitiesChanged: net=$int1 capabilities=$str1"
+            }
+        )
+    }
+
+    fun logOnLost(network: Network) {
+        buffer.log(
+            SB_LOGGING_TAG,
+            LogLevel.INFO,
+            {
+                int1 = network.getNetId()
+            },
+            {
+                "onLost: net=$int1"
+            }
+        )
+    }
+
+    companion object {
+        const val SB_LOGGING_TAG = "SbConnectivity"
+
+        /**
+         * Log a change in one of the **outputs** to the connectivity pipeline.
+         *
+         * @param prettyPrint an optional function to transform the value into a readable string.
+         *   [toString] is used if no custom function is provided.
+         */
+        fun <T : Any> Flow<T>.logOutputChange(
+                logger: ConnectivityPipelineLogger,
+                outputParamName: String,
+                prettyPrint: (T) -> String = { it.toString() }
+        ): Flow<T> {
+            return this.onEach { logger.logOutputChange(outputParamName, prettyPrint(it)) }
+        }
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
similarity index 64%
copy from packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
copy to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
index 8dde897..44c0496 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRecord.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
@@ -14,10 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spaprivileged.framework.app
+package com.android.systemui.statusbar.pipeline.wifi.data.model
 
-import android.content.pm.ApplicationInfo
-
-interface AppRecord {
-    val app: ApplicationInfo
-}
+/**
+ * Provides information on the current wifi activity.
+ */
+data class WifiActivityModel(
+    /** True if the wifi has activity in (download). */
+    val hasActivityIn: Boolean,
+    /** True if the wifi has activity out (upload). */
+    val hasActivityOut: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt
new file mode 100644
index 0000000..1b73322
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiModel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.wifi.data.model
+
+/** Provides information about the current wifi state. */
+data class WifiModel(
+    /** See [android.net.wifi.WifiInfo.ssid]. */
+    val ssid: String? = null,
+    /** See [android.net.wifi.WifiInfo.isPasspointAp]. */
+    val isPasspointAccessPoint: Boolean = false,
+    /** See [android.net.wifi.WifiInfo.isOsuAp]. */
+    val isOnlineSignUpForPasspointAccessPoint: Boolean = false,
+    /** See [android.net.wifi.WifiInfo.passpointProviderFriendlyName]. */
+    val passpointProviderFriendlyName: String? = null,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepo.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt
similarity index 91%
rename from packages/SystemUI/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepo.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt
index e5980c3..6c0a445 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepo.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepo.kt
@@ -16,7 +16,7 @@
 
 @file:OptIn(ExperimentalCoroutinesApi::class)
 
-package com.android.systemui.statusbar.pipeline.repository
+package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
 import android.annotation.SuppressLint
 import android.net.ConnectivityManager
@@ -25,7 +25,7 @@
 import android.net.NetworkRequest
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.statusbar.pipeline.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -35,7 +35,11 @@
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.stateIn
 
-/** Repository that contains all relevant [NetworkCapabilites] for the current networks */
+/**
+ * Repository that contains all relevant [NetworkCapabilities] for the current networks.
+ *
+ * TODO(b/238425913): Figure out how to merge this with [WifiRepository].
+ */
 @SysUISingleton
 class NetworkCapabilitiesRepo @Inject constructor(
     connectivityManager: ConnectivityManager,
@@ -88,5 +92,3 @@
     val network: Network,
     val capabilities: NetworkCapabilities,
 )
-
-private const val TAG = "ConnectivityRepository"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
new file mode 100644
index 0000000..012dde5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.pipeline.wifi.data.repository
+
+import android.net.wifi.WifiManager
+import android.net.wifi.WifiManager.TrafficStateCallback
+import android.util.Log
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/**
+ * Provides data related to the wifi state.
+ */
+interface WifiRepository {
+    /**
+     * Observable for the current state of wifi; `null` when there is no active wifi.
+     */
+    val wifiModel: Flow<WifiModel?>
+
+    /**
+     * Observable for the current wifi network activity.
+     */
+    val wifiActivity: Flow<WifiActivityModel>
+}
+
+/** Real implementation of [WifiRepository]. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class WifiRepositoryImpl @Inject constructor(
+        wifiManager: WifiManager?,
+        @Main mainExecutor: Executor,
+        logger: ConnectivityPipelineLogger,
+) : WifiRepository {
+
+    // TODO(b/238425913): Actually implement the wifiModel flow.
+    override val wifiModel: Flow<WifiModel?> = flowOf(WifiModel(ssid = "AB"))
+
+    override val wifiActivity: Flow<WifiActivityModel> =
+            if (wifiManager == null) {
+                Log.w(SB_LOGGING_TAG, "Null WifiManager; skipping activity callback")
+                flowOf(ACTIVITY_DEFAULT)
+            } else {
+                conflatedCallbackFlow {
+                    val callback = TrafficStateCallback { state ->
+                        logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
+                        trySend(trafficStateToWifiActivityModel(state))
+                    }
+
+                    wifiManager.registerTrafficStateCallback(mainExecutor, callback)
+                    trySend(ACTIVITY_DEFAULT)
+
+                    awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
+                }
+            }
+
+    companion object {
+        val ACTIVITY_DEFAULT = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+
+        private fun trafficStateToWifiActivityModel(state: Int): WifiActivityModel {
+            return WifiActivityModel(
+                hasActivityIn = state == TrafficStateCallback.DATA_ACTIVITY_IN ||
+                    state == TrafficStateCallback.DATA_ACTIVITY_INOUT,
+                hasActivityOut = state == TrafficStateCallback.DATA_ACTIVITY_OUT ||
+                    state == TrafficStateCallback.DATA_ACTIVITY_INOUT,
+            )
+        }
+
+        private fun prettyPrintActivity(activity: Int): String {
+            return when (activity) {
+                TrafficStateCallback.DATA_ACTIVITY_NONE -> "NONE"
+                TrafficStateCallback.DATA_ACTIVITY_IN -> "IN"
+                TrafficStateCallback.DATA_ACTIVITY_OUT -> "OUT"
+                TrafficStateCallback.DATA_ACTIVITY_INOUT -> "INOUT"
+                else -> "INVALID"
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
new file mode 100644
index 0000000..f705399
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.pipeline.wifi.domain.interactor
+
+import android.net.wifi.WifiManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+
+/**
+ * The business logic layer for the wifi icon.
+ *
+ * This interactor processes information from our data layer into information that the UI layer can
+ * use.
+ */
+@SysUISingleton
+class WifiInteractor @Inject constructor(
+        repository: WifiRepository,
+) {
+    private val ssid: Flow<String?> = repository.wifiModel.map { info ->
+        when {
+            info == null -> null
+            info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint ->
+                info.passpointProviderFriendlyName
+            info.ssid != WifiManager.UNKNOWN_SSID -> info.ssid
+            else -> null
+        }
+    }
+
+    val hasActivityIn: Flow<Boolean> = combine(repository.wifiActivity, ssid) { activity, ssid ->
+            activity.hasActivityIn && ssid != null
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
new file mode 100644
index 0000000..a19d1bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.pipeline.wifi.shared
+
+import android.content.Context
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * An object storing constants that we use for calculating the wifi icon. Stored in a class for
+ * logging purposes.
+ */
+@SysUISingleton
+class WifiConstants @Inject constructor(
+        context: Context,
+        dumpManager: DumpManager,
+) : Dumpable {
+    init {
+        dumpManager.registerDumpable("$SB_LOGGING_TAG:WifiConstants", this)
+    }
+
+    /** True if we should show the activityIn/activityOut icons and false otherwise. */
+    val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity)
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.apply {
+            println("shouldShowActivityConfig=$shouldShowActivityConfig")
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
new file mode 100644
index 0000000..b990eb7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.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.statusbar.pipeline.wifi.ui.viewmodel
+
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
+import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
+import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/**
+ * Models the UI state for the status bar wifi icon.
+ *
+ * TODO(b/238425913): Hook this up to the real status bar wifi view using a view binder.
+ */
+class WifiViewModel @Inject constructor(
+        private val constants: WifiConstants,
+        private val logger: ConnectivityPipelineLogger,
+        private val interactor: WifiInteractor,
+) {
+    val isActivityInVisible: Flow<Boolean>
+        get() =
+            if (!constants.shouldShowActivityConfig) {
+                flowOf(false)
+            } else {
+                interactor.hasActivityIn
+            }
+                .logOutputChange(logger, "activityInVisible")
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
index 699414c..e4e59a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -32,7 +32,6 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.Dependency;
 import com.android.systemui.EventLogTags;
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -81,12 +80,15 @@
         }
     }
 
-    public HeadsUpManager(@NonNull final Context context, HeadsUpManagerLogger logger,
-            @Main Handler handler) {
+    public HeadsUpManager(@NonNull final Context context,
+            HeadsUpManagerLogger logger,
+            @Main Handler handler,
+            AccessibilityManagerWrapper accessibilityManagerWrapper,
+            UiEventLogger uiEventLogger) {
         super(logger, handler);
         mContext = context;
-        mAccessibilityMgr = Dependency.get(AccessibilityManagerWrapper.class);
-        mUiEventLogger = Dependency.get(UiEventLogger.class);
+        mAccessibilityMgr = accessibilityManagerWrapper;
+        mUiEventLogger = uiEventLogger;
         Resources resources = context.getResources();
         mMinimumDisplayTime = resources.getInteger(R.integer.heads_up_notification_minimum_time);
         mAutoDismissNotificationDecay = resources.getInteger(R.integer.heads_up_notification_decay);
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 094490b..adef182 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -244,7 +244,8 @@
         final int currentUser = mUserTracker.getUserId();
         final boolean hadWallpaperColors = mCurrentColors.get(userId) != null;
         int latestWallpaperType = getLatestWallpaperType(userId);
-        if ((flags & latestWallpaperType) != 0) {
+        boolean eventForLatestWallpaper = (flags & latestWallpaperType) != 0;
+        if (eventForLatestWallpaper) {
             mCurrentColors.put(userId, wallpaperColors);
             if (DEBUG) Log.d(TAG, "got new colors: " + wallpaperColors + " where: " + flags);
         }
@@ -280,14 +281,19 @@
                 currentUser);
         boolean isDestinationBoth = (flags == (WallpaperManager.FLAG_SYSTEM
                 | WallpaperManager.FLAG_LOCK));
+        boolean isDestinationHomeOnly = (flags == WallpaperManager.FLAG_SYSTEM);
         try {
             JSONObject jsonObject = (overlayPackageJson == null) ? new JSONObject()
                     : new JSONObject(overlayPackageJson);
             // The latest applied wallpaper should be the source of system colors when:
             // There is not preset color applied and the incoming wallpaper color is not applied
-            if (!COLOR_SOURCE_PRESET.equals(jsonObject.optString(OVERLAY_COLOR_SOURCE))
-                    && ((flags & latestWallpaperType) != 0 && !isSeedColorSet(jsonObject,
-                    wallpaperColors))) {
+            String wallpaperPickerColorSource = jsonObject.optString(OVERLAY_COLOR_SOURCE);
+            boolean userChosePresetColor = COLOR_SOURCE_PRESET.equals(wallpaperPickerColorSource);
+            boolean userChoseLockScreenColor = COLOR_SOURCE_LOCK.equals(wallpaperPickerColorSource);
+            boolean preserveLockScreenColor = isDestinationHomeOnly && userChoseLockScreenColor;
+
+            if (!userChosePresetColor && !preserveLockScreenColor && eventForLatestWallpaper
+                    && !isSeedColorSet(jsonObject, wallpaperColors)) {
                 mSkipSettingChange = true;
                 if (jsonObject.has(OVERLAY_CATEGORY_ACCENT_COLOR) || jsonObject.has(
                         OVERLAY_CATEGORY_SYSTEM_PALETTE)) {
@@ -642,7 +648,7 @@
         }
         if (mNeedsOverlayCreation) {
             mNeedsOverlayCreation = false;
-            mThemeManager.applyCurrentUserOverlays(categoryToPackage, new FabricatedOverlay[] {
+            mThemeManager.applyCurrentUserOverlays(categoryToPackage, new FabricatedOverlay[]{
                     mSecondaryOverlay, mNeutralOverlay
             }, currentUser, managedProfiles);
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 0fe10cb..abffe555 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -26,6 +26,7 @@
 
 import androidx.annotation.Nullable;
 
+import com.android.internal.logging.UiEventLogger;
 import com.android.keyguard.KeyguardViewController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
@@ -63,6 +64,7 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.KeyguardEnvironmentImpl;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BatteryControllerImpl;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -173,7 +175,9 @@
             GroupMembershipManager groupManager,
             VisualStabilityProvider visualStabilityProvider,
             ConfigurationController configurationController,
-            @Main Handler handler) {
+            @Main Handler handler,
+            AccessibilityManagerWrapper accessibilityManagerWrapper,
+            UiEventLogger uiEventLogger) {
         return new HeadsUpManagerPhone(
                 context,
                 headsUpManagerLogger,
@@ -182,7 +186,9 @@
                 groupManager,
                 visualStabilityProvider,
                 configurationController,
-                handler
+                handler,
+                accessibilityManagerWrapper,
+                uiEventLogger
         );
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index 8f2a432..fc20ac2 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -138,7 +138,7 @@
 
         ensureOverlayRemoved()
 
-        val newRoot = SurfaceControlViewHost(context, context.display!!, wwm, false)
+        val newRoot = SurfaceControlViewHost(context, context.display!!, wwm)
         val newView =
             LightRevealScrim(context, null).apply {
                 revealEffect = createLightRevealEffect()
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 4c76270..aeab2df 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -38,6 +38,7 @@
 import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.service.dreams.IDreamManager;
 import android.service.notification.NotificationListenerService.RankingMap;
 import android.service.notification.ZenModeConfig;
 import android.util.Log;
@@ -101,6 +102,7 @@
     private final ShadeController mShadeController;
     private final IStatusBarService mBarService;
     private final INotificationManager mNotificationManager;
+    private final IDreamManager mDreamManager;
     private final NotificationVisibilityProvider mVisibilityProvider;
     private final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
     private final NotificationLockscreenUserManager mNotifUserManager;
@@ -126,6 +128,7 @@
             ShadeController shadeController,
             @Nullable IStatusBarService statusBarService,
             INotificationManager notificationManager,
+            IDreamManager dreamManager,
             NotificationVisibilityProvider visibilityProvider,
             NotificationInterruptStateProvider interruptionStateProvider,
             ZenModeController zenModeController,
@@ -144,6 +147,7 @@
                     shadeController,
                     statusBarService,
                     notificationManager,
+                    dreamManager,
                     visibilityProvider,
                     interruptionStateProvider,
                     zenModeController,
@@ -167,6 +171,7 @@
             ShadeController shadeController,
             @Nullable IStatusBarService statusBarService,
             INotificationManager notificationManager,
+            IDreamManager dreamManager,
             NotificationVisibilityProvider visibilityProvider,
             NotificationInterruptStateProvider interruptionStateProvider,
             ZenModeController zenModeController,
@@ -182,6 +187,7 @@
         mNotificationShadeWindowController = notificationShadeWindowController;
         mShadeController = shadeController;
         mNotificationManager = notificationManager;
+        mDreamManager = dreamManager;
         mVisibilityProvider = visibilityProvider;
         mNotificationInterruptStateProvider = interruptionStateProvider;
         mNotifUserManager = notifUserManager;
@@ -203,7 +209,7 @@
             @Override
             public void onKeyguardShowingChanged() {
                 boolean isUnlockedShade = !keyguardStateController.isShowing()
-                        && !keyguardStateController.isOccluded();
+                        && !isDreamingOrInPreview();
                 bubbles.onStatusBarStateChanged(isUnlockedShade);
             }
         });
@@ -397,6 +403,15 @@
         mBubbles.setSysuiProxy(mSysuiProxy);
     }
 
+    private boolean isDreamingOrInPreview() {
+        try {
+            return mDreamManager.isDreamingOrInPreview();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to query dream manager.", e);
+            return false;
+        }
+    }
+
     private void setupNotifPipeline() {
         mNotifPipeline.addCollectionListener(new NotifCollectionListener() {
             @Override
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index 763a5cb..ba28045 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -53,7 +53,8 @@
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.REGISTER_WINDOW_MANAGER_LISTENERS" />
 
-    <application android:debuggable="true" android:largeHeap="true">
+    <application android:debuggable="true" android:largeHeap="true"
+            android:enableOnBackInvokedCallback="true" >
         <uses-library android:name="android.test.runner" />
 
         <receiver android:name="com.android.systemui.SliceBroadcastRelayHandlerTest$Receiver"
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index c281965..3e00aec 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -16,6 +16,7 @@
 
 package com.android.keyguard;
 
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT;
 import static android.telephony.SubscriptionManager.DATA_ROAMING_DISABLE;
 import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID;
@@ -1006,7 +1007,7 @@
 
         // WHEN udfps is now enrolled
         when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true);
-        callback.onEnrollmentsChanged();
+        callback.onEnrollmentsChanged(TYPE_FINGERPRINT);
 
         // THEN isUdfspEnrolled is TRUE
         assertThat(mKeyguardUpdateMonitor.isUdfpsEnrolled()).isTrue();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
index 273786d..8fc0489 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt
@@ -664,6 +664,60 @@
     }
 
     @Test
+    fun animateAddition_runnableRunsWhenAnimationEnds() {
+        var runnableRun = false
+        val onAnimationEndRunnable = { runnableRun = true }
+
+        ViewHierarchyAnimator.animateAddition(
+                rootView,
+                origin = ViewHierarchyAnimator.Hotspot.CENTER,
+                includeMargins = true,
+                onAnimationEnd = onAnimationEndRunnable
+        )
+        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+        endAnimation(rootView)
+
+        assertEquals(true, runnableRun)
+    }
+
+    @Test
+    fun animateAddition_runnableDoesNotRunWhenAnimationCancelled() {
+        var runnableRun = false
+        val onAnimationEndRunnable = { runnableRun = true }
+
+        ViewHierarchyAnimator.animateAddition(
+            rootView,
+            origin = ViewHierarchyAnimator.Hotspot.CENTER,
+            includeMargins = true,
+            onAnimationEnd = onAnimationEndRunnable
+        )
+        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+        cancelAnimation(rootView)
+
+        assertEquals(false, runnableRun)
+    }
+
+    @Test
+    fun animationAddition_runnableDoesNotRunWhenOnlyPartwayThroughAnimation() {
+        var runnableRun = false
+        val onAnimationEndRunnable = { runnableRun = true }
+
+        ViewHierarchyAnimator.animateAddition(
+            rootView,
+            origin = ViewHierarchyAnimator.Hotspot.CENTER,
+            includeMargins = true,
+            onAnimationEnd = onAnimationEndRunnable
+        )
+        rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
+
+        advanceAnimation(rootView, 0.5f)
+
+        assertEquals(false, runnableRun)
+    }
+
+    @Test
     fun animatesViewRemovalFromStartToEnd() {
         setUpRootWithChildren()
 
@@ -1158,6 +1212,16 @@
         }
     }
 
+    private fun cancelAnimation(rootView: View) {
+        (rootView.getTag(R.id.tag_animator) as? ObjectAnimator)?.cancel()
+
+        if (rootView is ViewGroup) {
+            for (i in 0 until rootView.childCount) {
+                cancelAnimation(rootView.getChildAt(i))
+            }
+        }
+    }
+
     private fun endFadeInAnimation(rootView: View) {
         (rootView.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.end()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index d158892..e0d1f7a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -60,6 +60,9 @@
 import android.hardware.biometrics.SensorProperties;
 import android.hardware.display.DisplayManager;
 import android.hardware.face.FaceManager;
+import android.hardware.face.FaceSensorProperties;
+import android.hardware.face.FaceSensorPropertiesInternal;
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
@@ -154,7 +157,9 @@
     @Mock
     private InteractionJankMonitor mInteractionJankMonitor;
     @Captor
-    ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mAuthenticatorsRegisteredCaptor;
+    ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mFpAuthenticatorsRegisteredCaptor;
+    @Captor
+    ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback> mFaceAuthenticatorsRegisteredCaptor;
     @Captor
     ArgumentCaptor<BiometricStateListener> mBiometricStateCaptor;
     @Captor
@@ -193,25 +198,38 @@
         when(mDisplayManager.getStableDisplaySize()).thenReturn(new Point());
 
         when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFaceManager.isHardwareDetected()).thenReturn(true);
 
-        final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
-        componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */,
-                "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */,
-                "00000001" /* serialNumber */, "" /* softwareVersion */));
-        componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */,
-                "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
-                "vendor/version/revision" /* softwareVersion */));
+        final List<ComponentInfoInternal> fpComponentInfo = List.of(
+                new ComponentInfoInternal("faceSensor" /* componentId */,
+                        "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */,
+                        "00000001" /* serialNumber */, "" /* softwareVersion */));
+        final List<ComponentInfoInternal> faceComponentInfo = List.of(
+                new ComponentInfoInternal("matchingAlgorithm" /* componentId */,
+                        "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
+                        "vendor/version/revision" /* softwareVersion */));
 
-        FingerprintSensorPropertiesInternal prop = new FingerprintSensorPropertiesInternal(
-                1 /* sensorId */,
-                SensorProperties.STRENGTH_STRONG,
-                1 /* maxEnrollmentsPerUser */,
-                componentInfo,
-                FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
-                true /* resetLockoutRequireHardwareAuthToken */);
-        List<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
-        props.add(prop);
-        when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(props);
+        final List<FingerprintSensorPropertiesInternal> fpProps = List.of(
+                new FingerprintSensorPropertiesInternal(
+                        1 /* sensorId */,
+                        SensorProperties.STRENGTH_STRONG,
+                        1 /* maxEnrollmentsPerUser */,
+                        fpComponentInfo,
+                        FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
+                        true /* resetLockoutRequireHardwareAuthToken */));
+        when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(fpProps);
+
+        final List<FaceSensorPropertiesInternal> faceProps = List.of(
+                new FaceSensorPropertiesInternal(
+                        2 /* sensorId */,
+                        SensorProperties.STRENGTH_STRONG,
+                        1 /* maxEnrollmentsPerUser */,
+                        fpComponentInfo,
+                        FaceSensorProperties.TYPE_RGB,
+                        true /* supportsFaceDetection */,
+                        true /* supportsSelfIllumination */,
+                        true /* resetLockoutRequireHardwareAuthToken */));
+        when(mFaceManager.getSensorPropertiesInternal()).thenReturn(faceProps);
 
         mAuthController = new TestableAuthController(mContextSpy, mExecution, mCommandQueue,
                 mActivityTaskManager, mWindowManager, mFingerprintManager, mFaceManager,
@@ -219,12 +237,15 @@
 
         mAuthController.start();
         verify(mFingerprintManager).addAuthenticatorsRegisteredCallback(
-                mAuthenticatorsRegisteredCaptor.capture());
+                mFpAuthenticatorsRegisteredCaptor.capture());
+        verify(mFaceManager).addAuthenticatorsRegisteredCallback(
+                mFaceAuthenticatorsRegisteredCaptor.capture());
 
         when(mStatusBarStateController.isDozing()).thenReturn(false);
         verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture());
 
-        mAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(props);
+        mFpAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(fpProps);
+        mFaceAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(faceProps);
 
         // Ensures that the operations posted on the handler get executed.
         mTestableLooper.processAllMessages();
@@ -237,6 +258,7 @@
             throws RemoteException {
         // This test is sensitive to prior FingerprintManager interactions.
         reset(mFingerprintManager);
+        reset(mFaceManager);
 
         // This test requires an uninitialized AuthController.
         AuthController authController = new TestableAuthController(mContextSpy, mExecution,
@@ -246,21 +268,27 @@
         authController.start();
 
         verify(mFingerprintManager).addAuthenticatorsRegisteredCallback(
-                mAuthenticatorsRegisteredCaptor.capture());
+                mFpAuthenticatorsRegisteredCaptor.capture());
+        verify(mFaceManager).addAuthenticatorsRegisteredCallback(
+                mFaceAuthenticatorsRegisteredCaptor.capture());
         mTestableLooper.processAllMessages();
 
         verify(mFingerprintManager, never()).registerBiometricStateListener(any());
+        verify(mFaceManager, never()).registerBiometricStateListener(any());
 
-        mAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(new ArrayList<>());
+        mFpAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(List.of());
+        mFaceAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(List.of());
         mTestableLooper.processAllMessages();
 
         verify(mFingerprintManager).registerBiometricStateListener(any());
+        verify(mFaceManager).registerBiometricStateListener(any());
     }
 
     @Test
     public void testDoesNotCrash_afterEnrollmentsChangedForUnknownSensor() throws RemoteException {
         // This test is sensitive to prior FingerprintManager interactions.
         reset(mFingerprintManager);
+        reset(mFaceManager);
 
         // This test requires an uninitialized AuthController.
         AuthController authController = new TestableAuthController(mContextSpy, mExecution,
@@ -270,18 +298,25 @@
         authController.start();
 
         verify(mFingerprintManager).addAuthenticatorsRegisteredCallback(
-                mAuthenticatorsRegisteredCaptor.capture());
+                mFpAuthenticatorsRegisteredCaptor.capture());
+        verify(mFaceManager).addAuthenticatorsRegisteredCallback(
+                mFaceAuthenticatorsRegisteredCaptor.capture());
 
         // Emulates a device with no authenticators (empty list).
-        mAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(new ArrayList<>());
+        mFpAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(List.of());
+        mFaceAuthenticatorsRegisteredCaptor.getValue().onAllAuthenticatorsRegistered(List.of());
         mTestableLooper.processAllMessages();
 
         verify(mFingerprintManager).registerBiometricStateListener(
                 mBiometricStateCaptor.capture());
+        verify(mFaceManager).registerBiometricStateListener(
+                mBiometricStateCaptor.capture());
 
         // Enrollments changed for an unknown sensor.
-        mBiometricStateCaptor.getValue().onEnrollmentsChanged(0 /* userId */,
-                0xbeef /* sensorId */, true /* hasEnrollments */);
+        for (BiometricStateListener listener : mBiometricStateCaptor.getAllValues()) {
+            listener.onEnrollmentsChanged(0 /* userId */,
+                    0xbeef /* sensorId */, true /* hasEnrollments */);
+        }
         mTestableLooper.processAllMessages();
 
         // Nothing should crash.
@@ -827,4 +862,3 @@
         }
     }
 }
-
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index 9ffc5a5..b33f9a7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.doze;
 
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
 import static com.android.systemui.doze.DozeLog.REASON_SENSOR_TAP;
 import static com.android.systemui.doze.DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS;
 import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_LOCK_SCREEN;
@@ -412,7 +414,7 @@
 
         // WHEN enrollment changes to TRUE
         when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(true);
-        mAuthControllerCallback.onEnrollmentsChanged();
+        mAuthControllerCallback.onEnrollmentsChanged(TYPE_FINGERPRINT);
 
         // THEN mConfigured = TRUE
         assertTrue(triggerSensor.mConfigured);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
index 141a213..b42b769 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
@@ -42,8 +42,11 @@
 import android.testing.TestableLooper;
 import android.view.GestureDetector;
 import android.view.IWindowManager;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.WindowManagerPolicyConstants;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import androidx.test.filters.SmallTest;
 
@@ -73,6 +76,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -117,6 +122,8 @@
     @Mock private CentralSurfaces mCentralSurfaces;
     @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @Mock private DialogLaunchAnimator mDialogLaunchAnimator;
+    @Mock private OnBackInvokedDispatcher mOnBackInvokedDispatcher;
+    @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback;
 
     private TestableLooper mTestableLooper;
 
@@ -203,6 +210,58 @@
     }
 
     @Test
+    public void testPredictiveBackCallbackRegisteredAndUnregistered() {
+        mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
+        doReturn(4).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
+        doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayLockdown(any());
+        doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any());
+        String[] actions = {
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART,
+        };
+        doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
+
+        GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog();
+        dialog.setBackDispatcherOverride(mOnBackInvokedDispatcher);
+        dialog.create();
+        mTestableLooper.processAllMessages();
+        verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback(
+                eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), any());
+        dialog.onDetachedFromWindow();
+        mTestableLooper.processAllMessages();
+        verify(mOnBackInvokedDispatcher).unregisterOnBackInvokedCallback(any());
+    }
+
+    @Test
+    public void testPredictiveBackInvocationDismissesDialog() {
+        mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
+        doReturn(4).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
+        doReturn(true).when(mGlobalActionsDialogLite).shouldDisplayLockdown(any());
+        doReturn(true).when(mGlobalActionsDialogLite).shouldShowAction(any());
+        String[] actions = {
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialogLite.GLOBAL_ACTION_KEY_RESTART,
+        };
+        doReturn(actions).when(mGlobalActionsDialogLite).getDefaultActions();
+
+        GlobalActionsDialogLite.ActionsDialogLite dialog = mGlobalActionsDialogLite.createDialog();
+        dialog.create();
+        dialog.show();
+        mTestableLooper.processAllMessages();
+        dialog.getWindow().injectInputEvent(
+                new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK));
+        dialog.getWindow().injectInputEvent(
+                new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK));
+        mTestableLooper.processAllMessages();
+        verifyLogPosted(GlobalActionsDialogLite.GlobalActionsEvent.GA_CLOSE_BACK);
+        assertThat(dialog.isShowing()).isFalse();
+    }
+
+    @Test
     public void testSingleTap_logAndDismiss() {
         mGlobalActionsDialogLite = spy(mGlobalActionsDialogLite);
         doReturn(4).when(mGlobalActionsDialogLite).getMaxShownPowerItems();
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 24d0515..5ec6bdf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.keyguard;
 
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.keyguard.LockIconView.ICON_LOCK;
 import static com.android.keyguard.LockIconView.ICON_UNLOCK;
@@ -219,7 +221,7 @@
         Pair<Float, PointF> udfps = setupUdfps();
 
         // WHEN all authenticators are registered
-        mAuthControllerCallback.onAllAuthenticatorsRegistered();
+        mAuthControllerCallback.onAllAuthenticatorsRegistered(TYPE_FINGERPRINT);
         mDelayableExecutor.runAllReady();
 
         // THEN lock icon view location is updated with the same coordinates as auth controller vals
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
index fcfef4a4..c41fac7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/KeyguardMediaControllerTest.kt
@@ -16,19 +16,22 @@
 
 package com.android.systemui.media
 
+import android.provider.Settings
 import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
 import android.view.View.GONE
 import android.view.View.VISIBLE
 import android.widget.FrameLayout
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.stack.MediaContainerView
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.utils.os.FakeHandler
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertTrue
 import org.junit.Before
@@ -37,11 +40,12 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
 class KeyguardMediaControllerTest : SysuiTestCase() {
 
     @Mock
@@ -53,31 +57,33 @@
     @Mock
     private lateinit var configurationController: ConfigurationController
 
-    @Mock
-    private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
     @JvmField @Rule
     val mockito = MockitoJUnit.rule()
 
     private val mediaContainerView: MediaContainerView = MediaContainerView(context, null)
     private val hostView = UniqueObjectHostView(context)
+    private val settings = FakeSettings()
     private lateinit var keyguardMediaController: KeyguardMediaController
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var fakeHandler: FakeHandler
 
     @Before
     fun setup() {
         // default state is positive, media should show up
         whenever(mediaHost.visible).thenReturn(true)
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
-        whenever(notificationLockscreenUserManager.shouldShowLockscreenNotifications())
-                .thenReturn(true)
         whenever(mediaHost.hostView).thenReturn(hostView)
         hostView.layoutParams = FrameLayout.LayoutParams(100, 100)
+        testableLooper = TestableLooper.get(this)
+        fakeHandler = FakeHandler(testableLooper.looper)
         keyguardMediaController = KeyguardMediaController(
             mediaHost,
             bypassController,
             statusBarStateController,
-            notificationLockscreenUserManager,
             context,
-            configurationController
+            settings,
+            fakeHandler,
+            configurationController,
         )
         keyguardMediaController.attachSinglePaneContainer(mediaContainerView)
         keyguardMediaController.useSplitShade = false
@@ -106,9 +112,8 @@
     }
 
     @Test
-    fun testHiddenOnKeyguard_whenNotificationsAreHidden() {
-        whenever(notificationLockscreenUserManager.shouldShowLockscreenNotifications())
-                .thenReturn(false)
+    fun testHiddenOnKeyguard_whenMediaOnLockScreenDisabled() {
+        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 0)
 
         keyguardMediaController.refreshMediaPosition()
 
@@ -116,6 +121,15 @@
     }
 
     @Test
+    fun testAvailableOnKeyguard_whenMediaOnLockScreenEnabled() {
+        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1)
+
+        keyguardMediaController.refreshMediaPosition()
+
+        assertThat(mediaContainerView.visibility).isEqualTo(VISIBLE)
+    }
+
+    @Test
     fun testActivatesSplitShadeContainerInSplitShadeMode() {
         val splitShadeContainer = FrameLayout(context)
         keyguardMediaController.attachSplitShadeContainer(splitShadeContainer)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
index 1cce7cf..d1ed8e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -50,11 +50,10 @@
 import org.mockito.Mockito
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
-import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.junit.MockitoJUnit
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
 
 private const val KEY = "KEY"
 private const val KEY_2 = "KEY_2"
@@ -287,6 +286,30 @@
     }
 
     @Test
+    fun testOnNotificationAdded_hasSubstituteName_isUsed() {
+        val subName = "Substitute Name"
+        val notif = SbnBuilder().run {
+            modifyNotification(context).also {
+                it.extras = Bundle().apply {
+                    putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName)
+                }
+                it.setStyle(MediaStyle().apply {
+                    setMediaSession(session.sessionToken)
+                })
+            }
+            build()
+        }
+
+        mediaDataManager.onNotificationAdded(KEY, notif)
+        assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
+        assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
+        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true),
+            eq(0), eq(false))
+
+        assertThat(mediaDataCaptor.value!!.app).isEqualTo(subName)
+    }
+
+    @Test
     fun testLoadMediaDataInBg_invalidTokenNoCrash() {
         val bundle = Bundle()
         // wrong data type
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
index d65b6b3..18bfd04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.media
 
-import org.mockito.Mockito.`when` as whenever
 import android.graphics.Rect
+import android.provider.Settings
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.ViewGroup
@@ -30,7 +30,7 @@
 import com.android.systemui.dreams.DreamOverlayStateController
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.shade.testing.FakeNotifPanelEvents
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.KeyguardBypassController
@@ -38,6 +38,8 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.animation.UniqueObjectHostView
 import com.android.systemui.util.mockito.any
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.utils.os.FakeHandler
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertNotNull
 import org.junit.Before
@@ -50,10 +52,10 @@
 import org.mockito.ArgumentMatchers.anyLong
 import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
 
 @SmallTest
@@ -61,32 +63,18 @@
 @TestableLooper.RunWithLooper
 class MediaHierarchyManagerTest : SysuiTestCase() {
 
-    @Mock
-    private lateinit var lockHost: MediaHost
-    @Mock
-    private lateinit var qsHost: MediaHost
-    @Mock
-    private lateinit var qqsHost: MediaHost
-    @Mock
-    private lateinit var bypassController: KeyguardBypassController
-    @Mock
-    private lateinit var keyguardStateController: KeyguardStateController
-    @Mock
-    private lateinit var statusBarStateController: SysuiStatusBarStateController
-    @Mock
-    private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
-    @Mock
-    private lateinit var mediaCarouselController: MediaCarouselController
-    @Mock
-    private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
-    @Mock
-    private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
-    @Mock
-    private lateinit var keyguardViewController: KeyguardViewController
-    @Mock
-    private lateinit var uniqueObjectHostView: UniqueObjectHostView
-    @Mock
-    private lateinit var dreamOverlayStateController: DreamOverlayStateController
+    @Mock private lateinit var lockHost: MediaHost
+    @Mock private lateinit var qsHost: MediaHost
+    @Mock private lateinit var qqsHost: MediaHost
+    @Mock private lateinit var bypassController: KeyguardBypassController
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
+    @Mock private lateinit var mediaCarouselController: MediaCarouselController
+    @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler
+    @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+    @Mock private lateinit var keyguardViewController: KeyguardViewController
+    @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView
+    @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController
     @Captor
     private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)>
     @Captor
@@ -94,34 +82,42 @@
     @JvmField
     @Rule
     val mockito = MockitoJUnit.rule()
-    private lateinit var mediaHiearchyManager: MediaHierarchyManager
+    private lateinit var mediaHierarchyManager: MediaHierarchyManager
     private lateinit var mediaFrame: ViewGroup
     private val configurationController = FakeConfigurationController()
+    private val notifPanelEvents = FakeNotifPanelEvents()
+    private val settings = FakeSettings()
+    private lateinit var testableLooper: TestableLooper
+    private lateinit var fakeHandler: FakeHandler
 
     @Before
     fun setup() {
         context.getOrCreateTestableResources().addOverride(
                 R.bool.config_use_split_notification_shade, false)
         mediaFrame = FrameLayout(context)
-        `when`(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
-        mediaHiearchyManager = MediaHierarchyManager(
+        testableLooper = TestableLooper.get(this)
+        fakeHandler = FakeHandler(testableLooper.looper)
+        whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
+        mediaHierarchyManager = MediaHierarchyManager(
                 context,
                 statusBarStateController,
                 keyguardStateController,
                 bypassController,
                 mediaCarouselController,
-                notificationLockscreenUserManager,
+                keyguardViewController,
+                dreamOverlayStateController,
                 configurationController,
                 wakefulnessLifecycle,
-                keyguardViewController,
-                dreamOverlayStateController)
+                notifPanelEvents,
+                settings,
+                fakeHandler,)
         verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
         verify(statusBarStateController).addCallback(statusBarCallback.capture())
         setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN, LOCKSCREEN_TOP)
         setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP)
         setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP)
-        `when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
-        `when`(mediaCarouselController.mediaCarouselScrollHandler)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+        whenever(mediaCarouselController.mediaCarouselScrollHandler)
                 .thenReturn(mediaCarouselScrollHandler)
         val observer = wakefullnessObserver.value
         assertNotNull("lifecycle observer wasn't registered", observer)
@@ -131,30 +127,30 @@
     }
 
     private fun setupHost(host: MediaHost, location: Int, top: Int) {
-        `when`(host.location).thenReturn(location)
-        `when`(host.currentBounds).thenReturn(Rect(0, top, 0, top))
-        `when`(host.hostView).thenReturn(uniqueObjectHostView)
-        `when`(host.visible).thenReturn(true)
-        mediaHiearchyManager.register(host)
+        whenever(host.location).thenReturn(location)
+        whenever(host.currentBounds).thenReturn(Rect(0, top, 0, top))
+        whenever(host.hostView).thenReturn(uniqueObjectHostView)
+        whenever(host.visible).thenReturn(true)
+        mediaHierarchyManager.register(host)
     }
 
     @Test
     fun testHostViewSetOnRegister() {
-        val host = mediaHiearchyManager.register(lockHost)
+        val host = mediaHierarchyManager.register(lockHost)
         verify(lockHost).hostView = eq(host)
     }
 
     @Test
     fun testBlockedWhenScreenTurningOff() {
         // Let's set it onto QS:
-        mediaHiearchyManager.qsExpansion = 1.0f
+        mediaHierarchyManager.qsExpansion = 1.0f
         verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
                 any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
         val observer = wakefullnessObserver.value
         assertNotNull("lifecycle observer wasn't registered", observer)
         observer.onStartedGoingToSleep()
         clearInvocations(mediaCarouselController)
-        mediaHiearchyManager.qsExpansion = 0.0f
+        mediaHierarchyManager.qsExpansion = 0.0f
         verify(mediaCarouselController, times(0))
                 .onDesiredLocationChanged(ArgumentMatchers.anyInt(),
                 any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
@@ -163,13 +159,13 @@
     @Test
     fun testAllowedWhenNotTurningOff() {
         // Let's set it onto QS:
-        mediaHiearchyManager.qsExpansion = 1.0f
+        mediaHierarchyManager.qsExpansion = 1.0f
         verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
                 any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
         val observer = wakefullnessObserver.value
         assertNotNull("lifecycle observer wasn't registered", observer)
         clearInvocations(mediaCarouselController)
-        mediaHiearchyManager.qsExpansion = 0.0f
+        mediaHierarchyManager.qsExpansion = 0.0f
         verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
                 any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
     }
@@ -179,7 +175,7 @@
         goToLockscreen()
 
         // Let's transition all the way to full shade
-        mediaHiearchyManager.setTransitionToFullShadeAmount(100000f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(100000f)
         verify(mediaCarouselController).onDesiredLocationChanged(
             eq(MediaHierarchyManager.LOCATION_QQS),
             any(MediaHostState::class.java),
@@ -189,7 +185,7 @@
         clearInvocations(mediaCarouselController)
 
         // Let's go back to the lock screen
-        mediaHiearchyManager.setTransitionToFullShadeAmount(0.0f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(0.0f)
         verify(mediaCarouselController).onDesiredLocationChanged(
             eq(MediaHierarchyManager.LOCATION_LOCKSCREEN),
             any(MediaHostState::class.java),
@@ -198,7 +194,7 @@
             anyLong())
 
         // Let's make sure alpha is set
-        mediaHiearchyManager.setTransitionToFullShadeAmount(2.0f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(2.0f)
         assertThat(mediaFrame.alpha).isNotEqualTo(1.0f)
     }
 
@@ -207,7 +203,26 @@
         goToLockscreen()
         expandQS()
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
+        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
+    }
+
+    @Test
+    fun calculateTransformationType_notOnLockscreen_returnsTransition() {
+        expandQS()
+
+        val transformType = mediaHierarchyManager.calculateTransformationType()
+
+        assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION)
+    }
+
+    @Test
+    fun calculateTransformationType_onLockscreen_returnsTransition() {
+        goToLockscreen()
+        expandQS()
+
+        val transformType = mediaHierarchyManager.calculateTransformationType()
+
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
     }
 
@@ -216,9 +231,9 @@
         enableSplitShade()
         goToLockscreen()
         expandQS()
-        mediaHiearchyManager.setTransitionToFullShadeAmount(10000f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(10000f)
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_TRANSITION)
     }
 
@@ -228,9 +243,9 @@
         goToLockscreen()
         expandQS()
         whenever(lockHost.visible).thenReturn(false)
-        mediaHiearchyManager.setTransitionToFullShadeAmount(10000f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(10000f)
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
     }
 
@@ -240,9 +255,9 @@
         goToLockscreen()
         goToLockedShade()
         expandQS()
-        mediaHiearchyManager.setTransitionToFullShadeAmount(0f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(0f)
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
     }
 
@@ -251,13 +266,13 @@
         goToLockscreen()
         goToLockedShade()
 
-        val transformType = mediaHiearchyManager.calculateTransformationType()
+        val transformType = mediaHierarchyManager.calculateTransformationType()
         assertThat(transformType).isEqualTo(MediaHierarchyManager.TRANSFORMATION_TYPE_FADE)
     }
 
     @Test
     fun testCloseGutsRelayToCarousel() {
-        mediaHiearchyManager.closeGuts()
+        mediaHierarchyManager.closeGuts()
 
         verify(mediaCarouselController).closeGuts()
     }
@@ -271,7 +286,7 @@
 
     @Test
     fun getGuidedTransformationTranslationY_notInGuidedTransformation_returnsNegativeNumber() {
-        assertThat(mediaHiearchyManager.getGuidedTransformationTranslationY()).isLessThan(0)
+        assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY()).isLessThan(0)
     }
 
     @Test
@@ -279,7 +294,7 @@
         enterGuidedTransformation()
 
         val expectedTranslation = LOCKSCREEN_TOP - QS_TOP
-        assertThat(mediaHiearchyManager.getGuidedTransformationTranslationY())
+        assertThat(mediaHierarchyManager.getGuidedTransformationTranslationY())
                 .isEqualTo(expectedTranslation)
     }
 
@@ -291,7 +306,19 @@
         whenever(qsHost.visible).thenReturn(true)
         whenever(qqsHost.visible).thenReturn(true)
 
-        assertThat(mediaHiearchyManager.isCurrentlyInGuidedTransformation()).isTrue()
+        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue()
+    }
+
+    @Test
+    fun isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalse() {
+        notifPanelEvents.changeExpandImmediate(expandImmediate = true)
+        goToLockscreen()
+        enterGuidedTransformation()
+        whenever(lockHost.visible).thenReturn(true)
+        whenever(qsHost.visible).thenReturn(true)
+        whenever(qqsHost.visible).thenReturn(true)
+
+        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
     }
 
     @Test
@@ -302,7 +329,7 @@
         whenever(qsHost.visible).thenReturn(true)
         whenever(qqsHost.visible).thenReturn(true)
 
-        assertThat(mediaHiearchyManager.isCurrentlyInGuidedTransformation()).isFalse()
+        assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
     }
 
     private fun enableSplitShade() {
@@ -314,9 +341,7 @@
 
     private fun goToLockscreen() {
         whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
-        whenever(notificationLockscreenUserManager.shouldShowLockscreenNotifications()).thenReturn(
-            true
-        )
+        settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1)
         statusBarCallback.value.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD)
         clearInvocations(mediaCarouselController)
     }
@@ -330,13 +355,13 @@
     }
 
     private fun expandQS() {
-        mediaHiearchyManager.qsExpansion = 1.0f
+        mediaHierarchyManager.qsExpansion = 1.0f
     }
 
     private fun enterGuidedTransformation() {
-        mediaHiearchyManager.qsExpansion = 1.0f
+        mediaHierarchyManager.qsExpansion = 1.0f
         goToLockscreen()
-        mediaHiearchyManager.setTransitionToFullShadeAmount(123f)
+        mediaHierarchyManager.setTransitionToFullShadeAmount(123f)
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 314997d..6173692 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -24,9 +24,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.KeyguardManager;
 import android.content.Context;
 import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
 import android.media.AudioManager;
 import android.media.session.MediaController;
 import android.media.session.MediaSessionManager;
@@ -88,6 +88,7 @@
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
     private final AudioManager mAudioManager = mock(AudioManager.class);
     private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
+    private KeyguardManager mKeyguardManager = mock(KeyguardManager.class);
 
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
@@ -119,7 +120,8 @@
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
         mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mBroadcastSender,
                 mMediaOutputController);
         mMediaOutputBaseDialogImpl.onCreate(new Bundle());
@@ -276,7 +278,7 @@
         }
 
         @Override
-        Drawable getAppSourceIcon() {
+        IconCompat getAppSourceIcon() {
             return null;
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index 751c895..6dcf802 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -31,6 +31,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.KeyguardManager;
 import android.app.Notification;
 import android.content.Context;
 import android.graphics.drawable.Icon;
@@ -47,6 +48,7 @@
 import android.service.notification.StatusBarNotification;
 import android.testing.AndroidTestingRunner;
 import android.text.TextUtils;
+import android.view.View;
 
 import androidx.core.graphics.drawable.IconCompat;
 import androidx.test.filters.SmallTest;
@@ -57,6 +59,7 @@
 import com.android.settingslib.media.MediaDevice;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
 import com.android.systemui.plugins.ActivityStarter;
@@ -102,11 +105,16 @@
     private RoutingSessionInfo mRemoteSessionInfo = mock(RoutingSessionInfo.class);
     private ActivityStarter mStarter = mock(ActivityStarter.class);
     private AudioManager mAudioManager = mock(AudioManager.class);
+    private KeyguardManager mKeyguardManager = mock(KeyguardManager.class);
     private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
     private CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class);
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
+    private final ActivityLaunchAnimator.Controller mActivityLaunchAnimatorController = mock(
+            ActivityLaunchAnimator.Controller.class);
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
             NearbyMediaDevicesManager.class);
+    private View mDialogLaunchView = mock(View.class);
+    private MediaOutputController.Callback mCallback = mock(MediaOutputController.Callback.class);
 
     private Context mSpyContext;
     private MediaOutputController mMediaOutputController;
@@ -131,7 +139,8 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
         mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         MediaDescription.Builder builder = new MediaDescription.Builder();
@@ -183,7 +192,8 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, null,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
 
         mMediaOutputController.start(mCb);
 
@@ -212,7 +222,8 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, null,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
 
         mMediaOutputController.start(mCb);
 
@@ -461,7 +472,8 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, null,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
 
         assertThat(mMediaOutputController.getNotificationIcon()).isNull();
     }
@@ -557,4 +569,16 @@
         verify(mPowerExemptionManager).addToTemporaryAllowList(anyString(), anyInt(), anyString(),
                 anyLong());
     }
+
+    @Test
+    public void launchBluetoothPairing_isKeyguardLocked_dismissDialog() {
+        when(mDialogLaunchAnimator.createActivityLaunchController(mDialogLaunchView)).thenReturn(
+                mActivityLaunchAnimatorController);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        mMediaOutputController.mCallback = this.mCallback;
+
+        mMediaOutputController.launchBluetoothPairing(mDialogLaunchView);
+
+        verify(mCallback).dismissDialog();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index 4779d32..9557513 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.KeyguardManager;
 import android.media.AudioManager;
 import android.media.MediaRoute2Info;
 import android.media.session.MediaController;
@@ -85,6 +86,7 @@
             NearbyMediaDevicesManager.class);
     private final AudioManager mAudioManager = mock(AudioManager.class);
     private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
+    private KeyguardManager mKeyguardManager = mock(KeyguardManager.class);
 
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private MediaOutputDialog mMediaOutputDialog;
@@ -104,7 +106,8 @@
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         mMediaOutputDialog = new MediaOutputDialog(mContext, false, mBroadcastSender,
                 mMediaOutputController, mUiEventLogger);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
index 247316a..c101b9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.media.dream;
 
+import static com.android.systemui.flags.Flags.MEDIA_DREAM_COMPLICATION;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
@@ -28,6 +30,7 @@
 
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.MediaData;
 import com.android.systemui.media.MediaDataManager;
 
@@ -50,6 +53,9 @@
     @Mock
     MediaDreamComplication mComplication;
 
+    @Mock
+    FeatureFlags mFeatureFlags;
+
     final String mKey = "key";
     final String mOldKey = "old_key";
 
@@ -59,21 +65,18 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+
+        when(mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)).thenReturn(true);
     }
 
     @Test
     public void testComplicationAddition() {
         final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
-                mDreamOverlayStateController, mComplication);
+                mDreamOverlayStateController, mComplication, mFeatureFlags);
 
         sentinel.start();
 
-        ArgumentCaptor<MediaDataManager.Listener> listenerCaptor =
-                ArgumentCaptor.forClass(MediaDataManager.Listener.class);
-        verify(mMediaDataManager).addListener(listenerCaptor.capture());
-
-        final MediaDataManager.Listener listener = listenerCaptor.getValue();
-
+        final MediaDataManager.Listener listener = captureMediaDataListener();
         when(mMediaDataManager.hasActiveMedia()).thenReturn(false);
         listener.onMediaDataLoaded(mKey, mOldKey, mData, /* immediately= */ true,
                 /* receivedSmartspaceCardLatency= */ 0, /* isSsReactived= */ false);
@@ -92,4 +95,27 @@
         verify(mDreamOverlayStateController).removeComplication(eq(mComplication));
     }
 
+    @Test
+    public void testMediaDreamSentinel_mediaComplicationDisabled_doNotAddComplication() {
+        when(mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)).thenReturn(false);
+
+        final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
+                mDreamOverlayStateController, mComplication, mFeatureFlags);
+
+        sentinel.start();
+
+        final MediaDataManager.Listener listener = captureMediaDataListener();
+        when(mMediaDataManager.hasActiveMedia()).thenReturn(true);
+        listener.onMediaDataLoaded(mKey, mOldKey, mData, /* immediately= */true,
+                /* receivedSmartspaceCardLatency= */0, /* isSsReactived= */ false);
+        verify(mDreamOverlayStateController, never()).addComplication(any());
+    }
+
+    private MediaDataManager.Listener captureMediaDataListener() {
+        final ArgumentCaptor<MediaDataManager.Listener> listenerCaptor =
+                ArgumentCaptor.forClass(MediaDataManager.Listener.class);
+        verify(mMediaDataManager).addListener(listenerCaptor.capture());
+
+        return listenerCaptor.getValue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
index 1f28210..e4f47fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.qs
 
 import android.content.res.Configuration
-import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.systemui.SysuiTestCase
@@ -38,38 +38,32 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.Captor
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.any
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 class QuickQSPanelControllerTest : SysuiTestCase() {
 
-    @Mock
-    private lateinit var quickQSPanel: QuickQSPanel
-    @Mock
-    private lateinit var qsTileHost: QSTileHost
-    @Mock
-    private lateinit var qsCustomizerController: QSCustomizerController
-    @Mock
-    private lateinit var mediaHost: MediaHost
-    @Mock
-    private lateinit var metricsLogger: MetricsLogger
+    @Mock private lateinit var quickQSPanel: QuickQSPanel
+    @Mock private lateinit var qsTileHost: QSTileHost
+    @Mock private lateinit var qsCustomizerController: QSCustomizerController
+    @Mock private lateinit var mediaHost: MediaHost
+    @Mock private lateinit var metricsLogger: MetricsLogger
+    @Mock private lateinit var qsLogger: QSLogger
+    @Mock private lateinit var tile: QSTile
+    @Mock private lateinit var tileLayout: TileLayout
+    @Mock private lateinit var tileView: QSTileView
+    @Captor private lateinit var captor: ArgumentCaptor<QSPanel.OnConfigurationChangedListener>
+
     private val uiEventLogger = UiEventLoggerFake()
-    @Mock
-    private lateinit var qsLogger: QSLogger
     private val dumpManager = DumpManager()
-    @Mock
-    private lateinit var tile: QSTile
-    @Mock
-    private lateinit var tileLayout: TileLayout
-    @Mock
-    private lateinit var tileView: QSTileView
-    @Captor
-    private lateinit var captor: ArgumentCaptor<QSPanel.OnConfigurationChangedListener>
+
+    private var usingCollapsedLandscapeMedia = true
 
     private lateinit var controller: TestQuickQSPanelController
 
@@ -77,24 +71,24 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        `when`(quickQSPanel.tileLayout).thenReturn(tileLayout)
-        `when`(quickQSPanel.isAttachedToWindow).thenReturn(true)
-        `when`(quickQSPanel.dumpableTag).thenReturn("")
-        `when`(quickQSPanel.resources).thenReturn(mContext.resources)
-        `when`(qsTileHost.createTileView(any(), any(), anyBoolean())).thenReturn(tileView)
+        whenever(quickQSPanel.tileLayout).thenReturn(tileLayout)
+        whenever(quickQSPanel.isAttachedToWindow).thenReturn(true)
+        whenever(quickQSPanel.dumpableTag).thenReturn("")
+        whenever(quickQSPanel.resources).thenReturn(mContext.resources)
+        whenever(qsTileHost.createTileView(any(), any(), anyBoolean())).thenReturn(tileView)
 
-        controller = TestQuickQSPanelController(
+        controller =
+            TestQuickQSPanelController(
                 quickQSPanel,
                 qsTileHost,
                 qsCustomizerController,
-                false,
+                /* usingMediaPlayer = */ false,
                 mediaHost,
-                true,
+                { usingCollapsedLandscapeMedia },
                 metricsLogger,
                 uiEventLogger,
                 qsLogger,
-                dumpManager
-        )
+                dumpManager)
 
         controller.init()
     }
@@ -106,9 +100,9 @@
 
     @Test
     fun testTileSublistWithFewerTiles_noCrash() {
-        `when`(quickQSPanel.numQuickTiles).thenReturn(3)
+        whenever(quickQSPanel.numQuickTiles).thenReturn(3)
 
-        `when`(qsTileHost.tiles).thenReturn(listOf(tile, tile))
+        whenever(qsTileHost.tiles).thenReturn(listOf(tile, tile))
 
         controller.setTiles()
     }
@@ -116,8 +110,8 @@
     @Test
     fun testTileSublistWithTooManyTiles() {
         val limit = 3
-        `when`(quickQSPanel.numQuickTiles).thenReturn(limit)
-        `when`(qsTileHost.tiles).thenReturn(listOf(tile, tile, tile, tile))
+        whenever(quickQSPanel.numQuickTiles).thenReturn(limit)
+        whenever(qsTileHost.tiles).thenReturn(listOf(tile, tile, tile, tile))
 
         controller.setTiles()
 
@@ -125,39 +119,61 @@
     }
 
     @Test
-    fun testMediaExpansionUpdatedWhenConfigurationChanged() {
+    fun mediaExpansion_afterConfigChange_inLandscape_collapsedInLandscapeTrue_updatesToCollapsed() {
         // times(2) because both controller and base controller are registering their listeners
         verify(quickQSPanel, times(2)).addOnConfigurationChangedListener(captor.capture())
 
         // verify that media starts in the expanded state by default
         verify(mediaHost).expansion = MediaHostState.EXPANDED
 
-        // Rotate device, verify media size updated
+        // Rotate device, verify media size updated to collapsed
+        usingCollapsedLandscapeMedia = true
         controller.setRotation(RotationUtils.ROTATION_LANDSCAPE)
         captor.allValues.forEach { it.onConfigurationChange(Configuration.EMPTY) }
 
         verify(mediaHost).expansion = MediaHostState.COLLAPSED
     }
 
+    @Test
+    fun mediaExpansion_afterConfigChange_landscape_collapsedInLandscapeFalse_remainsExpanded() {
+        // times(2) because both controller and base controller are registering their listeners
+        verify(quickQSPanel, times(2)).addOnConfigurationChangedListener(captor.capture())
+        reset(mediaHost)
+
+        usingCollapsedLandscapeMedia = false
+        controller.setRotation(RotationUtils.ROTATION_LANDSCAPE)
+        captor.allValues.forEach { it.onConfigurationChange(Configuration.EMPTY) }
+
+        verify(mediaHost).expansion = MediaHostState.EXPANDED
+    }
+
     class TestQuickQSPanelController(
         view: QuickQSPanel,
         qsTileHost: QSTileHost,
         qsCustomizerController: QSCustomizerController,
         usingMediaPlayer: Boolean,
         mediaHost: MediaHost,
-        usingCollapsedLandscapeMedia: Boolean,
+        usingCollapsedLandscapeMedia: () -> Boolean,
         metricsLogger: MetricsLogger,
         uiEventLogger: UiEventLoggerFake,
         qsLogger: QSLogger,
         dumpManager: DumpManager
-    ) : QuickQSPanelController(view, qsTileHost, qsCustomizerController, usingMediaPlayer,
-        mediaHost, usingCollapsedLandscapeMedia, metricsLogger, uiEventLogger, qsLogger,
-        dumpManager) {
+    ) :
+        QuickQSPanelController(
+            view,
+            qsTileHost,
+            qsCustomizerController,
+            usingMediaPlayer,
+            mediaHost,
+            usingCollapsedLandscapeMedia,
+            metricsLogger,
+            uiEventLogger,
+            qsLogger,
+            dumpManager) {
 
         private var rotation = RotationUtils.ROTATION_NONE
 
-        @Override
-        override fun getRotation(): Int = rotation
+        @Override override fun getRotation(): Int = rotation
 
         fun setRotation(newRotation: Int) {
             rotation = newRotation
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeImageCapture.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeImageCapture.kt
new file mode 100644
index 0000000..447e28c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeImageCapture.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.screenshot
+
+import android.graphics.Bitmap
+import android.graphics.Rect
+
+internal class FakeImageCapture : ImageCapture {
+
+    var requestedDisplayId: Int? = null
+    var requestedDisplayCrop: Rect? = null
+    var requestedTaskId: Int? = null
+
+    var image: Bitmap? = null
+
+    override fun captureDisplay(displayId: Int, crop: Rect?): Bitmap? {
+        requestedDisplayId = displayId
+        requestedDisplayCrop = crop
+        return image
+    }
+
+    override suspend fun captureTask(taskId: Int): Bitmap? {
+        requestedTaskId = taskId
+        return image
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeScreenshotPolicy.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeScreenshotPolicy.kt
new file mode 100644
index 0000000..28d53c7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeScreenshotPolicy.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.screenshot
+
+import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
+
+internal class FakeScreenshotPolicy : ScreenshotPolicy {
+
+    private val userTypes = mutableMapOf<Int, Boolean>()
+    private val contentInfo = mutableMapOf<Int, DisplayContentInfo?>()
+
+    fun setManagedProfile(userId: Int, managedUser: Boolean) {
+        userTypes[userId] = managedUser
+    }
+    override suspend fun isManagedProfile(userId: Int): Boolean {
+        return userTypes[userId] ?: error("No managedProfile value set for userId $userId")
+    }
+
+    fun setDisplayContentInfo(userId: Int, contentInfo: DisplayContentInfo) {
+        this.contentInfo[userId] = contentInfo
+    }
+
+    override suspend fun findPrimaryContent(displayId: Int): DisplayContentInfo {
+        return contentInfo[displayId] ?: error("No DisplayContentInfo set for displayId $displayId")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageCaptureImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageCaptureImplTest.kt
index ce3f20d..00f3808 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageCaptureImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageCaptureImplTest.kt
@@ -27,6 +27,8 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,7 +39,10 @@
 class ImageCaptureImplTest : SysuiTestCase() {
     private val displayManager = mock<DisplayManager>()
     private val atmService = mock<IActivityTaskManager>()
-    private val capture = TestableImageCaptureImpl(displayManager, atmService)
+    private val capture = TestableImageCaptureImpl(
+        displayManager,
+        atmService,
+        Dispatchers.Unconfined)
 
     @Test
     fun captureDisplayWithCrop() {
@@ -59,9 +64,10 @@
 
     class TestableImageCaptureImpl(
         displayManager: DisplayManager,
-        atmService: IActivityTaskManager
+        atmService: IActivityTaskManager,
+        bgDispatcher: CoroutineDispatcher
     ) :
-        ImageCaptureImpl(displayManager, atmService) {
+        ImageCaptureImpl(displayManager, atmService, bgDispatcher) {
 
         var token: IBinder? = null
         var width: Int? = null
@@ -81,4 +87,4 @@
             return ScreenshotHardwareBuffer(null, null, false, false)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
index 024d3bd..48fbd35 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -22,86 +22,253 @@
 import android.graphics.Insets
 import android.graphics.Rect
 import android.hardware.HardwareBuffer
-import android.net.Uri
+import android.os.Bundle
 import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
 import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER
 import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
-import android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-
+import android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION
 import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler
+import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler.bundleToHardwareBitmap
 import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
-import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
 import com.google.common.truth.Truth.assertThat
-import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
 import org.junit.Test
-import org.mockito.Mockito.eq
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.isNull
+
+private const val USER_ID = 1
+private const val TASK_ID = 1
 
 class RequestProcessorTest {
-    private val controller = mock<ScreenshotController>()
-    private val bitmapCaptor = argumentCaptor<Bitmap>()
+    private val imageCapture = FakeImageCapture()
+    private val component = ComponentName("android.test", "android.test.Component")
+    private val bounds = Rect(25, 25, 75, 75)
 
+    private val scope = CoroutineScope(Dispatchers.Unconfined)
+    private val dispatcher = Dispatchers.Unconfined
+    private val policy = FakeScreenshotPolicy()
+    private val flags = FakeFeatureFlags()
+
+    /** Tests the Java-compatible function wrapper, ensures callback is invoked. */
     @Test
-    fun testFullScreenshot() {
+    fun testProcessAsync() {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
         val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
-        val onSavedListener = mock<Consumer<Uri>>()
-        val callback = mock<RequestCallback>()
-        val processor = RequestProcessor(controller)
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
 
-        processor.processRequest(request, onSavedListener, callback)
+        var result: ScreenshotRequest? = null
+        var callbackCount = 0
+        val callback: (ScreenshotRequest) -> Unit = { processedRequest: ScreenshotRequest ->
+            result = processedRequest
+            callbackCount++
+        }
 
-        verify(controller).takeScreenshotFullscreen(/* topComponent */ isNull(),
-            eq(onSavedListener), eq(callback))
+        // runs synchronously, using Unconfined Dispatcher
+        processor.processAsync(request, callback)
+
+        // Callback invoked once returning the same request (no changes)
+        assertThat(callbackCount).isEqualTo(1)
+        assertThat(result).isEqualTo(request)
     }
 
     @Test
-    fun testSelectedRegionScreenshot() {
+    fun testFullScreenshot_workProfilePolicyDisabled() = runBlocking {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
+        val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+        val processedRequest = processor.process(request)
+
+        // No changes
+        assertThat(processedRequest).isEqualTo(request)
+    }
+
+    @Test
+    fun testFullScreenshot() = runBlocking {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+        // Indicate that the primary content belongs to a normal user
+        policy.setManagedProfile(USER_ID, false)
+        policy.setDisplayContentInfo(
+            policy.getDefaultDisplayId(),
+            DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+
+        val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+        val processedRequest = processor.process(request)
+
+        // Request has topComponent added, but otherwise unchanged.
+        assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN)
+        assertThat(processedRequest.topComponent).isEqualTo(component)
+    }
+
+    @Test
+    fun testFullScreenshot_managedProfile() = runBlocking {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+        // Provide a fake task bitmap when asked
+        val bitmap = makeHardwareBitmap(100, 100)
+        imageCapture.image = bitmap
+
+        // Indicate that the primary content belongs to a manged profile
+        policy.setManagedProfile(USER_ID, true)
+        policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
+            DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+
+        val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+        val processedRequest = processor.process(request)
+
+        // Expect a task snapshot is taken, overriding the full screen mode
+        assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_PROVIDED_IMAGE)
+        assertThat(bitmap.equalsHardwareBitmapBundle(processedRequest.bitmapBundle)).isTrue()
+        assertThat(processedRequest.boundsInScreen).isEqualTo(bounds)
+        assertThat(processedRequest.insets).isEqualTo(Insets.NONE)
+        assertThat(processedRequest.taskId).isEqualTo(TASK_ID)
+        assertThat(imageCapture.requestedTaskId).isEqualTo(TASK_ID)
+        assertThat(processedRequest.userId).isEqualTo(USER_ID)
+        assertThat(processedRequest.topComponent).isEqualTo(component)
+    }
+
+    @Test
+    fun testSelectedRegionScreenshot_workProfilePolicyDisabled() = runBlocking {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
         val request = ScreenshotRequest(TAKE_SCREENSHOT_SELECTED_REGION, SCREENSHOT_KEY_CHORD)
-        val onSavedListener = mock<Consumer<Uri>>()
-        val callback = mock<RequestCallback>()
-        val processor = RequestProcessor(controller)
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
 
-        processor.processRequest(request, onSavedListener, callback)
+        val processedRequest = processor.process(request)
 
-        verify(controller).takeScreenshotPartial(/* topComponent */ isNull(),
-            eq(onSavedListener), eq(callback))
+        // No changes
+        assertThat(processedRequest).isEqualTo(request)
+     }
+
+    @Test
+    fun testSelectedRegionScreenshot() = runBlocking {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+        val request = ScreenshotRequest(TAKE_SCREENSHOT_SELECTED_REGION, SCREENSHOT_KEY_CHORD)
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+        policy.setManagedProfile(USER_ID, false)
+        policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
+            DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+
+        val processedRequest = processor.process(request)
+
+        // Request has topComponent added, but otherwise unchanged.
+        assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_FULLSCREEN)
+        assertThat(processedRequest.topComponent).isEqualTo(component)
     }
 
     @Test
-    fun testProvidedImageScreenshot() {
-        val taskId = 1111
-        val userId = 2222
-        val bounds = Rect(50, 50, 150, 150)
-        val topComponent = ComponentName("test", "test")
-        val processor = RequestProcessor(controller)
+    fun testSelectedRegionScreenshot_managedProfile() = runBlocking {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
 
-        val buffer = HardwareBuffer.create(100, 100, HardwareBuffer.RGBA_8888, 1,
-            HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE)
-        val bitmap = Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!!
+        // Provide a fake task bitmap when asked
+        val bitmap = makeHardwareBitmap(100, 100)
+        imageCapture.image = bitmap
+
+        val request = ScreenshotRequest(TAKE_SCREENSHOT_SELECTED_REGION, SCREENSHOT_KEY_CHORD)
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+        // Indicate that the primary content belongs to a manged profile
+        policy.setManagedProfile(USER_ID, true)
+        policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
+            DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+
+        val processedRequest = processor.process(request)
+
+        // Expect a task snapshot is taken, overriding the selected region mode
+        assertThat(processedRequest.type).isEqualTo(TAKE_SCREENSHOT_PROVIDED_IMAGE)
+        assertThat(bitmap.equalsHardwareBitmapBundle(processedRequest.bitmapBundle)).isTrue()
+        assertThat(processedRequest.boundsInScreen).isEqualTo(bounds)
+        assertThat(processedRequest.insets).isEqualTo(Insets.NONE)
+        assertThat(processedRequest.taskId).isEqualTo(TASK_ID)
+        assertThat(imageCapture.requestedTaskId).isEqualTo(TASK_ID)
+        assertThat(processedRequest.userId).isEqualTo(USER_ID)
+        assertThat(processedRequest.topComponent).isEqualTo(component)
+    }
+
+    @Test
+    fun testProvidedImageScreenshot_workProfilePolicyDisabled() = runBlocking {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, false)
+
+        val bounds = Rect(50, 50, 150, 150)
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+        val bitmap = makeHardwareBitmap(100, 100)
         val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
 
         val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
-            bitmapBundle, bounds, Insets.NONE, taskId, userId, topComponent)
+            bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
 
-        val onSavedListener = mock<Consumer<Uri>>()
-        val callback = mock<RequestCallback>()
+        val processedRequest = processor.process(request)
 
-        processor.processRequest(request, onSavedListener, callback)
-
-        verify(controller).handleImageAsScreenshot(
-            bitmapCaptor.capture(), eq(bounds), eq(Insets.NONE), eq(taskId), eq(userId),
-            eq(topComponent), eq(onSavedListener), eq(callback)
-        )
-
-        assertThat(bitmapCaptor.value.equalsHardwareBitmap(bitmap)).isTrue()
+        // No changes
+        assertThat(processedRequest).isEqualTo(request)
     }
 
-    private fun Bitmap.equalsHardwareBitmap(bitmap: Bitmap): Boolean {
-        return bitmap.hardwareBuffer == this.hardwareBuffer &&
-                bitmap.colorSpace == this.colorSpace
+    @Test
+    fun testProvidedImageScreenshot() = runBlocking {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+        val bounds = Rect(50, 50, 150, 150)
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+        policy.setManagedProfile(USER_ID, false)
+
+        val bitmap = makeHardwareBitmap(100, 100)
+        val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
+
+        val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
+            bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
+
+        val processedRequest = processor.process(request)
+
+        // No changes
+        assertThat(processedRequest).isEqualTo(request)
+    }
+
+    @Test
+    fun testProvidedImageScreenshot_managedProfile() = runBlocking {
+        flags.set(Flags.SCREENSHOT_WORK_PROFILE_POLICY, true)
+
+        val bounds = Rect(50, 50, 150, 150)
+        val processor = RequestProcessor(imageCapture, policy, flags, scope)
+
+        // Indicate that the screenshot belongs to a manged profile
+        policy.setManagedProfile(USER_ID, true)
+
+        val bitmap = makeHardwareBitmap(100, 100)
+        val bitmapBundle = HardwareBitmapBundler.hardwareBitmapToBundle(bitmap)
+
+        val request = ScreenshotRequest(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER,
+            bitmapBundle, bounds, Insets.NONE, TASK_ID, USER_ID, component)
+
+        val processedRequest = processor.process(request)
+
+        // Work profile, but already a task snapshot, so no changes
+        assertThat(processedRequest).isEqualTo(request)
+    }
+
+    private fun makeHardwareBitmap(width: Int, height: Int): Bitmap {
+        val buffer = HardwareBuffer.create(width, height, HardwareBuffer.RGBA_8888, 1,
+            HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE)
+        return Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB))!!
+    }
+
+    private fun Bitmap.equalsHardwareBitmapBundle(bundle: Bundle): Boolean {
+        val provided = bundleToHardwareBitmap(bundle)
+        return provided.hardwareBuffer == this.hardwareBuffer &&
+                provided.colorSpace == this.colorSpace
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 3f4e2a9..95211c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -238,6 +238,8 @@
     @Mock
     private DozeLog mDozeLog;
     @Mock
+    private ShadeLogger mShadeLog;
+    @Mock
     private CommandQueue mCommandQueue;
     @Mock
     private VibratorHelper mVibratorHelper;
@@ -524,7 +526,9 @@
                 mNotificationShadeWindowController,
                 mDozeLog, mDozeParameters, mCommandQueue, mVibratorHelper,
                 mLatencyTracker, mPowerManager, mAccessibilityManager, 0, mUpdateMonitor,
-                mMetricsLogger, mConfigurationController,
+                mMetricsLogger,
+                mShadeLog,
+                mConfigurationController,
                 () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager,
                 mConversationNotificationManager, mMediaHiearchyManager,
                 mStatusBarKeyguardViewManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt
new file mode 100644
index 0000000..d052138
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/testing/FakeNotifPanelEvents.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.testing
+
+import com.android.systemui.shade.NotifPanelEvents
+
+/** Fake implementation of [NotifPanelEvents] for testing. */
+class FakeNotifPanelEvents : NotifPanelEvents {
+
+    private val listeners = mutableListOf<NotifPanelEvents.Listener>()
+
+    override fun registerListener(listener: NotifPanelEvents.Listener) {
+        listeners.add(listener)
+    }
+
+    override fun unregisterListener(listener: NotifPanelEvents.Listener) {
+        listeners.remove(listener)
+    }
+
+    fun changeExpandImmediate(expandImmediate: Boolean) {
+        listeners.forEach { it.onExpandImmediateChanged(expandImmediate) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
index 64d0256..214ba16 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
@@ -98,7 +98,7 @@
 
     @Test
     fun testPrepareDialogForApp_onlyDefaultChannel() {
-        group.addChannel(channelDefault)
+        group.channels = listOf(channelDefault)
 
         controller.prepareDialogForApp(TEST_APP_NAME, TEST_PACKAGE_NAME, TEST_UID,
                 setOf(channelDefault), appIcon, clickListener)
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 c25737d..cc4cbbf 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
@@ -48,6 +48,7 @@
 import android.widget.RemoteViews;
 
 import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.TestableDependency;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
@@ -75,6 +76,7 @@
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
 import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder;
@@ -145,7 +147,9 @@
                 mock(GroupMembershipManager.class),
                 mock(VisualStabilityProvider.class),
                 mock(ConfigurationControllerImpl.class),
-                new Handler(mTestLooper.getLooper())
+                new Handler(mTestLooper.getLooper()),
+                mock(AccessibilityManagerWrapper.class),
+                mock(UiEventLogger.class)
         );
         mIconManager = new IconManager(
                 mock(CommonNotifCollection.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
index ac3d0c2..e252401 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
@@ -29,6 +29,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.AlertingNotificationManager;
 import com.android.systemui.statusbar.AlertingNotificationManagerTest;
@@ -65,6 +66,8 @@
     @Mock private StatusBarStateController mStatusBarStateController;
     @Mock private KeyguardBypassController mBypassController;
     @Mock private ConfigurationControllerImpl mConfigurationController;
+    @Mock private AccessibilityManagerWrapper mAccessibilityManagerWrapper;
+    @Mock private UiEventLogger mUiEventLogger;
     private boolean mLivesPastNormalTime;
 
     private static final class TestableHeadsUpManagerPhone extends HeadsUpManagerPhone {
@@ -76,7 +79,9 @@
                 StatusBarStateController statusBarStateController,
                 KeyguardBypassController keyguardBypassController,
                 ConfigurationController configurationController,
-                Handler handler
+                Handler handler,
+                AccessibilityManagerWrapper accessibilityManagerWrapper,
+                UiEventLogger uiEventLogger
         ) {
             super(
                     context,
@@ -86,7 +91,9 @@
                     groupManager,
                     visualStabilityProvider,
                     configurationController,
-                    handler
+                    handler,
+                    accessibilityManagerWrapper,
+                    uiEventLogger
             );
             mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
             mAutoDismissNotificationDecay = TEST_AUTO_DISMISS_TIME;
@@ -116,7 +123,9 @@
                 mStatusBarStateController,
                 mBypassController,
                 mConfigurationController,
-                mTestHandler
+                mTestHandler,
+                mAccessibilityManagerWrapper,
+                mUiEventLogger
         );
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt
index 515a7c9..7b492cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessorTest.kt
@@ -20,7 +20,7 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.repository.NetworkCapabilityInfo
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.NetworkCapabilityInfo
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
@@ -59,6 +59,7 @@
                 context,
                 scope,
                 statusBarPipelineFlags,
+                mock(),
         )
 
         var mostRecentValue: ProcessedConnectivityInfo? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLoggerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
index 2915ae8..36be1be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.pipeline
+package com.android.systemui.statusbar.pipeline.shared
 
 import android.net.Network
 import android.net.NetworkCapabilities
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
new file mode 100644
index 0000000..df389bc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.pipeline.wifi.data.repository
+
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Fake implementation of [WifiRepository] exposing set methods for all the flows. */
+class FakeWifiRepository : WifiRepository {
+    private val _wifiModel: MutableStateFlow<WifiModel?> = MutableStateFlow(null)
+    override val wifiModel: Flow<WifiModel?> = _wifiModel
+
+    private val _wifiActivity = MutableStateFlow(ACTIVITY_DEFAULT)
+    override val wifiActivity: Flow<WifiActivityModel> = _wifiActivity
+
+    fun setWifiModel(wifiModel: WifiModel?) {
+        _wifiModel.value = wifiModel
+    }
+
+    fun setWifiActivity(activity: WifiActivityModel) {
+        _wifiActivity.value = activity
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepoTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt
index 40f8fbf..6edf76c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepoTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/NetworkCapabilitiesRepoTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.pipeline.repository
+package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
 import android.net.ConnectivityManager
 import android.net.ConnectivityManager.NetworkCallback
@@ -28,7 +28,7 @@
 import android.test.suitebuilder.annotation.SmallTest
 import android.testing.AndroidTestingRunner
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
new file mode 100644
index 0000000..8b61364
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -0,0 +1,175 @@
+/*
+ * 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.pipeline.wifi.data.repository
+
+import android.net.wifi.WifiManager
+import android.net.wifi.WifiManager.TrafficStateCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class WifiRepositoryImplTest : SysuiTestCase() {
+
+    private lateinit var underTest: WifiRepositoryImpl
+
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var wifiManager: WifiManager
+    private lateinit var executor: Executor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        executor = FakeExecutor(FakeSystemClock())
+    }
+
+    @Test
+    fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) {
+        underTest = WifiRepositoryImpl(
+                wifiManager = null,
+                executor,
+                logger,
+        )
+
+        var latest: WifiActivityModel? = null
+        val job = underTest
+                .wifiActivity
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isEqualTo(ACTIVITY_DEFAULT)
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiActivity_callbackGivesNone_activityFlowHasNone() = runBlocking(IMMEDIATE) {
+        underTest = WifiRepositoryImpl(
+                wifiManager,
+                executor,
+                logger,
+        )
+
+        var latest: WifiActivityModel? = null
+        val job = underTest
+                .wifiActivity
+                .onEach { latest = it }
+                .launchIn(this)
+
+        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_NONE)
+
+        assertThat(latest).isEqualTo(
+            WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
+        )
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiActivity_callbackGivesIn_activityFlowHasIn() = runBlocking(IMMEDIATE) {
+        underTest = WifiRepositoryImpl(
+                wifiManager,
+                executor,
+                logger,
+        )
+
+        var latest: WifiActivityModel? = null
+        val job = underTest
+                .wifiActivity
+                .onEach { latest = it }
+                .launchIn(this)
+
+        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_IN)
+
+        assertThat(latest).isEqualTo(
+            WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+        )
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiActivity_callbackGivesOut_activityFlowHasOut() = runBlocking(IMMEDIATE) {
+        underTest = WifiRepositoryImpl(
+                wifiManager,
+                executor,
+                logger,
+        )
+
+        var latest: WifiActivityModel? = null
+        val job = underTest
+                .wifiActivity
+                .onEach { latest = it }
+                .launchIn(this)
+
+        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_OUT)
+
+        assertThat(latest).isEqualTo(
+            WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
+        )
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() = runBlocking(IMMEDIATE) {
+        underTest = WifiRepositoryImpl(
+                wifiManager,
+                executor,
+                logger,
+        )
+
+        var latest: WifiActivityModel? = null
+        val job = underTest
+                .wifiActivity
+                .onEach { latest = it }
+                .launchIn(this)
+
+        getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_INOUT)
+
+        assertThat(latest).isEqualTo(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
+
+        job.cancel()
+    }
+
+    private fun getTrafficStateCallback(): TrafficStateCallback {
+        val callbackCaptor = argumentCaptor<TrafficStateCallback>()
+        verify(wifiManager).registerTrafficStateCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+}
+
+private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
new file mode 100644
index 0000000..c52f347
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.pipeline.wifi.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class WifiInteractorTest : SysuiTestCase() {
+
+    private lateinit var underTest: WifiInteractor
+
+    private lateinit var repository: FakeWifiRepository
+
+    @Before
+    fun setUp() {
+        repository = FakeWifiRepository()
+        underTest = WifiInteractor(repository)
+    }
+
+    @Test
+    fun hasActivityIn_noInOrOut_outputsFalse() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun hasActivityIn_onlyOut_outputsFalse() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = true))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun hasActivityIn_onlyIn_outputsTrue() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun hasActivityIn_inAndOut_outputsTrue() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun hasActivityIn_ssidNull_outputsFalse() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = null))
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun hasActivityIn_multipleChanges_multipleOutputChanges() = runBlocking(IMMEDIATE) {
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+
+        var latest: Boolean? = null
+        val job = underTest
+                .hasActivityIn
+                .onEach { latest = it }
+                .launchIn(this)
+
+        // Conduct a series of changes and verify we catch each of them in succession
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
+        yield()
+        assertThat(latest).isTrue()
+
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = true))
+        yield()
+        assertThat(latest).isFalse()
+
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = true))
+        yield()
+        assertThat(latest).isTrue()
+
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
+        yield()
+        assertThat(latest).isTrue()
+
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = false, hasActivityOut = false))
+        yield()
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+}
+
+private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
new file mode 100644
index 0000000..e9259b0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.pipeline.wifi.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
+import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class WifiViewModelTest : SysuiTestCase() {
+
+    private lateinit var underTest: WifiViewModel
+
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var constants: WifiConstants
+    private lateinit var repository: FakeWifiRepository
+    private lateinit var interactor: WifiInteractor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        repository = FakeWifiRepository()
+        interactor = WifiInteractor(repository)
+
+        underTest = WifiViewModel(
+                constants,
+                logger,
+                interactor
+        )
+
+        // Set up with a valid SSID
+        repository.setWifiModel(WifiModel(ssid = "AB"))
+    }
+
+    @Test
+    fun activityInVisible_showActivityConfigFalse_receivesFalse() = runBlocking(IMMEDIATE) {
+        whenever(constants.shouldShowActivityConfig).thenReturn(false)
+
+        var latest: Boolean? = null
+        val job = underTest
+                .isActivityInVisible
+                .onEach { latest = it }
+                .launchIn(this)
+
+        // Verify that on launch, we receive a false.
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun activityInVisible_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) {
+        whenever(constants.shouldShowActivityConfig).thenReturn(false)
+
+        var latest: Boolean? = null
+        val job = underTest
+                .isActivityInVisible
+                .onEach { latest = it }
+                .launchIn(this)
+
+        // Update the repo to have activityIn
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
+        yield()
+
+        // Verify that we didn't update to activityIn=true (because our config is false)
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun activityInVisible_showActivityConfigTrue_receivesUpdate() = runBlocking(IMMEDIATE) {
+        whenever(constants.shouldShowActivityConfig).thenReturn(true)
+
+        var latest: Boolean? = null
+        val job = underTest
+                .isActivityInVisible
+                .onEach { latest = it }
+                .launchIn(this)
+
+        // Update the repo to have activityIn
+        repository.setWifiActivity(WifiActivityModel(hasActivityIn = true, hasActivityOut = false))
+        yield()
+
+        // Verify that we updated to activityIn=true
+        assertThat(latest).isTrue()
+
+        job.cancel()
+    }
+}
+
+private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java
index e2b9a9e..9764b8c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java
@@ -63,7 +63,6 @@
     private static final int TEST_A11Y_AUTO_DISMISS_TIME = 600;
     private static final int TEST_A11Y_TIMEOUT_TIME = 5_000;
 
-    private AccessibilityManagerWrapper mAccessibilityMgr;
     private HeadsUpManager mHeadsUpManager;
     private boolean mLivesPastNormalTime;
     private UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake();
@@ -72,10 +71,15 @@
     @Mock private StatusBarNotification mSbn;
     @Mock private Notification mNotification;
     @Mock private HeadsUpManagerLogger mLogger;
+    @Mock private AccessibilityManagerWrapper mAccessibilityMgr;
 
     private final class TestableHeadsUpManager extends HeadsUpManager {
-        TestableHeadsUpManager(Context context, HeadsUpManagerLogger logger, Handler handler) {
-            super(context, logger, handler);
+        TestableHeadsUpManager(Context context,
+                HeadsUpManagerLogger logger,
+                Handler handler,
+                AccessibilityManagerWrapper accessibilityManagerWrapper,
+                UiEventLogger uiEventLogger) {
+            super(context, logger, handler, accessibilityManagerWrapper, uiEventLogger);
             mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
             mAutoDismissNotificationDecay = TEST_AUTO_DISMISS_TIME;
         }
@@ -88,13 +92,11 @@
     @Before
     public void setUp() {
         initMocks(this);
-        mAccessibilityMgr = mDependency.injectMockDependency(AccessibilityManagerWrapper.class);
-        mDependency.injectTestDependency(UiEventLogger.class, mUiEventLoggerFake);
         when(mEntry.getSbn()).thenReturn(mSbn);
         when(mSbn.getNotification()).thenReturn(mNotification);
-
         super.setUp();
-        mHeadsUpManager = new TestableHeadsUpManager(mContext, mLogger, mTestHandler);
+        mHeadsUpManager = new TestableHeadsUpManager(mContext, mLogger, mTestHandler,
+                mAccessibilityMgr, mUiEventLoggerFake);
     }
 
     @After
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 b7f38f1..50259b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -310,7 +310,7 @@
     }
 
     @Test
-    public void onWallpaperColorsChanged_ResetThemeWithNewHomeWallpapers() {
+    public void onWallpaperColorsChanged_resetThemeWithNewHomeWallpapers() {
         // Should ask for a new theme when wallpaper colors change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
@@ -345,6 +345,61 @@
     }
 
     @Test
+    public void onWallpaperColorsChanged_keepsThemeWhenSetFromLockScreen() {
+        // Should ask for a new theme when wallpaper colors change
+        WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
+                Color.valueOf(Color.BLUE), null);
+        String jsonString =
+                "{\"android.theme.customization.color_source\":\"lock_wallpaper\","
+                        + "\"android.theme.customization.system_palette\":\"A16B00\","
+                        + "\"android.theme.customization.accent_color\":\"A16B00\","
+                        + "\"android.theme.customization.color_index\":\"2\"}";
+        when(mSecureSettings.getStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
+                .thenReturn(jsonString);
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_LOCK, USER_SYSTEM))
+                .thenReturn(20);
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_SYSTEM, USER_SYSTEM))
+                .thenReturn(21);
+        mColorsListener.getValue().onColorsChanged(mainColors, WallpaperManager.FLAG_SYSTEM,
+                USER_SYSTEM);
+        verify(mSecureSettings, never()).putStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), any(), anyInt());
+    }
+
+    @Test
+    public void onWallpaperColorsChanged_resetLockScreenThemeWhenBothSet() {
+        // Should ask for a new theme when wallpaper colors change
+        WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
+                Color.valueOf(Color.BLUE), null);
+        String jsonString =
+                "{\"android.theme.customization.color_source\":\"lock_wallpaper\","
+                        + "\"android.theme.customization.system_palette\":\"A16B00\","
+                        + "\"android.theme.customization.accent_color\":\"A16B00\","
+                        + "\"android.theme.customization.color_index\":\"2\"}";
+        when(mSecureSettings.getStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), anyInt()))
+                .thenReturn(jsonString);
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_LOCK, USER_SYSTEM))
+                .thenReturn(20);
+        when(mWallpaperManager.getWallpaperIdForUser(WallpaperManager.FLAG_SYSTEM, USER_SYSTEM))
+                .thenReturn(21);
+
+        mColorsListener.getValue().onColorsChanged(mainColors,
+                WallpaperManager.FLAG_SYSTEM | WallpaperManager.FLAG_LOCK,
+                USER_SYSTEM);
+
+        ArgumentCaptor<String> updatedSetting = ArgumentCaptor.forClass(String.class);
+        verify(mSecureSettings).putStringForUser(
+                eq(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES), updatedSetting.capture(),
+                anyInt());
+        assertThat(updatedSetting.getValue().contains(
+                "android.theme.customization.color_both\":\"1")).isTrue();
+        verify(mThemeOverlayApplier)
+                .applyCurrentUserOverlays(any(), any(), anyInt(), any());
+    }
+
+    @Test
     public void onSettingChanged_honorThemeStyle() {
         when(mDeviceProvisionedController.isUserSetup(anyInt())).thenReturn(true);
         List<Style> validStyles = Arrays.asList(Style.EXPRESSIVE, Style.SPRITZ, Style.TONAL_SPOT,
@@ -381,7 +436,7 @@
     }
 
     @Test
-    public void onWallpaperColorsChanged_ResetThemeWithNewHomeAndLockWallpaper() {
+    public void onWallpaperColorsChanged_resetThemeWithNewHomeAndLockWallpaper() {
         // Should ask for a new theme when wallpaper colors change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
@@ -450,7 +505,7 @@
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
         String jsonString =
-                "{\"android.theme.customization.color_source\":\"lock_wallpaper\","
+                "{\"android.theme.customization.color_source\":\"home_wallpaper\","
                         + "\"android.theme.customization.system_palette\":\"A16B00\","
                         + "\"android.theme.customization.accent_color\":\"A16B00\","
                         + "\"android.theme.customization.color_index\":\"2\"}";
@@ -476,7 +531,7 @@
     }
 
     @Test
-    public void onWallpaperColorsChanged_ResetThemeWhenFromLatestWallpaper() {
+    public void onWallpaperColorsChanged_resetThemeWhenFromLatestWallpaper() {
         // Should ask for a new theme when the colors of the last applied wallpaper change
         WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED),
                 Color.valueOf(Color.BLUE), null);
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 18acf3f..8f2b715 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -63,6 +63,7 @@
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.Handler;
 import android.os.PowerManager;
+import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.service.dreams.IDreamManager;
@@ -204,6 +205,8 @@
     private ArgumentCaptor<IntentFilter> mFilterArgumentCaptor;
     @Captor
     private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverArgumentCaptor;
+    @Captor
+    private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateControllerCallbackCaptor;
 
     private BubblesManager mBubblesManager;
     private TestableBubbleController mBubbleController;
@@ -240,6 +243,8 @@
     @Mock
     private IStatusBarService mStatusBarService;
     @Mock
+    private IDreamManager mIDreamManager;
+    @Mock
     private NotificationVisibilityProvider mVisibilityProvider;
     @Mock
     private LauncherApps mLauncherApps;
@@ -371,10 +376,11 @@
                 mContext,
                 mBubbleController.asBubbles(),
                 mNotificationShadeWindowController,
-                mock(KeyguardStateController.class),
+                mKeyguardStateController,
                 mShadeController,
                 mStatusBarService,
                 mock(INotificationManager.class),
+                mIDreamManager,
                 mVisibilityProvider,
                 interruptionStateProvider,
                 mZenModeController,
@@ -391,6 +397,25 @@
         verify(mNotifPipeline, atLeastOnce())
                 .addCollectionListener(mNotifListenerCaptor.capture());
         mEntryListener = mNotifListenerCaptor.getValue();
+
+        // Get a reference to KeyguardStateController.Callback
+        verify(mKeyguardStateController, atLeastOnce())
+                .addCallback(mKeyguardStateControllerCallbackCaptor.capture());
+    }
+
+    @Test
+    public void dreamingHidesBubbles() throws RemoteException {
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertTrue(mBubbleController.hasBubbles());
+        assertThat(mBubbleController.getStackView().getVisibility()).isEqualTo(View.VISIBLE);
+
+        when(mIDreamManager.isDreamingOrInPreview()).thenReturn(true); // dreaming is happening
+        when(mKeyguardStateController.isShowing()).thenReturn(false); // device is unlocked
+        KeyguardStateController.Callback callback =
+                mKeyguardStateControllerCallbackCaptor.getValue();
+        callback.onKeyguardShowingChanged();
+
+        assertThat(mBubbleController.getStackView().getVisibility()).isEqualTo(View.INVISIBLE);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
index 309acdf..f539dbd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
@@ -91,6 +91,8 @@
     fun capture(): T = wrapped.capture()
     val value: T
         get() = wrapped.value
+    val allValues: List<T>
+        get() = wrapped.allValues
 }
 
 /**
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index fe85db2..5a35474 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -231,7 +231,7 @@
             sendStateToClients(/* resetClient= */ false);
         }
         updateRemoteAugmentedAutofillService();
-        updateRemoteInlineSuggestionRenderServiceLocked();
+        getRemoteInlineSuggestionRenderServiceLocked();
 
         return enabledChanged;
     }
@@ -685,8 +685,12 @@
 
     @GuardedBy("mLock")
     void resetExtServiceLocked() {
-        if (sVerbose) Slog.v(TAG, "reset autofill service.");
+        if (sVerbose) Slog.v(TAG, "reset autofill service in ExtServices.");
         mFieldClassificationStrategy.reset();
+        if (mRemoteInlineSuggestionRenderService != null) {
+            mRemoteInlineSuggestionRenderService.destroy();
+            mRemoteInlineSuggestionRenderService = null;
+        }
     }
 
     @GuardedBy("mLock")
@@ -1583,18 +1587,6 @@
         return mFieldClassificationStrategy.getDefaultAlgorithm();
     }
 
-    private void updateRemoteInlineSuggestionRenderServiceLocked() {
-        if (mRemoteInlineSuggestionRenderService != null) {
-            if (sVerbose) {
-                Slog.v(TAG, "updateRemoteInlineSuggestionRenderService(): "
-                        + "destroying old remote service");
-            }
-            mRemoteInlineSuggestionRenderService = null;
-        }
-
-        mRemoteInlineSuggestionRenderService = getRemoteInlineSuggestionRenderServiceLocked();
-    }
-
     @Nullable RemoteInlineSuggestionRenderService getRemoteInlineSuggestionRenderServiceLocked() {
         if (mRemoteInlineSuggestionRenderService == null) {
             final ComponentName componentName = RemoteInlineSuggestionRenderService
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index 243a7e0..907daa3 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -160,7 +160,6 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Queue;
@@ -3401,7 +3400,8 @@
     }
 
     /**
-     * Selects transport {@code transportName} and returns previously selected transport.
+     * Selects transport {@code transportName}, if it is already registered, and returns previously
+     * selected transport. Returns {@code null} if the transport is not registered.
      *
      * @deprecated Use {@link #selectBackupTransportAsync(ComponentName,
      * ISelectBackupTransportCallback)} instead.
@@ -3414,6 +3414,17 @@
 
         final long oldId = Binder.clearCallingIdentity();
         try {
+            if (!mTransportManager.isTransportRegistered(transportName)) {
+                Slog.v(
+                        TAG,
+                        addUserIdToLogMessage(
+                                mUserId,
+                                "Could not select transport "
+                                        + transportName
+                                        + ", as the transport is not registered."));
+                return null;
+            }
+
             String previousTransportName = mTransportManager.selectTransport(transportName);
             updateStateForTransport(transportName);
             Slog.v(
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index 43e2b88..593a63c 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -77,6 +77,14 @@
             new ComponentName("android", BlockedAppStreamingActivity.class.getName());
 
     /**
+     * For communicating when a secure window shows on the virtual display.
+     */
+    public interface SecureWindowCallback {
+        /** Called when a secure window shows on the virtual display. */
+        void onSecureWindowShown(int displayId, int uid);
+    }
+
+    /**
      * If required, allow the secure activity to display on remote device since
      * {@link android.os.Build.VERSION_CODES#TIRAMISU}.
      */
@@ -108,6 +116,7 @@
             new ArraySet<>();
     @Nullable
     private final @AssociationRequest.DeviceProfile String mDeviceProfile;
+    @Nullable private final SecureWindowCallback mSecureWindowCallback;
 
     /**
      * Creates a window policy controller that is generic to the different use cases of virtual
@@ -131,6 +140,8 @@
      * @param activityListener Activity listener to listen for activity changes.
      * @param activityBlockedCallback Callback that is called when an activity is blocked from
      *   launching.
+     * @param secureWindowCallback Callback that is called when a secure window shows on the
+     *   virtual display.
      * @param deviceProfile The {@link AssociationRequest.DeviceProfile} of this virtual device.
      */
     public GenericWindowPolicyController(int windowFlags, int systemWindowFlags,
@@ -142,6 +153,7 @@
             @ActivityPolicy int defaultActivityPolicy,
             @NonNull ActivityListener activityListener,
             @NonNull ActivityBlockedCallback activityBlockedCallback,
+            @NonNull SecureWindowCallback secureWindowCallback,
             @AssociationRequest.DeviceProfile String deviceProfile) {
         super();
         mAllowedUsers = allowedUsers;
@@ -154,6 +166,7 @@
         setInterestedWindowFlags(windowFlags, systemWindowFlags);
         mActivityListener = activityListener;
         mDeviceProfile = deviceProfile;
+        mSecureWindowCallback = secureWindowCallback;
     }
 
     /**
@@ -234,6 +247,12 @@
     @Override
     public boolean keepActivityOnWindowFlagsChanged(ActivityInfo activityInfo, int windowFlags,
             int systemWindowFlags) {
+        // The callback is fired only when windowFlags are changed. To let VirtualDevice owner
+        // aware that the virtual display has a secure window on top.
+        if ((windowFlags & FLAG_SECURE) != 0) {
+            mSecureWindowCallback.onSecureWindowShown(mDisplayId, activityInfo.applicationInfo.uid);
+        }
+
         if (!canContainActivity(activityInfo, windowFlags, systemWindowFlags)) {
             mActivityBlockedCallback.onActivityBlocked(mDisplayId, activityInfo);
             return false;
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 5f337ab..cca3212 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -52,6 +52,7 @@
 import android.hardware.input.VirtualTouchEvent;
 import android.os.Binder;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
@@ -542,6 +543,7 @@
                             mParams.getDefaultActivityPolicy(),
                             createListenerAdapter(),
                             this::onActivityBlocked,
+                            this::onSecureWindowShown,
                             mAssociationInfo.getDeviceProfile());
             gwpc.registerRunningAppsChangedListener(/* listener= */ this);
             return gwpc;
@@ -591,6 +593,21 @@
                 mContext.getUser());
     }
 
+    private void onSecureWindowShown(int displayId, int uid) {
+        if (!mVirtualDisplayIds.contains(displayId)) {
+            return;
+        }
+
+        // If a virtual display isn't secure, the screen can't be captured. Show a warning toast
+        // if the secure window is shown on a non-secure virtual display.
+        DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+        Display display = displayManager.getDisplay(displayId);
+        if ((display.getFlags() & FLAG_SECURE) == 0) {
+            showToastWhereUidIsRunning(uid, com.android.internal.R.string.vdm_secure_window,
+                    Toast.LENGTH_LONG, mContext.getMainLooper());
+        }
+    }
+
     private ArraySet<UserHandle> getAllowedUserHandles() {
         ArraySet<UserHandle> result = new ArraySet<>();
         DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
@@ -650,14 +667,16 @@
     /**
      * Shows a toast on virtual displays owned by this device which have a given uid running.
      */
-    void showToastWhereUidIsRunning(int uid, @StringRes int resId, @Toast.Duration int duration) {
-        showToastWhereUidIsRunning(uid, mContext.getString(resId), duration);
+    void showToastWhereUidIsRunning(int uid, @StringRes int resId, @Toast.Duration int duration,
+            Looper looper) {
+        showToastWhereUidIsRunning(uid, mContext.getString(resId), duration, looper);
     }
 
     /**
      * Shows a toast on virtual displays owned by this device which have a given uid running.
      */
-    void showToastWhereUidIsRunning(int uid, String text, @Toast.Duration int duration) {
+    void showToastWhereUidIsRunning(int uid, String text, @Toast.Duration int duration,
+            Looper looper) {
         synchronized (mVirtualDeviceLock) {
             DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
             final int size = mWindowPolicyControllers.size();
@@ -666,7 +685,7 @@
                     int displayId = mWindowPolicyControllers.keyAt(i);
                     Display display = displayManager.getDisplay(displayId);
                     if (display != null && display.isValid()) {
-                        Toast.makeText(mContext.createDisplayContext(display), text,
+                        Toast.makeText(mContext.createDisplayContext(display), looper, text,
                                 duration).show();
                     }
                 }
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index b255188..cdddc1d 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -203,7 +203,7 @@
                         getContext().getString(
                             com.android.internal.R.string.vdm_camera_access_denied,
                             deviceName),
-                        Toast.LENGTH_LONG);
+                        Toast.LENGTH_LONG, Looper.myLooper());
             }
         }
     }
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 4a3f682a..5b1f740 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -19,12 +19,10 @@
 import static android.Manifest.permission.ACCESS_MTP;
 import static android.Manifest.permission.INSTALL_PACKAGES;
 import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
-import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.OP_LEGACY_STORAGE;
 import static android.app.AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.OP_REQUEST_INSTALL_PACKAGES;
-import static android.app.AppOpsManager.OP_WRITE_EXTERNAL_STORAGE;
 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
 import static android.app.PendingIntent.FLAG_ONE_SHOT;
@@ -4376,11 +4374,7 @@
                 }
             }
 
-            // Determine if caller is holding runtime permission
-            final boolean hasWrite = StorageManager.checkPermissionAndCheckOp(mContext, false, 0,
-                    uid, packageName, WRITE_EXTERNAL_STORAGE, OP_WRITE_EXTERNAL_STORAGE);
-
-            // We're only willing to give out installer access if they also hold
+            // We're only willing to give out installer access if they hold
             // runtime permission; this is a firm CDD requirement
             final boolean hasInstall = mIPackageManager.checkUidPermission(INSTALL_PACKAGES,
                     uid) == PERMISSION_GRANTED;
@@ -4396,7 +4390,7 @@
                     break;
                 }
             }
-            if ((hasInstall || hasInstallOp) && hasWrite) {
+            if (hasInstall || hasInstallOp) {
                 return StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER;
             }
             return StorageManager.MOUNT_MODE_EXTERNAL_DEFAULT;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 54a7811..297e6a2 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -459,6 +459,7 @@
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
 
 public class ActivityManagerService extends IActivityManager.Stub
         implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback, ActivityManagerGlobalLock {
@@ -4304,7 +4305,8 @@
                 null /* excludedPackages */, OP_NONE, null /* bOptions */, false /* ordered */,
                 false /* sticky */, MY_PID, SYSTEM_UID, Binder.getCallingUid(),
                 Binder.getCallingPid(), userId, false /* allowBackgroundActivityStarts */,
-                null /* backgroundActivityStartsToken */, broadcastAllowList);
+                null /* backgroundActivityStartsToken */,
+                broadcastAllowList, null /* filterExtrasForReceiver */);
     }
 
     private void cleanupDisabledPackageComponentsLocked(
@@ -13357,7 +13359,8 @@
                     BroadcastRecord r = new BroadcastRecord(queue, intent, 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 */);
+                            false /* only PRE_BOOT_COMPLETED should be exempt, no stickies */,
+                            null /* filterExtrasForReceiver */);
                     queue.enqueueParallelBroadcastLocked(r);
                     queue.scheduleBroadcastsLocked();
                 }
@@ -13620,7 +13623,8 @@
                 excludedPermissions, excludedPackages, appOp, bOptions, ordered, sticky, callingPid,
                 callingUid, realCallingUid, realCallingPid, userId,
                 false /* allowBackgroundActivityStarts */,
-                null /* tokenNeededForBackgroundActivityStarts */, null /* broadcastAllowList */);
+                null /* tokenNeededForBackgroundActivityStarts */,
+                null /* broadcastAllowList */, null /* filterExtrasForReceiver */);
     }
 
     @GuardedBy("this")
@@ -13633,7 +13637,8 @@
             int realCallingUid, int realCallingPid, int userId,
             boolean allowBackgroundActivityStarts,
             @Nullable IBinder backgroundActivityStartsToken,
-            @Nullable int[] broadcastAllowList) {
+            @Nullable int[] broadcastAllowList,
+            @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) {
         intent = new Intent(intent);
 
         final boolean callerInstantApp = isInstantApp(callerApp, callerPackage, callingUid);
@@ -14233,7 +14238,7 @@
                     requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
                     registeredReceivers, resultTo, resultCode, resultData, resultExtras, ordered,
                     sticky, false, userId, allowBackgroundActivityStarts,
-                    backgroundActivityStartsToken, timeoutExempt);
+                    backgroundActivityStartsToken, timeoutExempt, filterExtrasForReceiver);
             if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing parallel broadcast " + r);
             final boolean replaced = replacePending
                     && (queue.replaceParallelBroadcastLocked(r) != null);
@@ -14331,7 +14336,7 @@
                     requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
                     receivers, resultTo, resultCode, resultData, resultExtras,
                     ordered, sticky, false, userId, allowBackgroundActivityStarts,
-                    backgroundActivityStartsToken, timeoutExempt);
+                    backgroundActivityStartsToken, timeoutExempt, filterExtrasForReceiver);
 
             if (DEBUG_BROADCAST) Slog.v(TAG_BROADCAST, "Enqueueing ordered broadcast " + r);
 
@@ -14523,7 +14528,8 @@
                         resultTo, resultCode, resultData, resultExtras, requiredPermissions, null,
                         null, OP_NONE, bOptions, serialized, sticky, -1, uid, realCallingUid,
                         realCallingPid, userId, allowBackgroundActivityStarts,
-                        backgroundActivityStartsToken, broadcastAllowList);
+                        backgroundActivityStartsToken, broadcastAllowList,
+                        null /* filterExtrasForReceiver */);
             } finally {
                 Binder.restoreCallingIdentity(origId);
             }
@@ -16201,6 +16207,13 @@
         return mUserController.startUser(userId, /* foreground */ true, unlockListener);
     }
 
+    @Override
+    public boolean startUserInBackgroundOnSecondaryDisplay(int userId, int displayId) {
+        // Permission check done inside UserController.
+        return mUserController.startUserOnSecondaryDisplay(userId, displayId,
+                /* unlockListener= */ null);
+    }
+
     /**
      * Unlocks the given user.
      *
@@ -17058,7 +17071,9 @@
         public int broadcastIntent(Intent intent,
                 IIntentReceiver resultTo,
                 String[] requiredPermissions,
-                boolean serialized, int userId, int[] appIdAllowList, @Nullable Bundle bOptions) {
+                boolean serialized, int userId, int[] appIdAllowList,
+                @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
+                @Nullable Bundle bOptions) {
             synchronized (ActivityManagerService.this) {
                 intent = verifyBroadcastLocked(intent);
 
@@ -17074,7 +17089,8 @@
                             AppOpsManager.OP_NONE, bOptions /*options*/, serialized,
                             false /*sticky*/, callingPid, callingUid, callingUid, callingPid,
                             userId, false /*allowBackgroundStarts*/,
-                            null /*tokenNeededForBackgroundActivityStarts*/, appIdAllowList);
+                            null /*tokenNeededForBackgroundActivityStarts*/,
+                            appIdAllowList, filterExtrasForReceiver);
                 } finally {
                     Binder.restoreCallingIdentity(origId);
                 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 4bbfa3e..5cb25d3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -892,6 +892,7 @@
         }
     }
 
+    // TODO(b/239982558): might need to support --displayId as well
     private int runProfile(PrintWriter pw) throws RemoteException {
         final PrintWriter err = getErrPrintWriter();
         String profileFile = null;
@@ -2034,26 +2035,42 @@
     int runStartUser(PrintWriter pw) throws RemoteException {
         boolean wait = false;
         String opt;
+        int displayId = Display.INVALID_DISPLAY;
         while ((opt = getNextOption()) != null) {
-            if ("-w".equals(opt)) {
-                wait = true;
-            } else {
-                getErrPrintWriter().println("Error: unknown option: " + opt);
-                return -1;
+            switch(opt) {
+                case "-w":
+                    wait = true;
+                    break;
+                case "--display":
+                    displayId = getDisplayIdFromNextArg();
+                    break;
+                default:
+                    getErrPrintWriter().println("Error: unknown option: " + opt);
+                    return -1;
             }
         }
         int userId = Integer.parseInt(getNextArgRequired());
 
         final ProgressWaiter waiter = wait ? new ProgressWaiter() : null;
-        boolean success = mInterface.startUserInBackgroundWithListener(userId, waiter);
+
+        boolean success;
+        String displaySuffix;
+
+        if (displayId == Display.INVALID_DISPLAY) {
+            success = mInterface.startUserInBackgroundWithListener(userId, waiter);
+            displaySuffix = "";
+        } else {
+            success = mInterface.startUserInBackgroundOnSecondaryDisplay(userId, displayId);
+            displaySuffix = " on display " + displayId;
+        }
         if (wait && success) {
             success = waiter.waitForFinish(USER_OPERATION_TIMEOUT_MS);
         }
 
         if (success) {
-            pw.println("Success: user started");
+            pw.println("Success: user started" + displaySuffix);
         } else {
-            getErrPrintWriter().println("Error: could not start user");
+            getErrPrintWriter().println("Error: could not start user" + displaySuffix);
         }
         return 0;
     }
@@ -2506,6 +2523,14 @@
         }
     }
 
+    private int getDisplayIdFromNextArg() {
+        int displayId = Integer.parseInt(getNextArgRequired());
+        if (displayId < 0) {
+            throw new IllegalArgumentException("--display must be a non-negative integer");
+        }
+        return displayId;
+    }
+
     int runGetConfig(PrintWriter pw) throws RemoteException {
         int days = -1;
         int displayId = Display.DEFAULT_DISPLAY;
@@ -2524,10 +2549,7 @@
             } else if (opt.equals("--device")) {
                 inclDevice = true;
             } else if (opt.equals("--display")) {
-                displayId = Integer.parseInt(getNextArgRequired());
-                if (displayId < 0) {
-                    throw new IllegalArgumentException("--display must be a non-negative integer");
-                }
+                displayId = getDisplayIdFromNextArg();
             } else {
                 getErrPrintWriter().println("Error: Unknown option: " + opt);
                 return -1;
@@ -3714,10 +3736,13 @@
             pw.println("      execution of that user if it is currently stopped.");
             pw.println("  get-current-user");
             pw.println("      Returns id of the current foreground user.");
-            pw.println("  start-user [-w] <USER_ID>");
+            pw.println("  start-user [-w] [--display DISPLAY_ID] <USER_ID>");
             pw.println("      Start USER_ID in background if it is currently stopped;");
             pw.println("      use switch-user if you want to start the user in foreground.");
             pw.println("      -w: wait for start-user to complete and the user to be unlocked.");
+            pw.println("      --display <DISPLAY_ID>: allows the user to launch activities in the");
+            pw.println("        given display, when supported (typically on automotive builds");
+            pw.println("        wherethe vehicle has multiple displays)");
             pw.println("  unlock-user <USER_ID>");
             pw.println("      Unlock the given user.  This will only work if the user doesn't");
             pw.println("      have an LSKF (PIN/pattern/password).");
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index 9028eef..31d9f96 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -363,8 +363,8 @@
                     + ": " + r);
             mService.notifyPackageUse(r.intent.getComponent().getPackageName(),
                                       PackageManager.NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER);
-            thread.scheduleReceiver(new Intent(r.intent), r.curReceiver,
-                    null /* compatInfo (unused but need to keep method signature) */,
+            thread.scheduleReceiver(prepareReceiverIntent(r.intent, r.curFilteredExtras),
+                    r.curReceiver, null /* compatInfo (unused but need to keep method signature) */,
                     r.resultCode, r.resultData, r.resultExtras, r.ordered, r.userId,
                     app.mState.getReportedProcState());
             if (DEBUG_BROADCAST)  Slog.v(TAG_BROADCAST,
@@ -592,6 +592,7 @@
         r.curFilter = null;
         r.curReceiver = null;
         r.curApp = null;
+        r.curFilteredExtras = null;
         mPendingBroadcast = null;
 
         r.resultCode = resultCode;
@@ -943,6 +944,24 @@
             skip = true;
         }
 
+        // Filter packages in the intent extras, skipping delivery if none of the packages is
+        // visible to the receiver.
+        Bundle filteredExtras = null;
+        if (!skip && r.filterExtrasForReceiver != null) {
+            final Bundle extras = r.intent.getExtras();
+            if (extras != null) {
+                filteredExtras = r.filterExtrasForReceiver.apply(filter.receiverList.uid, extras);
+                if (filteredExtras == null) {
+                    if (DEBUG_BROADCAST) {
+                        Slog.v(TAG, "Skipping delivery to "
+                                + filter.receiverList.app
+                                + " : receiver is filtered by the package visibility");
+                    }
+                    skip = true;
+                }
+            }
+        }
+
         if (skip) {
             r.delivery[index] = BroadcastRecord.DELIVERY_SKIPPED;
             return;
@@ -1000,7 +1019,7 @@
                 maybeScheduleTempAllowlistLocked(filter.owningUid, r, r.options);
                 maybeReportBroadcastDispatchedEventLocked(r, filter.owningUid);
                 performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver,
-                        new Intent(r.intent), r.resultCode, r.resultData,
+                        prepareReceiverIntent(r.intent, filteredExtras), r.resultCode, r.resultData,
                         r.resultExtras, r.ordered, r.initialSticky, r.userId,
                         filter.receiverList.uid, r.callingUid,
                         r.dispatchTime - r.enqueueTime,
@@ -1167,6 +1186,15 @@
                 + ", uid=" + r.callingUid + ") to " + component.flattenToShortString();
     }
 
+    private static Intent prepareReceiverIntent(@NonNull Intent originalIntent,
+            @Nullable Bundle filteredExtras) {
+        final Intent intent = new Intent(originalIntent);
+        if (filteredExtras != null) {
+            intent.replaceExtras(filteredExtras);
+        }
+        return intent;
+    }
+
     final void processNextBroadcastLocked(boolean fromMsg, boolean skipOomAdj) {
         BroadcastRecord r;
 
@@ -1837,6 +1865,24 @@
             }
         }
 
+        // Filter packages in the intent extras, skipping delivery if none of the packages is
+        // visible to the receiver.
+        Bundle filteredExtras = null;
+        if (!skip && r.filterExtrasForReceiver != null) {
+            final Bundle extras = r.intent.getExtras();
+            if (extras != null) {
+                filteredExtras = r.filterExtrasForReceiver.apply(receiverUid, extras);
+                if (filteredExtras == null) {
+                    if (DEBUG_BROADCAST) {
+                        Slog.v(TAG, "Skipping delivery to "
+                                + info.activityInfo.packageName + " / " + receiverUid
+                                + " : receiver is filtered by the package visibility");
+                    }
+                    skip = true;
+                }
+            }
+        }
+
         if (skip) {
             if (DEBUG_BROADCAST)  Slog.v(TAG_BROADCAST,
                     "Skipping delivery of ordered [" + mQueueName + "] "
@@ -1855,6 +1901,7 @@
         r.state = BroadcastRecord.APP_RECEIVE;
         r.curComponent = component;
         r.curReceiver = info.activityInfo;
+        r.curFilteredExtras = filteredExtras;
         if (DEBUG_MU && r.callingUid > UserHandle.PER_USER_RANGE) {
             Slog.v(TAG_MU, "Updated broadcast record activity info for secondary user, "
                     + info.activityInfo + ", callingUid = " + r.callingUid + ", uid = "
@@ -1922,8 +1969,7 @@
                 info.activityInfo.applicationInfo, true,
                 r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND,
                 new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST, r.curComponent,
-                        r.intent.getAction(), (r.alarm ? HostingRecord.TRIGGER_TYPE_ALARM
-                        : HostingRecord.TRIGGER_TYPE_UNKNOWN)),
+                        r.intent.getAction(), getHostingRecordTriggerType(r)),
                 isActivityCapable ? ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE : ZYGOTE_POLICY_FLAG_EMPTY,
                 (r.intent.getFlags() & Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false);
         if (r.curApp == null) {
@@ -1946,6 +1992,16 @@
         mPendingBroadcastRecvIndex = recIdx;
     }
 
+    private String getHostingRecordTriggerType(BroadcastRecord r) {
+        if (r.alarm) {
+            return HostingRecord.TRIGGER_TYPE_ALARM;
+        } else if (r.pushMessage) {
+            return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE;
+        } else if (r.pushMessageOverQuota) {
+            return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA;
+        }
+        return HostingRecord.TRIGGER_TYPE_UNKNOWN;
+    }
 
     @Nullable
     private String getTargetPackage(BroadcastRecord r) {
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index ce4528b..18fbfde 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -55,6 +55,7 @@
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
 
 /**
  * An active intent broadcast.
@@ -71,6 +72,8 @@
     final boolean ordered;  // serialize the send to receivers?
     final boolean sticky;   // originated from existing sticky data?
     final boolean alarm;    // originated from an alarm triggering?
+    final boolean pushMessage; // originated from a push message?
+    final boolean pushMessageOverQuota; // originated from a push message which was over quota?
     final boolean initialSticky; // initial broadcast from register to sticky?
     final int userId;       // user id this broadcast was for
     final String resolvedType; // the resolved data type
@@ -114,6 +117,11 @@
     @Nullable
     final IBinder mBackgroundActivityStartsToken;
 
+    // Filter the intent extras by using the rules of the package visibility before broadcasting
+    // the intent to the receiver.
+    @Nullable
+    final BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver;
+
     static final int IDLE = 0;
     static final int APP_RECEIVE = 1;
     static final int CALL_IN_RECEIVE = 2;
@@ -134,6 +142,7 @@
     ProcessRecord curApp;       // hosting application of current receiver.
     ComponentName curComponent; // the receiver class that is currently running.
     ActivityInfo curReceiver;   // info about the receiver that is currently running.
+    Bundle curFilteredExtras;   // the bundle that has been filtered by the package visibility rules
 
     boolean mIsReceiverAppRunning; // Was the receiver's app already running.
 
@@ -227,6 +236,9 @@
                         pw.println(curReceiver.applicationInfo.sourceDir);
             }
         }
+        if (curFilteredExtras != null) {
+            pw.print(" filtered extras: "); pw.println(curFilteredExtras);
+        }
         if (state != IDLE) {
             String stateStr = " (?)";
             switch (state) {
@@ -273,7 +285,8 @@
             BroadcastOptions _options, List _receivers, IIntentReceiver _resultTo, int _resultCode,
             String _resultData, Bundle _resultExtras, boolean _serialized, boolean _sticky,
             boolean _initialSticky, int _userId, boolean allowBackgroundActivityStarts,
-            @Nullable IBinder backgroundActivityStartsToken, boolean timeoutExempt) {
+            @Nullable IBinder backgroundActivityStartsToken, boolean timeoutExempt,
+            @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver) {
         if (_intent == null) {
             throw new NullPointerException("Can't construct with a null intent");
         }
@@ -309,6 +322,9 @@
         mBackgroundActivityStartsToken = backgroundActivityStartsToken;
         this.timeoutExempt = timeoutExempt;
         alarm = options != null && options.isAlarmBroadcast();
+        pushMessage = options != null && options.isPushMessagingBroadcast();
+        pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast();
+        this.filterExtrasForReceiver = filterExtrasForReceiver;
     }
 
     /**
@@ -362,6 +378,9 @@
         mBackgroundActivityStartsToken = from.mBackgroundActivityStartsToken;
         timeoutExempt = from.timeoutExempt;
         alarm = from.alarm;
+        pushMessage = from.pushMessage;
+        pushMessageOverQuota = from.pushMessageOverQuota;
+        filterExtrasForReceiver = from.filterExtrasForReceiver;
     }
 
     /**
@@ -397,7 +416,7 @@
                 requiredPermissions, excludedPermissions, excludedPackages, appOp, options,
                 splitReceivers, resultTo, resultCode, resultData, resultExtras, ordered, sticky,
                 initialSticky, userId, allowBackgroundActivityStarts,
-                mBackgroundActivityStartsToken, timeoutExempt);
+                mBackgroundActivityStartsToken, timeoutExempt, filterExtrasForReceiver);
         split.enqueueTime = this.enqueueTime;
         split.enqueueRealTime = this.enqueueRealTime;
         split.enqueueClockTime = this.enqueueClockTime;
@@ -476,7 +495,8 @@
                     requiredPermissions, excludedPermissions, excludedPackages, appOp, options,
                     uid2receiverList.valueAt(i), null /* _resultTo */,
                     resultCode, resultData, resultExtras, ordered, sticky, initialSticky, userId,
-                    allowBackgroundActivityStarts, mBackgroundActivityStartsToken, timeoutExempt);
+                    allowBackgroundActivityStarts, mBackgroundActivityStartsToken, timeoutExempt,
+                    filterExtrasForReceiver);
             br.enqueueTime = this.enqueueTime;
             br.enqueueRealTime = this.enqueueRealTime;
             br.enqueueClockTime = this.enqueueClockTime;
diff --git a/services/core/java/com/android/server/am/HostingRecord.java b/services/core/java/com/android/server/am/HostingRecord.java
index efc2a27..2498f76 100644
--- a/services/core/java/com/android/server/am/HostingRecord.java
+++ b/services/core/java/com/android/server/am/HostingRecord.java
@@ -30,6 +30,8 @@
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SERVICE;
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SYSTEM;
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_TOP_ACTIVITY;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_PUSH_MESSAGE;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_PUSH_MESSAGE_OVER_QUOTA;
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_ALARM;
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_UNKNOWN;
 import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TYPE__UNKNOWN;
@@ -93,6 +95,8 @@
 
     public static final String TRIGGER_TYPE_UNKNOWN = "unknown";
     public static final String TRIGGER_TYPE_ALARM = "alarm";
+    public static final String TRIGGER_TYPE_PUSH_MESSAGE = "push_message";
+    public static final String TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA = "push_message_over_quota";
 
     @NonNull private final String mHostingType;
     private final String mHostingName;
@@ -308,6 +312,10 @@
         switch(triggerType) {
             case TRIGGER_TYPE_ALARM:
                 return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_ALARM;
+            case TRIGGER_TYPE_PUSH_MESSAGE:
+                return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_PUSH_MESSAGE;
+            case TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA:
+                return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_PUSH_MESSAGE_OVER_QUOTA;
             default:
                 return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_UNKNOWN;
         }
diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java
index 262436d..eb1fd3a 100644
--- a/services/core/java/com/android/server/am/ProcessStateRecord.java
+++ b/services/core/java/com/android/server/am/ProcessStateRecord.java
@@ -497,6 +497,7 @@
     @GuardedBy({"mService", "mProcLock"})
     void setCurAdj(int curAdj) {
         mCurAdj = curAdj;
+        mApp.getWindowProcessController().setCurrentAdj(curAdj);
     }
 
     @GuardedBy(anyOf = {"mService", "mProcLock"})
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 0c0ae7d..a1f3dae 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -16,9 +16,11 @@
 
 package com.android.server.am;
 
+import static android.Manifest.permission.CREATE_USERS;
 import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
 import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.Manifest.permission.MANAGE_USERS;
 import static android.app.ActivityManager.STOP_USER_ON_SWITCH_DEFAULT;
 import static android.app.ActivityManager.STOP_USER_ON_SWITCH_TRUE;
 import static android.app.ActivityManager.StopUserOnSwitch;
@@ -69,6 +71,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackagePartitions;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.os.BatteryStats;
 import android.os.Binder;
 import android.os.Bundle;
@@ -100,6 +103,7 @@
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.util.proto.ProtoOutputStream;
+import android.view.Display;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -107,6 +111,7 @@
 import com.android.internal.policy.IKeyguardDismissCallback;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.util.Preconditions;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.FactoryResetter;
 import com.android.server.FgThread;
@@ -1071,6 +1076,12 @@
                         Binder.getCallingPid(), UserHandle.USER_ALL);
             });
         }
+
+        // TODO(b/239982558): for now we're just updating the user's visibility, but most likely
+        // we'll need to remove this call and handle that as part of the user state workflow
+        // instead.
+        // TODO(b/240613396) also check if multi-display is supported
+        mInjector.getUserManagerInternal().assignUserToDisplay(userId, Display.INVALID_DISPLAY);
     }
 
     private void finishUserStopping(final int userId, final UserState uss,
@@ -1357,12 +1368,14 @@
         List<UserInfo> profilesToStart = new ArrayList<>(profiles.size());
         for (UserInfo user : profiles) {
             if ((user.flags & UserInfo.FLAG_INITIALIZED) == UserInfo.FLAG_INITIALIZED
-                    && user.id != currentUserId && !user.isQuietModeEnabled()) {
+                    && user.id != currentUserId
+                    && shouldStartWithParent(user)) {
                 profilesToStart.add(user);
             }
         }
         final int profilesToStartSize = profilesToStart.size();
         int i = 0;
+        // TODO(b/239982558): pass displayId
         for (; i < profilesToStartSize && i < (getMaxRunningUsers() - 1); ++i) {
             startUser(profilesToStart.get(i).id, /* foreground= */ false);
         }
@@ -1371,6 +1384,14 @@
         }
     }
 
+    private boolean shouldStartWithParent(UserInfo user) {
+        final UserProperties properties = mInjector.getUserManagerInternal()
+                .getUserProperties(user.id);
+        return (properties != null && properties.getStartWithParent())
+                && !user.isQuietModeEnabled();
+    }
+
+    // TODO(b/239982558): might need to infer the display id based on parent user
     /**
      * Starts a user only if it's a profile, with a more relaxed permission requirement:
      * {@link android.Manifest.permission#MANAGE_USERS} or
@@ -1399,7 +1420,8 @@
             return false;
         }
 
-        return startUserNoChecks(userId, /* foreground= */ false, /* unlockListener= */ null);
+        return startUserNoChecks(userId, Display.DEFAULT_DISPLAY, /* foreground= */ false,
+                /* unlockListener= */ null);
     }
 
     @VisibleForTesting
@@ -1445,26 +1467,70 @@
             @Nullable IProgressListener unlockListener) {
         checkCallingPermission(INTERACT_ACROSS_USERS_FULL, "startUser");
 
-        return startUserNoChecks(userId, foreground, unlockListener);
+        return startUserNoChecks(userId, Display.DEFAULT_DISPLAY, foreground, unlockListener);
     }
 
-    private boolean startUserNoChecks(final @UserIdInt int userId, final boolean foreground,
+    // TODO(b/239982558): add javadoc (need to wait until the intents / SystemService callbacks are
+    // defined
+    boolean startUserOnSecondaryDisplay(@UserIdInt int userId, int displayId,
+            @Nullable IProgressListener unlockListener) {
+        checkCallingHasOneOfThosePermissions("startUserOnSecondaryDisplay",
+                MANAGE_USERS, CREATE_USERS);
+
+        // DEFAULT_DISPLAY is used for "regular" start user operations
+        Preconditions.checkArgument(displayId != Display.DEFAULT_DISPLAY,
+                "Cannot use DEFAULT_DISPLAY");
+
+        try {
+            return startUserNoChecks(userId, displayId, /* foreground= */ false, unlockListener);
+        } catch (RuntimeException e) {
+            Slogf.w(TAG, "startUserOnSecondaryDisplay(%d, %d) failed: %s", userId, displayId, e);
+            return false;
+        }
+    }
+
+    private boolean startUserNoChecks(@UserIdInt int userId, int displayId, boolean foreground,
             @Nullable IProgressListener unlockListener) {
         TimingsTraceAndSlog t = new TimingsTraceAndSlog();
 
-        t.traceBegin("UserController.startUser-" + userId + "-" + (foreground ? "fg" : "bg"));
+        t.traceBegin("UserController.startUser-" + userId
+                + (displayId == Display.DEFAULT_DISPLAY ? "" : "-display-" + displayId)
+                + "-" + (foreground ? "fg" : "bg"));
         try {
-            return startUserInternal(userId, foreground, unlockListener, t);
+            return startUserInternal(userId, displayId, foreground, unlockListener, t);
         } finally {
             t.traceEnd();
         }
     }
 
-    private boolean startUserInternal(@UserIdInt int userId, boolean foreground,
+    private boolean startUserInternal(@UserIdInt int userId, int displayId, boolean foreground,
             @Nullable IProgressListener unlockListener, @NonNull TimingsTraceAndSlog t) {
         if (DEBUG_MU) {
-            Slogf.i(TAG, "Starting user %d%s", userId, foreground ? " in foreground" : "");
+            Slogf.i(TAG, "Starting user %d on display %d %s", userId, displayId,
+                    foreground ? " in foreground" : "");
         }
+
+        // TODO(b/239982558): move logic below to a different class (like DisplayAssignmentManager)
+        if (displayId != Display.DEFAULT_DISPLAY) {
+            // This is called by startUserOnSecondaryDisplay()
+            if (!UserManager.isUsersOnSecondaryDisplaysEnabled()) {
+                // TODO(b/239824814): add CTS test and/or unit test for all exceptional cases
+                throw new UnsupportedOperationException("Not supported by device");
+            }
+
+            // TODO(b/239982558): call DisplayManagerInternal to check if display is valid instead
+            Preconditions.checkArgument(displayId > 0, "Invalid display id (%d)", displayId);
+            Preconditions.checkArgument(userId != UserHandle.USER_SYSTEM, "Cannot start system user"
+                    + " on secondary display (%d)", displayId);
+            Preconditions.checkArgument(!foreground, "Cannot start user %d in foreground AND "
+                    + "on secondary display (%d)", userId, displayId);
+
+            // TODO(b/239982558): for now we're just updating the user's visibility, but most likely
+            // we'll need to remove this call and handle that as part of the user state workflow
+            // instead.
+            mInjector.getUserManagerInternal().assignUserToDisplay(userId, displayId);
+        }
+
         EventLog.writeEvent(EventLogTags.UC_START_USER_INTERNAL, userId);
 
         final int callingUid = Binder.getCallingUid();
@@ -1519,6 +1585,7 @@
                 return false;
             }
 
+            // TODO(b/239982558): might need something similar for bg users on secondary display
             if (foreground && isUserSwitchUiEnabled()) {
                 t.traceBegin("startFreezingScreen");
                 mInjector.getWindowManager().startFreezingScreen(
@@ -2568,15 +2635,24 @@
     }
 
     private void checkCallingPermission(String permission, String methodName) {
-        if (mInjector.checkCallingPermission(permission)
-                != PackageManager.PERMISSION_GRANTED) {
-            String msg = "Permission denial: " + methodName
-                    + "() from pid=" + Binder.getCallingPid()
-                    + ", uid=" + Binder.getCallingUid()
-                    + " requires " + permission;
-            Slogf.w(TAG, msg);
-            throw new SecurityException(msg);
+        checkCallingHasOneOfThosePermissions(methodName, permission);
+    }
+
+    private void checkCallingHasOneOfThosePermissions(String methodName, String...permissions) {
+        for (String permission : permissions) {
+            if (mInjector.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) {
+                return;
+            }
         }
+        String msg = "Permission denial: " + methodName
+                + "() from pid=" + Binder.getCallingPid()
+                + ", uid=" + Binder.getCallingUid()
+                + " requires "
+                + (permissions.length == 1
+                        ? permissions[0]
+                        : "one of " + Arrays.toString(permissions));
+        Slogf.w(TAG, msg);
+        throw new SecurityException(msg);
     }
 
     private void enforceShellRestriction(String restriction, @UserIdInt int userId) {
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index 854b818..1302e22 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -99,6 +99,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Service to manage game related features.
@@ -119,7 +120,7 @@
     static final int SET_GAME_STATE = 4;
     static final int CANCEL_GAME_LOADING_MODE = 5;
     static final int WRITE_GAME_MODE_INTERVENTION_LIST_FILE = 6;
-    static final int WRITE_SETTINGS_DELAY = 10 * 1000;  // 10 seconds
+    static final int WRITE_DELAY_MILLIS = 10 * 1000;  // 10 seconds
     static final int LOADING_BOOST_MAX_DURATION = 5 * 1000;  // 5 seconds
 
     private static final String PACKAGE_NAME_MSG_KEY = "packageName";
@@ -130,8 +131,8 @@
     private final Context mContext;
     private final Object mLock = new Object();
     private final Object mDeviceConfigLock = new Object();
-    private final Object mOverrideConfigLock = new Object();
-    private final Handler mHandler;
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    final Handler mHandler;
     private final PackageManager mPackageManager;
     private final UserManager mUserManager;
     private final PowerManagerInternal mPowerManagerInternal;
@@ -143,8 +144,6 @@
     private final ArrayMap<Integer, GameManagerSettings> mSettings = new ArrayMap<>();
     @GuardedBy("mDeviceConfigLock")
     private final ArrayMap<String, GamePackageConfiguration> mConfigs = new ArrayMap<>();
-    @GuardedBy("mOverrideConfigLock")
-    private final ArrayMap<String, GamePackageConfiguration> mOverrideConfigs = new ArrayMap<>();
     @Nullable
     private final GameServiceController mGameServiceController;
 
@@ -236,7 +235,7 @@
         final int userId = ActivityManager.getCurrentUser();
         String[] packageList = getInstalledGamePackageNames(userId);
         for (final String packageName : packageList) {
-            pw.println(getInterventionList(packageName));
+            pw.println(getInterventionList(packageName, userId));
         }
     }
 
@@ -258,14 +257,13 @@
                     if (userId < 0) {
                         Slog.wtf(TAG, "Attempt to write settings for invalid user: " + userId);
                         synchronized (mLock) {
-                            removeMessages(WRITE_SETTINGS, msg.obj);
+                            removeEqualMessages(WRITE_SETTINGS, msg.obj);
                         }
                         break;
                     }
-
                     Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
                     synchronized (mLock) {
-                        removeMessages(WRITE_SETTINGS, msg.obj);
+                        removeEqualMessages(WRITE_SETTINGS, msg.obj);
                         if (mSettings.containsKey(userId)) {
                             GameManagerSettings userSettings = mSettings.get(userId);
                             userSettings.writePersistentDataLocked();
@@ -279,8 +277,8 @@
                     if (userId < 0) {
                         Slog.wtf(TAG, "Attempt to write settings for invalid user: " + userId);
                         synchronized (mLock) {
-                            removeMessages(WRITE_SETTINGS, msg.obj);
-                            removeMessages(REMOVE_SETTINGS, msg.obj);
+                            removeEqualMessages(WRITE_SETTINGS, msg.obj);
+                            removeEqualMessages(REMOVE_SETTINGS, msg.obj);
                         }
                         break;
                     }
@@ -288,8 +286,8 @@
                     synchronized (mLock) {
                         // Since the user was removed, ignore previous write message
                         // and do write here.
-                        removeMessages(WRITE_SETTINGS, msg.obj);
-                        removeMessages(REMOVE_SETTINGS, msg.obj);
+                        removeEqualMessages(WRITE_SETTINGS, msg.obj);
+                        removeEqualMessages(REMOVE_SETTINGS, msg.obj);
                         if (mSettings.containsKey(userId)) {
                             final GameManagerSettings userSettings = mSettings.get(userId);
                             mSettings.remove(userId);
@@ -299,7 +297,7 @@
                     break;
                 }
                 case POPULATE_GAME_MODE_SETTINGS: {
-                    removeMessages(POPULATE_GAME_MODE_SETTINGS, msg.obj);
+                    removeEqualMessages(POPULATE_GAME_MODE_SETTINGS, msg.obj);
                     final int userId = (int) msg.obj;
                     final String[] packageNames = getInstalledGamePackageNames(userId);
                     updateConfigsForUser(userId, false /*checkGamePackage*/, packageNames);
@@ -345,13 +343,13 @@
                     if (userId < 0) {
                         Slog.wtf(TAG, "Attempt to write setting for invalid user: " + userId);
                         synchronized (mLock) {
-                            removeMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, null);
+                            removeEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, msg.obj);
                         }
                         break;
                     }
 
                     Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
-                    removeMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, null);
+                    removeEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, msg.obj);
                     writeGameModeInterventionsToFile(userId);
                     Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                     break;
@@ -446,8 +444,7 @@
     /**
      * GamePackageConfiguration manages all game mode config details for its associated package.
      */
-    @VisibleForTesting
-    public class GamePackageConfiguration {
+    public static class GamePackageConfiguration {
         public static final String TAG = "GameManagerService_GamePackageConfiguration";
 
         /**
@@ -499,12 +496,16 @@
         private boolean mAllowAngle;
         private boolean mAllowFpsOverride;
 
-        GamePackageConfiguration(String packageName, int userId) {
+        GamePackageConfiguration(String packageName) {
+            mPackageName = packageName;
+        }
+
+        GamePackageConfiguration(PackageManager packageManager, String packageName, int userId) {
             mPackageName = packageName;
             try {
-                final ApplicationInfo ai = mPackageManager.getApplicationInfoAsUser(packageName,
+                final ApplicationInfo ai = packageManager.getApplicationInfoAsUser(packageName,
                         PackageManager.GET_META_DATA, userId);
-                if (!parseInterventionFromXml(ai, packageName)) {
+                if (!parseInterventionFromXml(packageManager, ai, packageName)) {
                     if (ai.metaData != null) {
                         mPerfModeOptedIn = ai.metaData.getBoolean(METADATA_PERFORMANCE_MODE_ENABLE);
                         mBatteryModeOptedIn = ai.metaData.getBoolean(METADATA_BATTERY_MODE_ENABLE);
@@ -538,16 +539,17 @@
             }
         }
 
-        private boolean parseInterventionFromXml(ApplicationInfo ai, String packageName) {
+        private boolean parseInterventionFromXml(PackageManager packageManager, ApplicationInfo ai,
+                String packageName) {
             boolean xmlFound = false;
-            try (XmlResourceParser parser = ai.loadXmlMetaData(mPackageManager,
+            try (XmlResourceParser parser = ai.loadXmlMetaData(packageManager,
                     METADATA_GAME_MODE_CONFIG)) {
                 if (parser == null) {
                     Slog.v(TAG, "No " + METADATA_GAME_MODE_CONFIG
                             + " meta-data found for package " + mPackageName);
                 } else {
                     xmlFound = true;
-                    final Resources resources = mPackageManager.getResourcesForApplication(
+                    final Resources resources = packageManager.getResourcesForApplication(
                             packageName);
                     final AttributeSet attributeSet = Xml.asAttributeSet(parser);
                     int type;
@@ -596,7 +598,6 @@
          * GameModeConfiguration contains all the values for all the interventions associated with
          * a game mode.
          */
-        @VisibleForTesting
         public class GameModeConfiguration {
             public static final String TAG = "GameManagerService_GameModeConfiguration";
             public static final String MODE_KEY = "mode";
@@ -613,8 +614,8 @@
             private final @GameMode int mGameMode;
             private float mScaling = DEFAULT_SCALING;
             private String mFps = DEFAULT_FPS;
-            private final boolean mUseAngle;
-            private final int mLoadingBoostDuration;
+            private boolean mUseAngle;
+            private int mLoadingBoostDuration;
 
             GameModeConfiguration(int gameMode) {
                 mGameMode = gameMode;
@@ -657,11 +658,15 @@
                 return GameManagerService.getFpsInt(mFps);
             }
 
-            public boolean getUseAngle() {
+            synchronized String getFpsStr() {
+                return mFps;
+            }
+
+            public synchronized boolean getUseAngle() {
                 return mUseAngle;
             }
 
-            public int getLoadingBoostDuration() {
+            public synchronized int getLoadingBoostDuration() {
                 return mLoadingBoostDuration;
             }
 
@@ -673,6 +678,14 @@
                 mFps = fpsStr;
             }
 
+            public synchronized void setUseAngle(boolean useAngle) {
+                mUseAngle = useAngle;
+            }
+
+            public synchronized void setLoadingBoostDuration(int loadingBoostDuration) {
+                mLoadingBoostDuration = loadingBoostDuration;
+            }
+
             public boolean isActive() {
                 return (mGameMode == GameManager.GAME_MODE_STANDARD
                         || mGameMode == GameManager.GAME_MODE_PERFORMANCE
@@ -759,7 +772,7 @@
         }
 
         /**
-         * Inserts a new GameModeConfiguration
+         * Inserts a new GameModeConfiguration.
          */
         public void addModeConfig(GameModeConfiguration config) {
             if (config.isActive()) {
@@ -772,6 +785,15 @@
             }
         }
 
+        /**
+         * Removes the GameModeConfiguration.
+         */
+        public void removeModeConfig(int mode) {
+            synchronized (mModeConfigLock) {
+                mModeConfigs.remove(mode);
+            }
+        }
+
         public boolean isActive() {
             synchronized (mModeConfigLock) {
                 return mModeConfigs.size() > 0 || mBatteryModeOptedIn || mPerfModeOptedIn;
@@ -823,7 +845,8 @@
 
         @Override
         public void onUserStarting(@NonNull TargetUser user) {
-            mService.onUserStarting(user);
+            mService.onUserStarting(user,
+                    Environment.getDataSystemDeDirectory(user.getUserIdentifier()));
         }
 
         @Override
@@ -860,7 +883,10 @@
     }
 
     private @GameMode int[] getAvailableGameModesUnchecked(String packageName) {
-        final GamePackageConfiguration config = getConfig(packageName);
+        final GamePackageConfiguration config;
+        synchronized (mDeviceConfigLock) {
+            config = mConfigs.get(packageName);
+        }
         if (config == null) {
             return new int[]{};
         }
@@ -987,18 +1013,11 @@
             }
             GameManagerSettings userSettings = mSettings.get(userId);
             userSettings.setGameModeLocked(packageName, gameMode);
-            final Message msg = mHandler.obtainMessage(WRITE_SETTINGS);
-            msg.obj = userId;
-            if (!mHandler.hasEqualMessages(WRITE_SETTINGS, userId)) {
-                mHandler.sendMessageDelayed(msg, WRITE_SETTINGS_DELAY);
-            }
         }
         updateInterventions(packageName, gameMode, userId);
-        final Message msg = mHandler.obtainMessage(WRITE_GAME_MODE_INTERVENTION_LIST_FILE);
-        msg.obj = userId;
-        if (!mHandler.hasEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, userId)) {
-            mHandler.sendMessage(msg);
-        }
+        sendUserMessage(userId, WRITE_SETTINGS, "SET_GAME_MODE", WRITE_DELAY_MILLIS);
+        sendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE,
+                "SET_GAME_MODE", 0 /*delayMillis*/);
     }
 
     /**
@@ -1033,7 +1052,6 @@
      * the boost duration. If no configuration is available for the selected package or mode, the
      * default is returned.
      */
-    @VisibleForTesting
     public int getLoadingBoostDuration(String packageName, int userId)
             throws SecurityException {
         final int gameMode = getGameMode(packageName, userId);
@@ -1165,7 +1183,7 @@
     }
 
     float getResolutionScalingFactorInternal(String packageName, int gameMode, int userId) {
-        final GamePackageConfiguration packageConfig = getConfig(packageName);
+        final GamePackageConfiguration packageConfig = getConfig(packageName, userId);
         if (packageConfig == null) {
             return GamePackageConfiguration.GameModeConfiguration.DEFAULT_SCALING;
         }
@@ -1186,22 +1204,46 @@
         if (mGameServiceController != null) {
             mGameServiceController.onBootComplete();
         }
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (Intent.ACTION_SHUTDOWN.equals(intent.getAction())) {
+                    synchronized (mLock) {
+                        // Note that the max wait time of broadcast is 10s (see
+                        // {@ShutdownThread#MAX_BROADCAST_TIMEMAX_BROADCAST_TIME}) currently so
+                        // this can be optional only if we have message delay plus processing
+                        // time significant smaller to prevent data loss.
+                        for (Map.Entry<Integer, GameManagerSettings> entry : mSettings.entrySet()) {
+                            final int userId = entry.getKey();
+                            sendUserMessage(userId, WRITE_SETTINGS,
+                                    Intent.ACTION_SHUTDOWN, 0 /*delayMillis*/);
+                            sendUserMessage(userId,
+                                    WRITE_GAME_MODE_INTERVENTION_LIST_FILE, Intent.ACTION_SHUTDOWN,
+                                    0 /*delayMillis*/);
+                        }
+                    }
+                }
+            }
+        }, new IntentFilter(Intent.ACTION_SHUTDOWN));
     }
 
-    void onUserStarting(@NonNull TargetUser user) {
-        final int userId = user.getUserIdentifier();
+    private void sendUserMessage(int userId, int what, String eventForLog, int delayMillis) {
+        Message msg = mHandler.obtainMessage(what, userId);
+        if (!mHandler.sendMessageDelayed(msg, delayMillis)) {
+            Slog.e(TAG, "Failed to send user message " + what + " on " + eventForLog);
+        }
+    }
 
+    void onUserStarting(@NonNull TargetUser user, File settingDataDir) {
+        final int userId = user.getUserIdentifier();
         synchronized (mLock) {
             if (!mSettings.containsKey(userId)) {
-                GameManagerSettings userSettings =
-                        new GameManagerSettings(Environment.getDataSystemDeDirectory(userId));
+                GameManagerSettings userSettings = new GameManagerSettings(settingDataDir);
                 mSettings.put(userId, userSettings);
                 userSettings.readPersistentDataLocked();
             }
         }
-        final Message msg = mHandler.obtainMessage(POPULATE_GAME_MODE_SETTINGS);
-        msg.obj = userId;
-        mHandler.sendMessage(msg);
+        sendUserMessage(userId, POPULATE_GAME_MODE_SETTINGS, "ON_USER_STARTING", 0 /*delayMillis*/);
 
         if (mGameServiceController != null) {
             mGameServiceController.notifyUserStarted(user);
@@ -1221,9 +1263,7 @@
             if (!mSettings.containsKey(userId)) {
                 return;
             }
-            final Message msg = mHandler.obtainMessage(REMOVE_SETTINGS);
-            msg.obj = userId;
-            mHandler.sendMessage(msg);
+            sendUserMessage(userId, REMOVE_SETTINGS, "ON_USER_STOPPING", 0 /*delayMillis*/);
         }
 
         if (mGameServiceController != null) {
@@ -1237,15 +1277,14 @@
             synchronized (mLock) {
                 final int fromUserId = from.getUserIdentifier();
                 if (mSettings.containsKey(fromUserId)) {
-                    final Message msg = mHandler.obtainMessage(REMOVE_SETTINGS);
-                    msg.obj = fromUserId;
-                    mHandler.sendMessage(msg);
+                    sendUserMessage(fromUserId, REMOVE_SETTINGS, "ON_USER_SWITCHING",
+                            0 /*delayMillis*/);
                 }
             }
         }
-        final Message msg = mHandler.obtainMessage(POPULATE_GAME_MODE_SETTINGS);
-        msg.obj = toUserId;
-        mHandler.sendMessage(msg);
+
+        sendUserMessage(toUserId, POPULATE_GAME_MODE_SETTINGS, "ON_USER_SWITCHING",
+                0 /*delayMillis*/);
 
         if (mGameServiceController != null) {
             mGameServiceController.notifyNewForegroundUser(to);
@@ -1265,7 +1304,7 @@
         }
     }
 
-    private int modeToBitmask(@GameMode int gameMode) {
+    private static int modeToBitmask(@GameMode int gameMode) {
         return (1 << gameMode);
     }
 
@@ -1305,7 +1344,7 @@
             resetFps(packageName, userId);
             return;
         }
-        final GamePackageConfiguration packageConfig = getConfig(packageName);
+        final GamePackageConfiguration packageConfig = getConfig(packageName, userId);
         if (packageConfig == null) {
             Slog.v(TAG, "Package configuration not found for " + packageName);
             return;
@@ -1318,7 +1357,7 @@
     }
 
     /**
-     * Set the override Game Mode Configuration.
+     * Set the Game Mode Configuration override.
      * Update the config if exists, create one if not.
      */
     @VisibleForTesting
@@ -1326,95 +1365,86 @@
     public void setGameModeConfigOverride(String packageName, @UserIdInt int userId,
             @GameMode int gameMode, String fpsStr, String scaling) throws SecurityException {
         checkPermission(Manifest.permission.MANAGE_GAME_MODE);
+        // Adding game mode config override of the given package name
+        GamePackageConfiguration configOverride;
         synchronized (mLock) {
             if (!mSettings.containsKey(userId)) {
                 return;
             }
-        }
-        // Adding override game mode configuration of the given package name
-        GamePackageConfiguration overrideConfig;
-        synchronized (mOverrideConfigLock) {
-            // look for the existing override GamePackageConfiguration
-            overrideConfig = mOverrideConfigs.get(packageName);
-            if (overrideConfig == null) {
-                overrideConfig = new GamePackageConfiguration(packageName, userId);
-                mOverrideConfigs.put(packageName, overrideConfig);
+            final GameManagerSettings settings = mSettings.get(userId);
+            // look for the existing GamePackageConfiguration override
+            configOverride = settings.getConfigOverride(packageName);
+            if (configOverride == null) {
+                configOverride = new GamePackageConfiguration(mPackageManager, packageName, userId);
+                settings.setConfigOverride(packageName, configOverride);
             }
         }
         // modify GameModeConfiguration intervention settings
-        GamePackageConfiguration.GameModeConfiguration overrideModeConfig =
-                overrideConfig.getOrAddDefaultGameModeConfiguration(gameMode);
+        GamePackageConfiguration.GameModeConfiguration modeConfigOverride =
+                configOverride.getOrAddDefaultGameModeConfiguration(gameMode);
 
         if (fpsStr != null) {
-            overrideModeConfig.setFpsStr(fpsStr);
+            modeConfigOverride.setFpsStr(fpsStr);
         } else {
-            overrideModeConfig.setFpsStr(
+            modeConfigOverride.setFpsStr(
                     GamePackageConfiguration.GameModeConfiguration.DEFAULT_FPS);
         }
         if (scaling != null) {
-            overrideModeConfig.setScaling(Float.parseFloat(scaling));
+            modeConfigOverride.setScaling(Float.parseFloat(scaling));
         }
         Slog.i(TAG, "Package Name: " + packageName
-                + " FPS: " + String.valueOf(overrideModeConfig.getFps())
-                + " Scaling: " + overrideModeConfig.getScaling());
+                + " FPS: " + String.valueOf(modeConfigOverride.getFps())
+                + " Scaling: " + modeConfigOverride.getScaling());
         setGameMode(packageName, gameMode, userId);
     }
 
     /**
      * Reset the overridden gameModeConfiguration of the given mode.
-     * Remove the override config if game mode is not specified.
+     * Remove the config override if game mode is not specified.
      */
     @VisibleForTesting
     @RequiresPermission(Manifest.permission.MANAGE_GAME_MODE)
     public void resetGameModeConfigOverride(String packageName, @UserIdInt int userId,
             @GameMode int gameModeToReset) throws SecurityException {
         checkPermission(Manifest.permission.MANAGE_GAME_MODE);
-        synchronized (mLock) {
-            if (!mSettings.containsKey(userId)) {
-                return;
-            }
+        final GamePackageConfiguration deviceConfig;
+        synchronized (mDeviceConfigLock) {
+            deviceConfig = mConfigs.get(packageName);
         }
 
         // resets GamePackageConfiguration of a given packageName.
         // If a gameMode is specified, only reset the GameModeConfiguration of the gameMode.
-        if (gameModeToReset != -1) {
-            GamePackageConfiguration overrideConfig = null;
-            synchronized (mOverrideConfigLock) {
-                overrideConfig = mOverrideConfigs.get(packageName);
-            }
-
-            GamePackageConfiguration config = null;
-            synchronized (mDeviceConfigLock) {
-                config = mConfigs.get(packageName);
-            }
-
-            int[] modes = overrideConfig.getAvailableGameModes();
-
-            // First check if the mode to reset exists
-            boolean isGameModeExist = false;
-            for (int mode : modes) {
-                if (gameModeToReset == mode) {
-                    isGameModeExist = true;
-                }
-            }
-            if (!isGameModeExist) {
+        synchronized (mLock) {
+            if (!mSettings.containsKey(userId)) {
                 return;
             }
-
-            // If the game mode to reset is the only mode other than standard mode,
-            // the override config is removed.
-            if (modes.length <= 2) {
-                synchronized (mOverrideConfigLock) {
-                    mOverrideConfigs.remove(packageName);
+            final GameManagerSettings settings = mSettings.get(userId);
+            if (gameModeToReset != -1) {
+                final GamePackageConfiguration configOverride = settings.getConfigOverride(
+                        packageName);
+                if (configOverride == null) {
+                    return;
+                }
+                final int modesBitfield = configOverride.getAvailableGameModesBitfield();
+                if (!bitFieldContainsModeBitmask(modesBitfield, gameModeToReset)) {
+                    return;
+                }
+                // if the game mode to reset is the only mode other than standard mode or there
+                // is device config, the config override is removed.
+                if (Integer.bitCount(modesBitfield) <= 2 || deviceConfig == null) {
+                    settings.removeConfigOverride(packageName);
+                } else {
+                    final GamePackageConfiguration.GameModeConfiguration defaultModeConfig =
+                            deviceConfig.getGameModeConfiguration(gameModeToReset);
+                    // otherwise we reset the mode by copying the original config.
+                    if (defaultModeConfig == null) {
+                        configOverride.removeModeConfig(gameModeToReset);
+                    } else {
+                        configOverride.addModeConfig(defaultModeConfig);
+                    }
                 }
             } else {
-                // otherwise we reset the mode by copying the original config.
-                overrideConfig.addModeConfig(config.getGameModeConfiguration(gameModeToReset));
-            }
-        } else {
-            synchronized (mOverrideConfigLock) {
-                // remove override config if there is one
-                mOverrideConfigs.remove(packageName);
+                settings.removeConfigOverride(packageName);
             }
         }
 
@@ -1422,7 +1452,7 @@
         // If not, set the game mode to standard
         int gameMode = getGameMode(packageName, userId);
 
-        final GamePackageConfiguration config = getConfig(packageName);
+        final GamePackageConfiguration config = getConfig(packageName, userId);
         final int newGameMode = getNewGameMode(gameMode, config);
         if (gameMode != newGameMode) {
             setGameMode(packageName, GameManager.GAME_MODE_STANDARD, userId);
@@ -1460,8 +1490,8 @@
     /**
      * Returns the string listing all the interventions currently set to a game.
      */
-    public String getInterventionList(String packageName) {
-        final GamePackageConfiguration packageConfig = getConfig(packageName);
+    public String getInterventionList(String packageName, int userId) {
+        final GamePackageConfiguration packageConfig = getConfig(packageName, userId);
         final StringBuilder listStrSb = new StringBuilder();
         if (packageConfig == null) {
             listStrSb.append("\n No intervention found for package ")
@@ -1487,7 +1517,7 @@
             synchronized (mDeviceConfigLock) {
                 for (final String packageName : packageNames) {
                     final GamePackageConfiguration config =
-                            new GamePackageConfiguration(packageName, userId);
+                            new GamePackageConfiguration(mPackageManager, packageName, userId);
                     if (config.isActive()) {
                         if (DEBUG) {
                             Slog.i(TAG, "Adding config: " + config.toString());
@@ -1526,15 +1556,11 @@
                     updateInterventions(packageName, gameMode, userId);
                 }
             }
+            sendUserMessage(userId, WRITE_GAME_MODE_INTERVENTION_LIST_FILE,
+                    "UPDATE_CONFIGS_FOR_USERS", 0 /*delayMillis*/);
         } catch (Exception e) {
             Slog.e(TAG, "Failed to update configs for user " + userId + ": " + e);
         }
-
-        final Message msg = mHandler.obtainMessage(WRITE_GAME_MODE_INTERVENTION_LIST_FILE);
-        msg.obj = userId;
-        if (!mHandler.hasEqualMessages(WRITE_GAME_MODE_INTERVENTION_LIST_FILE, userId)) {
-            mHandler.sendMessage(msg);
-        }
     }
 
     /*
@@ -1556,7 +1582,7 @@
             final StringBuilder sb = new StringBuilder();
             final List<String> installedGamesList = getInstalledGamePackageNamesByAllUsers(userId);
             for (final String packageName : installedGamesList) {
-                GamePackageConfiguration packageConfig = getConfig(packageName);
+                GamePackageConfiguration packageConfig = getConfig(packageName, userId);
                 if (packageConfig == null) {
                     continue;
                 }
@@ -1634,11 +1660,12 @@
     /**
      * @hide
      */
-    @VisibleForTesting
-    public GamePackageConfiguration getConfig(String packageName) {
+    public GamePackageConfiguration getConfig(String packageName, int userId) {
         GamePackageConfiguration packageConfig = null;
-        synchronized (mOverrideConfigLock) {
-            packageConfig = mOverrideConfigs.get(packageName);
+        synchronized (mLock) {
+            if (mSettings.containsKey(userId)) {
+                packageConfig = mSettings.get(userId).getConfigOverride(packageName);
+            }
         }
         if (packageConfig == null) {
             synchronized (mDeviceConfigLock) {
@@ -1679,9 +1706,6 @@
                             break;
                         case ACTION_PACKAGE_REMOVED:
                             if (!intent.getBooleanExtra(EXTRA_REPLACING, false)) {
-                                synchronized (mOverrideConfigLock) {
-                                    mOverrideConfigs.remove(packageName);
-                                }
                                 synchronized (mDeviceConfigLock) {
                                     mConfigs.remove(packageName);
                                 }
@@ -1689,6 +1713,11 @@
                                     if (mSettings.containsKey(userId)) {
                                         mSettings.get(userId).removeGame(packageName);
                                     }
+                                    sendUserMessage(userId, WRITE_SETTINGS,
+                                            Intent.ACTION_PACKAGE_REMOVED, WRITE_DELAY_MILLIS);
+                                    sendUserMessage(userId,
+                                            WRITE_GAME_MODE_INTERVENTION_LIST_FILE,
+                                            Intent.ACTION_PACKAGE_REMOVED, WRITE_DELAY_MILLIS);
                                 }
                             }
                             break;
diff --git a/services/core/java/com/android/server/app/GameManagerSettings.java b/services/core/java/com/android/server/app/GameManagerSettings.java
index 1455a61..1162498 100644
--- a/services/core/java/com/android/server/app/GameManagerSettings.java
+++ b/services/core/java/com/android/server/app/GameManagerSettings.java
@@ -27,6 +27,8 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.XmlUtils;
+import com.android.server.app.GameManagerService.GamePackageConfiguration;
+import com.android.server.app.GameManagerService.GamePackageConfiguration.GameModeConfiguration;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -39,10 +41,11 @@
 
 /**
  * Persists all GameService related settings.
+ *
  * @hide
  */
 public class GameManagerSettings {
-
+    public static final String TAG = "GameManagerService_GameManagerSettings";
     // The XML file follows the below format:
     // <?xml>
     // <packages>
@@ -53,8 +56,14 @@
 
     private static final String TAG_PACKAGE = "package";
     private static final String TAG_PACKAGES = "packages";
+    private static final String TAG_GAME_MODE_CONFIG = "gameModeConfig";
+
     private static final String ATTR_NAME = "name";
     private static final String ATTR_GAME_MODE = "gameMode";
+    private static final String ATTR_SCALING = "scaling";
+    private static final String ATTR_FPS = "fps";
+    private static final String ATTR_USE_ANGLE = "useAngle";
+    private static final String ATTR_LOADING_BOOST_DURATION = "loadingBoost";
 
     private final File mSystemDir;
     @VisibleForTesting
@@ -62,6 +71,8 @@
 
     // PackageName -> GameMode
     private final ArrayMap<String, Integer> mGameModes = new ArrayMap<>();
+    // PackageName -> GamePackageConfiguration
+    private final ArrayMap<String, GamePackageConfiguration> mConfigOverrides = new ArrayMap<>();
 
     GameManagerSettings(File dataDir) {
         mSystemDir = new File(dataDir, "system");
@@ -74,7 +85,7 @@
     }
 
     /**
-     * Return the game mode of a given package.
+     * Returns the game mode of a given package.
      * This operation must be synced with an external lock.
      */
     int getGameModeLocked(String packageName) {
@@ -85,7 +96,7 @@
     }
 
     /**
-     * Set the game mode of a given package.
+     * Sets the game mode of a given package.
      * This operation must be synced with an external lock.
      */
     void setGameModeLocked(String packageName, int gameMode) {
@@ -93,15 +104,40 @@
     }
 
     /**
-     * Remove the game mode of a given package.
+     * Removes all game settings of a given package.
      * This operation must be synced with an external lock.
      */
     void removeGame(String packageName) {
         mGameModes.remove(packageName);
+        mConfigOverrides.remove(packageName);
     }
 
     /**
-     * Write all current game service settings into disk.
+     * Returns the game config override of a given package or null if absent.
+     * This operation must be synced with an external lock.
+     */
+    GamePackageConfiguration getConfigOverride(String packageName) {
+        return mConfigOverrides.get(packageName);
+    }
+
+    /**
+     * Sets the game config override of a given package.
+     * This operation must be synced with an external lock.
+     */
+    void setConfigOverride(String packageName, GamePackageConfiguration configOverride) {
+        mConfigOverrides.put(packageName, configOverride);
+    }
+
+    /**
+     * Removes the game mode config override of a given package.
+     * This operation must be synced with an external lock.
+     */
+    void removeConfigOverride(String packageName) {
+        mConfigOverrides.remove(packageName);
+    }
+
+    /**
+     * Writes all current game service settings into disk.
      * This operation must be synced with an external lock.
      */
     void writePersistentDataLocked() {
@@ -115,9 +151,11 @@
 
             serializer.startTag(null, TAG_PACKAGES);
             for (Map.Entry<String, Integer> entry : mGameModes.entrySet()) {
+                String packageName = entry.getKey();
                 serializer.startTag(null, TAG_PACKAGE);
-                serializer.attribute(null, ATTR_NAME, entry.getKey());
+                serializer.attribute(null, ATTR_NAME, packageName);
                 serializer.attributeInt(null, ATTR_GAME_MODE, entry.getValue());
+                writeGameModeConfigTags(serializer, mConfigOverrides.get(packageName));
                 serializer.endTag(null, TAG_PACKAGE);
             }
             serializer.endTag(null, TAG_PACKAGES);
@@ -133,20 +171,41 @@
             return;
         } catch (java.io.IOException e) {
             mSettingsFile.failWrite(fstr);
-            Slog.wtf(GameManagerService.TAG, "Unable to write game manager service settings, "
+            Slog.wtf(TAG, "Unable to write game manager service settings, "
                     + "current changes will be lost at reboot", e);
         }
     }
 
+    private void writeGameModeConfigTags(TypedXmlSerializer serializer,
+            GamePackageConfiguration config) throws IOException {
+        if (config == null) {
+            return;
+        }
+        final int[] gameModes = config.getAvailableGameModes();
+        for (final int mode : gameModes) {
+            final GameModeConfiguration modeConfig = config.getGameModeConfiguration(mode);
+            if (modeConfig != null) {
+                serializer.startTag(null, TAG_GAME_MODE_CONFIG);
+                serializer.attributeInt(null, ATTR_GAME_MODE, mode);
+                serializer.attributeBoolean(null, ATTR_USE_ANGLE, modeConfig.getUseAngle());
+                serializer.attribute(null, ATTR_FPS, modeConfig.getFpsStr());
+                serializer.attributeFloat(null, ATTR_SCALING, modeConfig.getScaling());
+                serializer.attributeInt(null, ATTR_LOADING_BOOST_DURATION,
+                        modeConfig.getLoadingBoostDuration());
+                serializer.endTag(null, TAG_GAME_MODE_CONFIG);
+            }
+        }
+    }
+
     /**
-     * Read game service settings from the disk.
+     * Reads game service settings from the disk.
      * This operation must be synced with an external lock.
      */
     boolean readPersistentDataLocked() {
         mGameModes.clear();
 
         if (!mSettingsFile.exists()) {
-            Slog.v(GameManagerService.TAG, "Settings file doesn't exists, skip reading");
+            Slog.v(TAG, "Settings file doesn't exist, skip reading");
             return false;
         }
 
@@ -160,8 +219,7 @@
                 // Do nothing
             }
             if (type != XmlPullParser.START_TAG) {
-                Slog.wtf(GameManagerService.TAG,
-                        "No start tag found in package manager settings");
+                Slog.wtf(TAG, "No start tag found in package manager settings");
                 return false;
             }
 
@@ -173,35 +231,107 @@
                 }
 
                 String tagName = parser.getName();
-                if (tagName.equals(TAG_PACKAGE)) {
+                if (type == XmlPullParser.START_TAG && TAG_PACKAGE.equals(tagName)) {
                     readPackage(parser);
                 } else {
-                    Slog.w(GameManagerService.TAG, "Unknown element: " + parser.getName());
                     XmlUtils.skipCurrentTag(parser);
+                    Slog.w(TAG, "Unknown element under packages tag: " + tagName + " with type: "
+                            + type);
                 }
             }
         } catch (XmlPullParserException | java.io.IOException e) {
-            Slog.wtf(GameManagerService.TAG, "Error reading package manager settings", e);
+            Slog.wtf(TAG, "Error reading package manager settings", e);
             return false;
         }
-
         return true;
     }
 
+    // this must be called on tag of type START_TAG.
     private void readPackage(TypedXmlPullParser parser) throws XmlPullParserException,
             IOException {
-        String name = null;
+        final String name = parser.getAttributeValue(null, ATTR_NAME);
+        if (name == null) {
+            Slog.wtf(TAG, "No package name found in package tag");
+            XmlUtils.skipCurrentTag(parser);
+            return;
+        }
         int gameMode = GameManager.GAME_MODE_UNSUPPORTED;
         try {
-            name = parser.getAttributeValue(null, ATTR_NAME);
             gameMode = parser.getAttributeInt(null, ATTR_GAME_MODE);
         } catch (XmlPullParserException e) {
-            Slog.wtf(GameManagerService.TAG, "Error reading game mode", e);
+            Slog.wtf(TAG, "Invalid game mode in package tag: "
+                    + parser.getAttributeValue(null, ATTR_GAME_MODE), e);
+            return;
         }
-        if (name != null) {
-            mGameModes.put(name, gameMode);
-        } else {
-            XmlUtils.skipCurrentTag(parser);
+        mGameModes.put(name, gameMode);
+        final int packageTagDepth = parser.getDepth();
+        int type;
+        final GamePackageConfiguration config = new GamePackageConfiguration(name);
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG
+                || parser.getDepth() > packageTagDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+            final String tagName = parser.getName();
+            if (type == XmlPullParser.START_TAG && TAG_GAME_MODE_CONFIG.equals(tagName)) {
+                readGameModeConfig(parser, config);
+            } else {
+                XmlUtils.skipCurrentTag(parser);
+                Slog.w(TAG, "Unknown element under package tag: " + tagName + " with type: "
+                        + type);
+            }
+        }
+        if (config.getAvailableGameModes().length > 1) {
+            mConfigOverrides.put(name, config);
+        }
+    }
+
+    // this must be called on tag of type START_TAG.
+    private void readGameModeConfig(TypedXmlPullParser parser, GamePackageConfiguration config) {
+        final int gameMode;
+        try {
+            gameMode = parser.getAttributeInt(null, ATTR_GAME_MODE);
+        } catch (XmlPullParserException e) {
+            Slog.wtf(TAG, "Invalid game mode value in config tag: " + parser.getAttributeValue(null,
+                    ATTR_GAME_MODE), e);
+            return;
+        }
+
+        final GameModeConfiguration modeConfig = config.getOrAddDefaultGameModeConfiguration(
+                gameMode);
+        try {
+            final float scaling = parser.getAttributeFloat(null, ATTR_SCALING);
+            modeConfig.setScaling(scaling);
+        } catch (XmlPullParserException e) {
+            final String rawScaling = parser.getAttributeValue(null, ATTR_SCALING);
+            if (rawScaling != null) {
+                Slog.wtf(TAG, "Invalid scaling value in config tag: " + rawScaling, e);
+            }
+        }
+
+        final String fps = parser.getAttributeValue(null, ATTR_FPS);
+        modeConfig.setFpsStr(fps != null ? fps : GameModeConfiguration.DEFAULT_FPS);
+
+        try {
+            final boolean useAngle = parser.getAttributeBoolean(null, ATTR_USE_ANGLE);
+            modeConfig.setUseAngle(useAngle);
+        } catch (XmlPullParserException e) {
+            final String rawUseAngle = parser.getAttributeValue(null, ATTR_USE_ANGLE);
+            if (rawUseAngle != null) {
+                Slog.wtf(TAG, "Invalid useAngle value in config tag: " + rawUseAngle, e);
+            }
+        }
+        try {
+            final int loadingBoostDuration = parser.getAttributeInt(null,
+                    ATTR_LOADING_BOOST_DURATION);
+            modeConfig.setLoadingBoostDuration(loadingBoostDuration);
+        } catch (XmlPullParserException e) {
+            final String rawLoadingBoost = parser.getAttributeValue(null,
+                    ATTR_LOADING_BOOST_DURATION);
+            if (rawLoadingBoost != null) {
+                Slog.wtf(TAG, "Invalid loading boost in config tag: " + rawLoadingBoost, e);
+            }
         }
     }
 }
diff --git a/services/core/java/com/android/server/app/GameManagerShellCommand.java b/services/core/java/com/android/server/app/GameManagerShellCommand.java
index 6e289b1..cdbffbe 100644
--- a/services/core/java/com/android/server/app/GameManagerShellCommand.java
+++ b/services/core/java/com/android/server/app/GameManagerShellCommand.java
@@ -81,7 +81,8 @@
         final GameManagerService gameManagerService = (GameManagerService)
                 ServiceManager.getService(Context.GAME_SERVICE);
 
-        final String listStr = gameManagerService.getInterventionList(packageName);
+        final String listStr = gameManagerService.getInterventionList(packageName,
+                ActivityManager.getCurrentUser());
 
         if (listStr == null) {
             pw.println("No interventions found for " + packageName);
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index a5bcb05..248e35e 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -63,7 +63,6 @@
 import static android.app.AppOpsManager.UID_STATE_MAX_LAST_NON_RESTRICTED;
 import static android.app.AppOpsManager.UID_STATE_PERSISTENT;
 import static android.app.AppOpsManager.UID_STATE_TOP;
-import static android.app.AppOpsManager.WATCH_FOREGROUND_CHANGES;
 import static android.app.AppOpsManager._NUM_OP;
 import static android.app.AppOpsManager.extractFlagsFromKey;
 import static android.app.AppOpsManager.extractUidStateFromKey;
@@ -563,6 +562,7 @@
         public ArrayMap<String, Ops> pkgOps;
 
         // true indicates there is an interested observer, false there isn't but it has such an op
+        //TODO: Move foregroundOps and hasForegroundWatchers into the AppOpsServiceInterface.
         public SparseBooleanArray foregroundOps;
         public boolean hasForegroundWatchers;
 
@@ -658,48 +658,24 @@
             return mode;
         }
 
-        private void evalForegroundWatchers(int op, SparseArray<ArraySet<ModeCallback>> watchers,
-                SparseBooleanArray which) {
-            boolean curValue = which.get(op, false);
-            ArraySet<ModeCallback> callbacks = watchers.get(op);
-            if (callbacks != null) {
-                for (int cbi = callbacks.size() - 1; !curValue && cbi >= 0; cbi--) {
-                    if ((callbacks.valueAt(cbi).mFlags
-                            & AppOpsManager.WATCH_FOREGROUND_CHANGES) != 0) {
-                        hasForegroundWatchers = true;
-                        curValue = true;
-                    }
-                }
-            }
-            which.put(op, curValue);
-        }
-
-        public void evalForegroundOps(SparseArray<ArraySet<ModeCallback>> watchers) {
-            SparseBooleanArray which = null;
-            hasForegroundWatchers = false;
-            final SparseIntArray opModes = getNonDefaultUidModes();
-            for (int i = opModes.size() - 1; i >= 0; i--) {
-                if (opModes.valueAt(i) == AppOpsManager.MODE_FOREGROUND) {
-                    if (which == null) {
-                        which = new SparseBooleanArray();
-                    }
-                    evalForegroundWatchers(opModes.keyAt(i), watchers, which);
-                }
-            }
+        public void evalForegroundOps() {
+            foregroundOps = null;
+            foregroundOps = mAppOpsServiceInterface.evalForegroundUidOps(uid, foregroundOps);
             if (pkgOps != null) {
                 for (int i = pkgOps.size() - 1; i >= 0; i--) {
-                    Ops ops = pkgOps.valueAt(i);
-                    for (int j = ops.size() - 1; j >= 0; j--) {
-                        if (ops.valueAt(j).getMode() == AppOpsManager.MODE_FOREGROUND) {
-                            if (which == null) {
-                                which = new SparseBooleanArray();
-                            }
-                            evalForegroundWatchers(ops.keyAt(j), watchers, which);
-                        }
+                    foregroundOps = mAppOpsServiceInterface
+                            .evalForegroundPackageOps(pkgOps.valueAt(i).packageName, foregroundOps);
+                }
+            }
+            hasForegroundWatchers = false;
+            if (foregroundOps != null) {
+                for (int i = 0;  i < foregroundOps.size(); i++) {
+                    if (foregroundOps.valueAt(i)) {
+                        hasForegroundWatchers = true;
+                        break;
                     }
                 }
             }
-            foregroundOps = which;
         }
     }
 
@@ -1562,33 +1538,24 @@
         }
     }
 
-    final SparseArray<ArraySet<ModeCallback>> mOpModeWatchers = new SparseArray<>();
-    final ArrayMap<String, ArraySet<ModeCallback>> mPackageModeWatchers = new ArrayMap<>();
     final ArrayMap<IBinder, ModeCallback> mModeWatchers = new ArrayMap<>();
     final ArrayMap<IBinder, SparseArray<ActiveCallback>> mActiveWatchers = new ArrayMap<>();
     final ArrayMap<IBinder, SparseArray<StartedCallback>> mStartedWatchers = new ArrayMap<>();
     final ArrayMap<IBinder, SparseArray<NotedCallback>> mNotedWatchers = new ArrayMap<>();
     final AudioRestrictionManager mAudioRestrictionManager = new AudioRestrictionManager();
 
-    final class ModeCallback implements DeathRecipient {
+    final class ModeCallback extends OnOpModeChangedListener implements DeathRecipient  {
         /** If mWatchedOpCode==ALL_OPS notify for ops affected by the switch-op */
         public static final int ALL_OPS = -2;
 
-        final IAppOpsCallback mCallback;
-        final int mWatchingUid;
-        final int mFlags;
-        final int mWatchedOpCode;
-        final int mCallingUid;
-        final int mCallingPid;
+        // Need to keep this only because stopWatchingMode needs an IAppOpsCallback.
+        // Otherwise we can just use the IBinder object.
+        private final IAppOpsCallback mCallback;
 
-        ModeCallback(IAppOpsCallback callback, int watchingUid, int flags, int watchedOp,
+        ModeCallback(IAppOpsCallback callback, int watchingUid, int flags, int watchedOpCode,
                 int callingUid, int callingPid) {
-            mCallback = callback;
-            mWatchingUid = watchingUid;
-            mFlags = flags;
-            mWatchedOpCode = watchedOp;
-            mCallingUid = callingUid;
-            mCallingPid = callingPid;
+            super(watchingUid, flags, watchedOpCode, callingUid, callingPid);
+            this.mCallback = callback;
             try {
                 mCallback.asBinder().linkToDeath(this, 0);
             } catch (RemoteException e) {
@@ -1596,20 +1563,16 @@
             }
         }
 
-        public boolean isWatchingUid(int uid) {
-            return uid == UID_ANY || mWatchingUid < 0 || mWatchingUid == uid;
-        }
-
         @Override
         public String toString() {
             StringBuilder sb = new StringBuilder(128);
             sb.append("ModeCallback{");
             sb.append(Integer.toHexString(System.identityHashCode(this)));
             sb.append(" watchinguid=");
-            UserHandle.formatUid(sb, mWatchingUid);
+            UserHandle.formatUid(sb, getWatchingUid());
             sb.append(" flags=0x");
-            sb.append(Integer.toHexString(mFlags));
-            switch (mWatchedOpCode) {
+            sb.append(Integer.toHexString(getFlags()));
+            switch (getWatchedOpCode()) {
                 case OP_NONE:
                     break;
                 case ALL_OPS:
@@ -1617,13 +1580,13 @@
                     break;
                 default:
                     sb.append(" op=");
-                    sb.append(opToName(mWatchedOpCode));
+                    sb.append(opToName(getWatchedOpCode()));
                     break;
             }
             sb.append(" from uid=");
-            UserHandle.formatUid(sb, mCallingUid);
+            UserHandle.formatUid(sb, getCallingUid());
             sb.append(" pid=");
-            sb.append(mCallingPid);
+            sb.append(getCallingPid());
             sb.append('}');
             return sb.toString();
         }
@@ -1636,6 +1599,11 @@
         public void binderDied() {
             stopWatchingMode(mCallback);
         }
+
+        @Override
+        public void onOpModeChanged(int op, int uid, String packageName) throws RemoteException {
+            mCallback.opChanged(op, uid, packageName);
+        }
     }
 
     final class ActiveCallback implements DeathRecipient {
@@ -1804,7 +1772,14 @@
 
     public AppOpsService(File storagePath, Handler handler, Context context) {
         mContext = context;
-        mAppOpsServiceInterface = new LegacyAppOpsServiceInterfaceImpl(this, this);
+
+        for (int switchedCode = 0; switchedCode < _NUM_OP; switchedCode++) {
+            int switchCode = AppOpsManager.opToSwitch(switchedCode);
+            mSwitchedOps.put(switchCode,
+                    ArrayUtils.appendInt(mSwitchedOps.get(switchCode), switchedCode));
+        }
+        mAppOpsServiceInterface =
+                new LegacyAppOpsServiceInterfaceImpl(this, this, handler, context, mSwitchedOps);
 
         LockGuard.installLock(this, LockGuard.INDEX_APP_OPS);
         mFile = new AtomicFile(storagePath, "appops");
@@ -1818,12 +1793,6 @@
         mHandler = handler;
         mConstants = new Constants(mHandler);
         readState();
-
-        for (int switchedCode = 0; switchedCode < _NUM_OP; switchedCode++) {
-            int switchCode = AppOpsManager.opToSwitch(switchedCode);
-            mSwitchedOps.put(switchCode,
-                    ArrayUtils.appendInt(mSwitchedOps.get(switchCode), switchedCode));
-        }
     }
 
     public void publish() {
@@ -1982,20 +1951,20 @@
                 final String[] changedPkgs = intent.getStringArrayExtra(
                         Intent.EXTRA_CHANGED_PACKAGE_LIST);
                 for (int code : OPS_RESTRICTED_ON_SUSPEND) {
-                    ArraySet<ModeCallback> callbacks;
+                    ArraySet<OnOpModeChangedListener> onModeChangedListeners;
                     synchronized (AppOpsService.this) {
-                        callbacks = mOpModeWatchers.get(code);
-                        if (callbacks == null) {
+                        onModeChangedListeners =
+                                mAppOpsServiceInterface.getOpModeChangedListeners(code);
+                        if (onModeChangedListeners == null) {
                             continue;
                         }
-                        callbacks = new ArraySet<>(callbacks);
                     }
                     for (int i = 0; i < changedUids.length; i++) {
                         final int changedUid = changedUids[i];
                         final String changedPkg = changedPkgs[i];
                         // We trust packagemanager to insert matching uid and packageNames in the
                         // extras
-                        notifyOpChanged(callbacks, code, changedUid, changedPkg);
+                        notifyOpChanged(onModeChangedListeners, code, changedUid, changedPkg);
                     }
                 }
             }
@@ -2596,7 +2565,7 @@
             if (!uidState.setUidMode(code, mode)) {
                 return;
             }
-            uidState.evalForegroundOps(mOpModeWatchers);
+            uidState.evalForegroundOps();
             if (mode != MODE_ERRORED && mode != previousMode) {
                 updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid);
             }
@@ -2615,78 +2584,10 @@
      */
     private void notifyOpChangedForAllPkgsInUid(int code, int uid, boolean onlyForeground,
             @Nullable IAppOpsCallback callbackToIgnore) {
-        String[] uidPackageNames = getPackagesForUid(uid);
-        ArrayMap<ModeCallback, ArraySet<String>> callbackSpecs = null;
-
-        synchronized (this) {
-            ArraySet<ModeCallback> callbacks = mOpModeWatchers.get(code);
-            if (callbacks != null) {
-                final int callbackCount = callbacks.size();
-                for (int i = 0; i < callbackCount; i++) {
-                    ModeCallback callback = callbacks.valueAt(i);
-                    if (onlyForeground && (callback.mFlags & WATCH_FOREGROUND_CHANGES) == 0) {
-                        continue;
-                    }
-
-                    ArraySet<String> changedPackages = new ArraySet<>();
-                    Collections.addAll(changedPackages, uidPackageNames);
-                    if (callbackSpecs == null) {
-                        callbackSpecs = new ArrayMap<>();
-                    }
-                    callbackSpecs.put(callback, changedPackages);
-                }
-            }
-
-            for (String uidPackageName : uidPackageNames) {
-                callbacks = mPackageModeWatchers.get(uidPackageName);
-                if (callbacks != null) {
-                    if (callbackSpecs == null) {
-                        callbackSpecs = new ArrayMap<>();
-                    }
-                    final int callbackCount = callbacks.size();
-                    for (int i = 0; i < callbackCount; i++) {
-                        ModeCallback callback = callbacks.valueAt(i);
-                        if (onlyForeground && (callback.mFlags & WATCH_FOREGROUND_CHANGES) == 0) {
-                            continue;
-                        }
-
-                        ArraySet<String> changedPackages = callbackSpecs.get(callback);
-                        if (changedPackages == null) {
-                            changedPackages = new ArraySet<>();
-                            callbackSpecs.put(callback, changedPackages);
-                        }
-                        changedPackages.add(uidPackageName);
-                    }
-                }
-            }
-
-            if (callbackSpecs != null && callbackToIgnore != null) {
-                callbackSpecs.remove(mModeWatchers.get(callbackToIgnore.asBinder()));
-            }
-        }
-
-        if (callbackSpecs == null) {
-            return;
-        }
-
-        for (int i = 0; i < callbackSpecs.size(); i++) {
-            final ModeCallback callback = callbackSpecs.keyAt(i);
-            final ArraySet<String> reportedPackageNames = callbackSpecs.valueAt(i);
-            if (reportedPackageNames == null) {
-                mHandler.sendMessage(PooledLambda.obtainMessage(
-                        AppOpsService::notifyOpChanged,
-                        this, callback, code, uid, (String) null));
-
-            } else {
-                final int reportedPackageCount = reportedPackageNames.size();
-                for (int j = 0; j < reportedPackageCount; j++) {
-                    final String reportedPackageName = reportedPackageNames.valueAt(j);
-                    mHandler.sendMessage(PooledLambda.obtainMessage(
-                            AppOpsService::notifyOpChanged,
-                            this, callback, code, uid, reportedPackageName));
-                }
-            }
-        }
+        ModeCallback listenerToIgnore = callbackToIgnore != null
+                ? mModeWatchers.get(callbackToIgnore.asBinder()) : null;
+        mAppOpsServiceInterface.notifyOpChangedForAllPkgsInUid(code, uid, onlyForeground,
+                listenerToIgnore);
     }
 
     private void updatePermissionRevokedCompat(int uid, int switchCode, int mode) {
@@ -2810,7 +2711,7 @@
             return;
         }
 
-        ArraySet<ModeCallback> repCbs = null;
+        ArraySet<OnOpModeChangedListener> repCbs = null;
         code = AppOpsManager.opToSwitch(code);
 
         PackageVerificationResult pvr;
@@ -2831,16 +2732,17 @@
                     op.setMode(mode);
 
                     if (uidState != null) {
-                        uidState.evalForegroundOps(mOpModeWatchers);
+                        uidState.evalForegroundOps();
                     }
-                    ArraySet<ModeCallback> cbs = mOpModeWatchers.get(code);
+                    ArraySet<OnOpModeChangedListener> cbs =
+                            mAppOpsServiceInterface.getOpModeChangedListeners(code);
                     if (cbs != null) {
                         if (repCbs == null) {
                             repCbs = new ArraySet<>();
                         }
                         repCbs.addAll(cbs);
                     }
-                    cbs = mPackageModeWatchers.get(packageName);
+                    cbs = mAppOpsServiceInterface.getPackageModeChangedListeners(packageName);
                     if (cbs != null) {
                         if (repCbs == null) {
                             repCbs = new ArraySet<>();
@@ -2871,47 +2773,17 @@
         notifyOpChangedSync(code, uid, packageName, mode, previousMode);
     }
 
-    private void notifyOpChanged(ArraySet<ModeCallback> callbacks, int code,
+    private void notifyOpChanged(ArraySet<OnOpModeChangedListener> callbacks, int code,
             int uid, String packageName) {
         for (int i = 0; i < callbacks.size(); i++) {
-            final ModeCallback callback = callbacks.valueAt(i);
+            final OnOpModeChangedListener callback = callbacks.valueAt(i);
             notifyOpChanged(callback, code, uid, packageName);
         }
     }
 
-    private void notifyOpChanged(ModeCallback callback, int code,
+    private void notifyOpChanged(OnOpModeChangedListener callback, int code,
             int uid, String packageName) {
-        if (uid != UID_ANY && callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) {
-            return;
-        }
-
-        // See CALL_BACK_ON_CHANGED_LISTENER_WITH_SWITCHED_OP_CHANGE
-        int[] switchedCodes;
-        if (callback.mWatchedOpCode == ALL_OPS) {
-            switchedCodes = mSwitchedOps.get(code);
-        } else if (callback.mWatchedOpCode == OP_NONE) {
-            switchedCodes = new int[]{code};
-        } else {
-            switchedCodes = new int[]{callback.mWatchedOpCode};
-        }
-
-        for (int switchedCode : switchedCodes) {
-            // There are features watching for mode changes such as window manager
-            // and location manager which are in our process. The callbacks in these
-            // features may require permissions our remote caller does not have.
-            final long identity = Binder.clearCallingIdentity();
-            try {
-                if (shouldIgnoreCallback(switchedCode, callback.mCallingPid,
-                        callback.mCallingUid)) {
-                    continue;
-                }
-                callback.mCallback.opChanged(switchedCode, uid, packageName);
-            } catch (RemoteException e) {
-                /* ignore */
-            } finally {
-                Binder.restoreCallingIdentity(identity);
-            }
-        }
+        mAppOpsServiceInterface.notifyOpChanged(callback, code, uid, packageName);
     }
 
     private static ArrayList<ChangeRec> addChange(ArrayList<ChangeRec> reports,
@@ -2936,9 +2808,10 @@
         return reports;
     }
 
-    private static HashMap<ModeCallback, ArrayList<ChangeRec>> addCallbacks(
-            HashMap<ModeCallback, ArrayList<ChangeRec>> callbacks,
-            int op, int uid, String packageName, int previousMode, ArraySet<ModeCallback> cbs) {
+    private static HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> addCallbacks(
+            HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> callbacks,
+            int op, int uid, String packageName, int previousMode,
+            ArraySet<OnOpModeChangedListener> cbs) {
         if (cbs == null) {
             return callbacks;
         }
@@ -2947,7 +2820,7 @@
         }
         final int N = cbs.size();
         for (int i=0; i<N; i++) {
-            ModeCallback cb = cbs.valueAt(i);
+            OnOpModeChangedListener cb = cbs.valueAt(i);
             ArrayList<ChangeRec> reports = callbacks.get(cb);
             ArrayList<ChangeRec> changed = addChange(reports, op, uid, packageName, previousMode);
             if (changed != reports) {
@@ -2990,7 +2863,7 @@
 
         enforceManageAppOpsModes(callingPid, callingUid, reqUid);
 
-        HashMap<ModeCallback, ArrayList<ChangeRec>> callbacks = null;
+        HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> callbacks = null;
         ArrayList<ChangeRec> allChanges = new ArrayList<>();
         synchronized (this) {
             boolean changed = false;
@@ -3007,9 +2880,11 @@
                             uidState.setUidMode(code, AppOpsManager.opToDefaultMode(code));
                             for (String packageName : getPackagesForUid(uidState.uid)) {
                                 callbacks = addCallbacks(callbacks, code, uidState.uid, packageName,
-                                        previousMode, mOpModeWatchers.get(code));
+                                        previousMode,
+                                        mAppOpsServiceInterface.getOpModeChangedListeners(code));
                                 callbacks = addCallbacks(callbacks, code, uidState.uid, packageName,
-                                        previousMode, mPackageModeWatchers.get(packageName));
+                                        previousMode, mAppOpsServiceInterface
+                                                .getPackageModeChangedListeners(packageName));
 
                                 allChanges = addChange(allChanges, code, uidState.uid,
                                         packageName, previousMode);
@@ -3053,9 +2928,11 @@
                             uidChanged = true;
                             final int uid = curOp.uidState.uid;
                             callbacks = addCallbacks(callbacks, curOp.op, uid, packageName,
-                                    previousMode, mOpModeWatchers.get(curOp.op));
+                                    previousMode,
+                                    mAppOpsServiceInterface.getOpModeChangedListeners(curOp.op));
                             callbacks = addCallbacks(callbacks, curOp.op, uid, packageName,
-                                    previousMode, mPackageModeWatchers.get(packageName));
+                                    previousMode, mAppOpsServiceInterface
+                                            .getPackageModeChangedListeners(packageName));
 
                             allChanges = addChange(allChanges, curOp.op, uid, packageName,
                                     previousMode);
@@ -3075,7 +2952,7 @@
                     mUidStates.remove(uidState.uid);
                 }
                 if (uidChanged) {
-                    uidState.evalForegroundOps(mOpModeWatchers);
+                    uidState.evalForegroundOps();
                 }
             }
 
@@ -3084,8 +2961,9 @@
             }
         }
         if (callbacks != null) {
-            for (Map.Entry<ModeCallback, ArrayList<ChangeRec>> ent : callbacks.entrySet()) {
-                ModeCallback cb = ent.getKey();
+            for (Map.Entry<OnOpModeChangedListener, ArrayList<ChangeRec>> ent
+                    : callbacks.entrySet()) {
+                OnOpModeChangedListener cb = ent.getKey();
                 ArrayList<ChangeRec> reports = ent.getValue();
                 for (int i=0; i<reports.size(); i++) {
                     ChangeRec rep = reports.get(i);
@@ -3121,7 +2999,7 @@
         for (int uidi = mUidStates.size() - 1; uidi >= 0; uidi--) {
             final UidState uidState = mUidStates.valueAt(uidi);
             if (uidState.foregroundOps != null) {
-                uidState.evalForegroundOps(mOpModeWatchers);
+                uidState.evalForegroundOps();
             }
         }
     }
@@ -3169,20 +3047,10 @@
                 mModeWatchers.put(callback.asBinder(), cb);
             }
             if (switchOp != AppOpsManager.OP_NONE) {
-                ArraySet<ModeCallback> cbs = mOpModeWatchers.get(switchOp);
-                if (cbs == null) {
-                    cbs = new ArraySet<>();
-                    mOpModeWatchers.put(switchOp, cbs);
-                }
-                cbs.add(cb);
+                mAppOpsServiceInterface.startWatchingOpModeChanged(cb, switchOp);
             }
             if (mayWatchPackageName) {
-                ArraySet<ModeCallback> cbs = mPackageModeWatchers.get(packageName);
-                if (cbs == null) {
-                    cbs = new ArraySet<>();
-                    mPackageModeWatchers.put(packageName, cbs);
-                }
-                cbs.add(cb);
+                mAppOpsServiceInterface.startWatchingPackageModeChanged(cb, packageName);
             }
             evalAllForegroundOpsLocked();
         }
@@ -3197,21 +3065,9 @@
             ModeCallback cb = mModeWatchers.remove(callback.asBinder());
             if (cb != null) {
                 cb.unlinkToDeath();
-                for (int i=mOpModeWatchers.size()-1; i>=0; i--) {
-                    ArraySet<ModeCallback> cbs = mOpModeWatchers.valueAt(i);
-                    cbs.remove(cb);
-                    if (cbs.size() <= 0) {
-                        mOpModeWatchers.removeAt(i);
-                    }
-                }
-                for (int i=mPackageModeWatchers.size()-1; i>=0; i--) {
-                    ArraySet<ModeCallback> cbs = mPackageModeWatchers.valueAt(i);
-                    cbs.remove(cb);
-                    if (cbs.size() <= 0) {
-                        mPackageModeWatchers.removeAt(i);
-                    }
-                }
+                mAppOpsServiceInterface.removeListener(cb);
             }
+
             evalAllForegroundOpsLocked();
         }
     }
@@ -4542,12 +4398,14 @@
                             AppOpsService::notifyOpChangedForAllPkgsInUid,
                             this, code, uidState.uid, true, null));
                 } else if (uidState.pkgOps != null) {
-                    final ArraySet<ModeCallback> callbacks = mOpModeWatchers.get(code);
-                    if (callbacks != null) {
-                        for (int cbi = callbacks.size() - 1; cbi >= 0; cbi--) {
-                            final ModeCallback callback = callbacks.valueAt(cbi);
-                            if ((callback.mFlags & AppOpsManager.WATCH_FOREGROUND_CHANGES) == 0
-                                    || !callback.isWatchingUid(uidState.uid)) {
+                    final ArraySet<OnOpModeChangedListener> listenerSet =
+                            mAppOpsServiceInterface.getOpModeChangedListeners(code);
+                    if (listenerSet != null) {
+                        for (int cbi = listenerSet.size() - 1; cbi >= 0; cbi--) {
+                            final OnOpModeChangedListener listener = listenerSet.valueAt(cbi);
+                            if ((listener.getFlags()
+                                    & AppOpsManager.WATCH_FOREGROUND_CHANGES) == 0
+                                    || !listener.isWatchingUid(uidState.uid)) {
                                 continue;
                             }
                             for (int pkgi = uidState.pkgOps.size() - 1; pkgi >= 0; pkgi--) {
@@ -4558,7 +4416,7 @@
                                 if (op.getMode() == AppOpsManager.MODE_FOREGROUND) {
                                     mHandler.sendMessage(PooledLambda.obtainMessage(
                                             AppOpsService::notifyOpChanged,
-                                            this, callback, code, uidState.uid,
+                                            this, listenerSet.valueAt(cbi), code, uidState.uid,
                                             uidState.pkgOps.keyAt(pkgi)));
                                 }
                             }
@@ -5045,7 +4903,7 @@
                 }
             }
             if (changed) {
-                uidState.evalForegroundOps(mOpModeWatchers);
+                uidState.evalForegroundOps();
             }
         }
     }
@@ -5131,7 +4989,7 @@
                 XmlUtils.skipCurrentTag(parser);
             }
         }
-        uidState.evalForegroundOps(mOpModeWatchers);
+        uidState.evalForegroundOps();
     }
 
     private void readAttributionOp(TypedXmlPullParser parser, @NonNull Op parent,
@@ -6122,62 +5980,17 @@
                 }
                 pw.println();
             }
-            if (mOpModeWatchers.size() > 0 && !dumpHistory) {
-                boolean printedHeader = false;
-                for (int i=0; i<mOpModeWatchers.size(); i++) {
-                    if (dumpOp >= 0 && dumpOp != mOpModeWatchers.keyAt(i)) {
-                        continue;
-                    }
-                    boolean printedOpHeader = false;
-                    ArraySet<ModeCallback> callbacks = mOpModeWatchers.valueAt(i);
-                    for (int j=0; j<callbacks.size(); j++) {
-                        final ModeCallback cb = callbacks.valueAt(j);
-                        if (dumpPackage != null
-                                && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
-                            continue;
-                        }
-                        needSep = true;
-                        if (!printedHeader) {
-                            pw.println("  Op mode watchers:");
-                            printedHeader = true;
-                        }
-                        if (!printedOpHeader) {
-                            pw.print("    Op ");
-                            pw.print(AppOpsManager.opToName(mOpModeWatchers.keyAt(i)));
-                            pw.println(":");
-                            printedOpHeader = true;
-                        }
-                        pw.print("      #"); pw.print(j); pw.print(": ");
-                        pw.println(cb);
-                    }
-                }
+
+            if (!dumpHistory) {
+                needSep |= mAppOpsServiceInterface.dumpListeners(dumpOp, dumpUid, dumpPackage, pw);
             }
-            if (mPackageModeWatchers.size() > 0 && dumpOp < 0 && !dumpHistory) {
-                boolean printedHeader = false;
-                for (int i=0; i<mPackageModeWatchers.size(); i++) {
-                    if (dumpPackage != null && !dumpPackage.equals(mPackageModeWatchers.keyAt(i))) {
-                        continue;
-                    }
-                    needSep = true;
-                    if (!printedHeader) {
-                        pw.println("  Package mode watchers:");
-                        printedHeader = true;
-                    }
-                    pw.print("    Pkg "); pw.print(mPackageModeWatchers.keyAt(i));
-                    pw.println(":");
-                    ArraySet<ModeCallback> callbacks = mPackageModeWatchers.valueAt(i);
-                    for (int j=0; j<callbacks.size(); j++) {
-                        pw.print("      #"); pw.print(j); pw.print(": ");
-                        pw.println(callbacks.valueAt(j));
-                    }
-                }
-            }
+
             if (mModeWatchers.size() > 0 && dumpOp < 0 && !dumpHistory) {
                 boolean printedHeader = false;
-                for (int i=0; i<mModeWatchers.size(); i++) {
+                for (int i = 0; i < mModeWatchers.size(); i++) {
                     final ModeCallback cb = mModeWatchers.valueAt(i);
                     if (dumpPackage != null
-                            && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
+                            && dumpUid != UserHandle.getAppId(cb.getWatchingUid())) {
                         continue;
                     }
                     needSep = true;
@@ -6729,16 +6542,15 @@
     }
 
     private void notifyWatchersOfChange(int code, int uid) {
-        final ArraySet<ModeCallback> clonedCallbacks;
+        final ArraySet<OnOpModeChangedListener> modeChangedListenerSet;
         synchronized (this) {
-            ArraySet<ModeCallback> callbacks = mOpModeWatchers.get(code);
-            if (callbacks == null) {
+            modeChangedListenerSet = mAppOpsServiceInterface.getOpModeChangedListeners(code);
+            if (modeChangedListenerSet == null) {
                 return;
             }
-            clonedCallbacks = new ArraySet<>(callbacks);
         }
 
-        notifyOpChanged(clonedCallbacks,  code, uid, null);
+        notifyOpChanged(modeChangedListenerSet,  code, uid, null);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/appop/AppOpsServiceInterface.java b/services/core/java/com/android/server/appop/AppOpsServiceInterface.java
index cd5ea12..c707086 100644
--- a/services/core/java/com/android/server/appop/AppOpsServiceInterface.java
+++ b/services/core/java/com/android/server/appop/AppOpsServiceInterface.java
@@ -14,12 +14,20 @@
  * limitations under the License.
  */
 package com.android.server.appop;
+
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.AppOpsManager.Mode;
+import android.util.ArraySet;
+import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
+
+import java.io.PrintWriter;
+
 /**
  * Interface for accessing and modifying modes for app-ops i.e. package and uid modes.
- * In the future this interface will also include mode callbacks and op restrictions.
+ * This interface also includes functions for added and removing op mode watchers.
+ * In the future this interface will also include op restrictions.
  */
 public interface AppOpsServiceInterface {
     /**
@@ -95,4 +103,93 @@
      * Stop tracking app-op modes for all uid and packages.
      */
     void clearAllModes();
+
+    /**
+     * Registers changedListener to listen to op's mode change.
+     * @param changedListener the listener that must be trigger on the op's mode change.
+     * @param op op representing the app-op whose mode change needs to be listened to.
+     */
+    void startWatchingOpModeChanged(@NonNull OnOpModeChangedListener changedListener, int op);
+
+    /**
+     * Registers changedListener to listen to package's app-op's mode change.
+     * @param changedListener the listener that must be trigger on the mode change.
+     * @param packageName of the package whose app-op's mode change needs to be listened to.
+     */
+    void startWatchingPackageModeChanged(@NonNull OnOpModeChangedListener changedListener,
+            @NonNull String packageName);
+
+    /**
+     * Stop the changedListener from triggering on any mode change.
+     * @param changedListener the listener that needs to be removed.
+     */
+    void removeListener(@NonNull OnOpModeChangedListener changedListener);
+
+    /**
+     * Temporary API which will be removed once we can safely untangle the methods that use this.
+     * Returns a set of OnOpModeChangedListener that are listening for op's mode changes.
+     * @param op app-op whose mode change is being listened to.
+     */
+    ArraySet<OnOpModeChangedListener> getOpModeChangedListeners(int op);
+
+    /**
+     * Temporary API which will be removed once we can safely untangle the methods that use this.
+     * Returns a set of OnOpModeChangedListener that are listening for package's op's mode changes.
+     * @param packageName of package whose app-op's mode change is being listened to.
+     */
+    ArraySet<OnOpModeChangedListener> getPackageModeChangedListeners(@NonNull String packageName);
+
+    /**
+     * Temporary API which will be removed once we can safely untangle the methods that use this.
+     * Notify that the app-op's mode is changed by triggering the change listener.
+     * @param changedListener the change listener.
+     * @param op App-op whose mode has changed
+     * @param uid user id associated with the app-op
+     * @param packageName package name that is associated with the app-op
+     */
+    void notifyOpChanged(@NonNull OnOpModeChangedListener changedListener, int op, int uid,
+            @Nullable String packageName);
+
+    /**
+     * Temporary API which will be removed once we can safely untangle the methods that use this.
+     * Notify that the app-op's mode is changed to all packages associated with the uid by
+     * triggering the appropriate change listener.
+     * @param op App-op whose mode has changed
+     * @param uid user id associated with the app-op
+     * @param onlyForeground true if only watchers that
+     * @param callbackToIgnore callback that should be ignored.
+     */
+    void notifyOpChangedForAllPkgsInUid(int op, int uid, boolean onlyForeground,
+            @Nullable OnOpModeChangedListener callbackToIgnore);
+
+    /**
+     * TODO: Move hasForegroundWatchers and foregroundOps into this.
+     * Go over the list of app-ops for the uid and mark app-ops with MODE_FOREGROUND in
+     * foregroundOps.
+     * @param uid for which the app-op's mode needs to be marked.
+     * @param foregroundOps boolean array where app-ops that have MODE_FOREGROUND are marked true.
+     * @return  foregroundOps.
+     */
+    SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps);
+
+    /**
+     * Go over the list of app-ops for the package name and mark app-ops with MODE_FOREGROUND in
+     * foregroundOps.
+     * @param packageName for which the app-op's mode needs to be marked.
+     * @param foregroundOps boolean array where app-ops that have MODE_FOREGROUND are marked true.
+     * @return foregroundOps.
+     */
+    SparseBooleanArray evalForegroundPackageOps(String packageName,
+            SparseBooleanArray foregroundOps);
+
+    /**
+     * Dump op mode and package mode listeners and their details.
+     * @param dumpOp if -1 then op mode listeners for all app-ops are dumped. If it's set to an
+     *               app-op, only the watchers for that app-op are dumped.
+     * @param dumpUid uid for which we want to dump op mode watchers.
+     * @param dumpPackage if not null and if dumpOp is -1, dumps watchers for the package name.
+     * @param printWriter writer to dump to.
+     */
+    boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage, PrintWriter printWriter);
+
 }
diff --git a/services/core/java/com/android/server/appop/LegacyAppOpsServiceInterfaceImpl.java b/services/core/java/com/android/server/appop/LegacyAppOpsServiceInterfaceImpl.java
index c27c0d3..2d498ea 100644
--- a/services/core/java/com/android/server/appop/LegacyAppOpsServiceInterfaceImpl.java
+++ b/services/core/java/com/android/server/appop/LegacyAppOpsServiceInterfaceImpl.java
@@ -16,15 +16,39 @@
 
 package com.android.server.appop;
 
+import static android.app.AppOpsManager.OP_NONE;
+import static android.app.AppOpsManager.WATCH_FOREGROUND_CHANGES;
+import static android.app.AppOpsManager.opRestrictsRead;
+
+import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
+
+import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppGlobals;
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.Mode;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.UserHandle;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.function.pooled.PooledLambda;
+
+import libcore.util.EmptyArray;
+
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.Objects;
 
 
 /**
@@ -33,8 +57,13 @@
  */
 public class LegacyAppOpsServiceInterfaceImpl implements AppOpsServiceInterface {
 
-    // Should be the same object that the AppOpsService is using for locking.
+    static final String TAG = "LegacyAppOpsServiceInterfaceImpl";
+
+    // Must be the same object that the AppOpsService is using for locking.
     final Object mLock;
+    final Handler mHandler;
+    final Context mContext;
+    final SparseArray<int[]> mSwitchedOps;
 
     @GuardedBy("mLock")
     @VisibleForTesting
@@ -43,13 +72,25 @@
     @GuardedBy("mLock")
     final ArrayMap<String, SparseIntArray> mPackageModes = new ArrayMap<>();
 
+    final SparseArray<ArraySet<OnOpModeChangedListener>> mOpModeWatchers = new SparseArray<>();
+    final ArrayMap<String, ArraySet<OnOpModeChangedListener>> mPackageModeWatchers =
+            new ArrayMap<>();
+
     final PersistenceScheduler mPersistenceScheduler;
 
 
+    // Constant meaning that any UID should be matched when dispatching callbacks
+    private static final int UID_ANY = -2;
+
+
     LegacyAppOpsServiceInterfaceImpl(PersistenceScheduler persistenceScheduler,
-            @NonNull Object lock) {
+            @NonNull Object lock, Handler handler, Context context,
+            SparseArray<int[]> switchedOps) {
         this.mPersistenceScheduler = persistenceScheduler;
         this.mLock = lock;
+        this.mHandler = handler;
+        this.mContext = context;
+        this.mSwitchedOps = switchedOps;
     }
 
     @Override
@@ -158,7 +199,6 @@
         }
     }
 
-
     @Override
     public boolean areUidModesDefault(int uid) {
         synchronized (mLock) {
@@ -195,4 +235,335 @@
         }
     }
 
-}
+    @Override
+    public void startWatchingOpModeChanged(@NonNull OnOpModeChangedListener changedListener,
+            int op) {
+        Objects.requireNonNull(changedListener);
+        synchronized (mLock) {
+            ArraySet<OnOpModeChangedListener> modeWatcherSet = mOpModeWatchers.get(op);
+            if (modeWatcherSet == null) {
+                modeWatcherSet = new ArraySet<>();
+                mOpModeWatchers.put(op, modeWatcherSet);
+            }
+            modeWatcherSet.add(changedListener);
+        }
+    }
+
+    @Override
+    public void startWatchingPackageModeChanged(@NonNull OnOpModeChangedListener changedListener,
+            @NonNull String packageName) {
+        Objects.requireNonNull(changedListener);
+        Objects.requireNonNull(packageName);
+        synchronized (mLock) {
+            ArraySet<OnOpModeChangedListener> modeWatcherSet =
+                    mPackageModeWatchers.get(packageName);
+            if (modeWatcherSet == null) {
+                modeWatcherSet = new ArraySet<>();
+                mPackageModeWatchers.put(packageName, modeWatcherSet);
+            }
+            modeWatcherSet.add(changedListener);
+        }
+    }
+
+    @Override
+    public void removeListener(@NonNull OnOpModeChangedListener changedListener) {
+        Objects.requireNonNull(changedListener);
+
+        synchronized (mLock) {
+            for (int i = mOpModeWatchers.size() - 1; i >= 0; i--) {
+                ArraySet<OnOpModeChangedListener> cbs = mOpModeWatchers.valueAt(i);
+                cbs.remove(changedListener);
+                if (cbs.size() <= 0) {
+                    mOpModeWatchers.removeAt(i);
+                }
+            }
+
+            for (int i = mPackageModeWatchers.size() - 1; i >= 0; i--) {
+                ArraySet<OnOpModeChangedListener> cbs = mPackageModeWatchers.valueAt(i);
+                cbs.remove(changedListener);
+                if (cbs.size() <= 0) {
+                    mPackageModeWatchers.removeAt(i);
+                }
+            }
+        }
+    }
+
+    @Override
+    public ArraySet<OnOpModeChangedListener> getOpModeChangedListeners(int op) {
+        synchronized (mLock) {
+            ArraySet<OnOpModeChangedListener> modeChangedListenersSet = mOpModeWatchers.get(op);
+            if (modeChangedListenersSet == null) {
+                return new ArraySet<>();
+            }
+            return new ArraySet<>(modeChangedListenersSet);
+        }
+    }
+
+    @Override
+    public ArraySet<OnOpModeChangedListener> getPackageModeChangedListeners(
+            @NonNull String packageName) {
+        Objects.requireNonNull(packageName);
+
+        synchronized (mLock) {
+            ArraySet<OnOpModeChangedListener> modeChangedListenersSet =
+                    mPackageModeWatchers.get(packageName);
+            if (modeChangedListenersSet == null) {
+                return new ArraySet<>();
+            }
+            return new ArraySet<>(modeChangedListenersSet);
+        }
+    }
+
+    @Override
+    public void notifyOpChanged(@NonNull OnOpModeChangedListener onModeChangedListener, int code,
+            int uid, @Nullable String packageName) {
+        Objects.requireNonNull(onModeChangedListener);
+
+        if (uid != UID_ANY && onModeChangedListener.getWatchingUid() >= 0
+                && onModeChangedListener.getWatchingUid() != uid) {
+            return;
+        }
+
+        // See CALL_BACK_ON_CHANGED_LISTENER_WITH_SWITCHED_OP_CHANGE
+        int[] switchedCodes;
+        if (onModeChangedListener.getWatchedOpCode() == ALL_OPS) {
+            switchedCodes = mSwitchedOps.get(code);
+        } else if (onModeChangedListener.getWatchedOpCode() == OP_NONE) {
+            switchedCodes = new int[]{code};
+        } else {
+            switchedCodes = new int[]{onModeChangedListener.getWatchedOpCode()};
+        }
+
+        for (int switchedCode : switchedCodes) {
+            // There are features watching for mode changes such as window manager
+            // and location manager which are in our process. The callbacks in these
+            // features may require permissions our remote caller does not have.
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                if (shouldIgnoreCallback(switchedCode, onModeChangedListener.getCallingPid(),
+                        onModeChangedListener.getCallingUid())) {
+                    continue;
+                }
+                onModeChangedListener.onOpModeChanged(switchedCode, uid, packageName);
+            } catch (RemoteException e) {
+                /* ignore */
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
+    private boolean shouldIgnoreCallback(int op, int watcherPid, int watcherUid) {
+        // If it's a restricted read op, ignore it if watcher doesn't have manage ops permission,
+        // as watcher should not use this to signal if the value is changed.
+        return opRestrictsRead(op) && mContext.checkPermission(Manifest.permission.MANAGE_APPOPS,
+                watcherPid, watcherUid) != PackageManager.PERMISSION_GRANTED;
+    }
+
+    @Override
+    public void notifyOpChangedForAllPkgsInUid(int code, int uid, boolean onlyForeground,
+            @Nullable OnOpModeChangedListener callbackToIgnore) {
+        String[] uidPackageNames = getPackagesForUid(uid);
+        ArrayMap<OnOpModeChangedListener, ArraySet<String>> callbackSpecs = null;
+
+        synchronized (mLock) {
+            ArraySet<OnOpModeChangedListener> callbacks = mOpModeWatchers.get(code);
+            if (callbacks != null) {
+                final int callbackCount = callbacks.size();
+                for (int i = 0; i < callbackCount; i++) {
+                    OnOpModeChangedListener callback = callbacks.valueAt(i);
+
+                    if (onlyForeground && (callback.getFlags()
+                            & WATCH_FOREGROUND_CHANGES) == 0) {
+                        continue;
+                    }
+
+                    ArraySet<String> changedPackages = new ArraySet<>();
+                    Collections.addAll(changedPackages, uidPackageNames);
+                    if (callbackSpecs == null) {
+                        callbackSpecs = new ArrayMap<>();
+                    }
+                    callbackSpecs.put(callback, changedPackages);
+                }
+            }
+
+            for (String uidPackageName : uidPackageNames) {
+                callbacks = mPackageModeWatchers.get(uidPackageName);
+                if (callbacks != null) {
+                    if (callbackSpecs == null) {
+                        callbackSpecs = new ArrayMap<>();
+                    }
+                    final int callbackCount = callbacks.size();
+                    for (int i = 0; i < callbackCount; i++) {
+                        OnOpModeChangedListener callback = callbacks.valueAt(i);
+
+                        if (onlyForeground && (callback.getFlags()
+                                & WATCH_FOREGROUND_CHANGES) == 0) {
+                            continue;
+                        }
+
+                        ArraySet<String> changedPackages = callbackSpecs.get(callback);
+                        if (changedPackages == null) {
+                            changedPackages = new ArraySet<>();
+                            callbackSpecs.put(callback, changedPackages);
+                        }
+                        changedPackages.add(uidPackageName);
+                    }
+                }
+            }
+
+            if (callbackSpecs != null && callbackToIgnore != null) {
+                callbackSpecs.remove(callbackToIgnore);
+            }
+        }
+
+        if (callbackSpecs == null) {
+            return;
+        }
+
+        for (int i = 0; i < callbackSpecs.size(); i++) {
+            final OnOpModeChangedListener callback = callbackSpecs.keyAt(i);
+            final ArraySet<String> reportedPackageNames = callbackSpecs.valueAt(i);
+            if (reportedPackageNames == null) {
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        LegacyAppOpsServiceInterfaceImpl::notifyOpChanged,
+                        this, callback, code, uid, (String) null));
+
+            } else {
+                final int reportedPackageCount = reportedPackageNames.size();
+                for (int j = 0; j < reportedPackageCount; j++) {
+                    final String reportedPackageName = reportedPackageNames.valueAt(j);
+                    mHandler.sendMessage(PooledLambda.obtainMessage(
+                            LegacyAppOpsServiceInterfaceImpl::notifyOpChanged,
+                            this, callback, code, uid, reportedPackageName));
+                }
+            }
+        }
+    }
+
+    private static String[] getPackagesForUid(int uid) {
+        String[] packageNames = null;
+
+        // Very early during boot the package manager is not yet or not yet fully started. At this
+        // time there are no packages yet.
+        if (AppGlobals.getPackageManager() != null) {
+            try {
+                packageNames = AppGlobals.getPackageManager().getPackagesForUid(uid);
+            } catch (RemoteException e) {
+                /* ignore - local call */
+            }
+        }
+        if (packageNames == null) {
+            return EmptyArray.STRING;
+        }
+        return packageNames;
+    }
+
+    @Override
+    public SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps) {
+        synchronized (mLock) {
+            return evalForegroundOps(mUidModes.get(uid), foregroundOps);
+        }
+    }
+
+    @Override
+    public SparseBooleanArray evalForegroundPackageOps(String packageName,
+            SparseBooleanArray foregroundOps) {
+        synchronized (mLock) {
+            return evalForegroundOps(mPackageModes.get(packageName), foregroundOps);
+        }
+    }
+
+    private SparseBooleanArray evalForegroundOps(SparseIntArray opModes,
+            SparseBooleanArray foregroundOps) {
+        SparseBooleanArray tempForegroundOps = foregroundOps;
+        if (opModes != null) {
+            for (int i = opModes.size() - 1; i >= 0; i--) {
+                if (opModes.valueAt(i) == AppOpsManager.MODE_FOREGROUND) {
+                    if (tempForegroundOps == null) {
+                        tempForegroundOps = new SparseBooleanArray();
+                    }
+                    evalForegroundWatchers(opModes.keyAt(i), tempForegroundOps);
+                }
+            }
+        }
+        return tempForegroundOps;
+    }
+
+    private void evalForegroundWatchers(int op, SparseBooleanArray foregroundOps) {
+        boolean curValue = foregroundOps.get(op, false);
+        ArraySet<OnOpModeChangedListener> listenerSet = mOpModeWatchers.get(op);
+        if (listenerSet != null) {
+            for (int cbi = listenerSet.size() - 1; !curValue && cbi >= 0; cbi--) {
+                if ((listenerSet.valueAt(cbi).getFlags()
+                        & AppOpsManager.WATCH_FOREGROUND_CHANGES) != 0) {
+                    curValue = true;
+                }
+            }
+        }
+        foregroundOps.put(op, curValue);
+    }
+
+    @Override
+    public boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage,
+            PrintWriter printWriter) {
+        boolean needSep = false;
+        if (mOpModeWatchers.size() > 0) {
+            boolean printedHeader = false;
+            for (int i = 0; i < mOpModeWatchers.size(); i++) {
+                if (dumpOp >= 0 && dumpOp != mOpModeWatchers.keyAt(i)) {
+                    continue;
+                }
+                boolean printedOpHeader = false;
+                ArraySet<OnOpModeChangedListener> modeChangedListenerSet =
+                        mOpModeWatchers.valueAt(i);
+                for (int j = 0; j < modeChangedListenerSet.size(); j++) {
+                    final OnOpModeChangedListener listener = modeChangedListenerSet.valueAt(j);
+                    if (dumpPackage != null
+                            && dumpUid != UserHandle.getAppId(listener.getWatchingUid())) {
+                        continue;
+                    }
+                    needSep = true;
+                    if (!printedHeader) {
+                        printWriter.println("  Op mode watchers:");
+                        printedHeader = true;
+                    }
+                    if (!printedOpHeader) {
+                        printWriter.print("    Op ");
+                        printWriter.print(AppOpsManager.opToName(mOpModeWatchers.keyAt(i)));
+                        printWriter.println(":");
+                        printedOpHeader = true;
+                    }
+                    printWriter.print("      #"); printWriter.print(j); printWriter.print(": ");
+                    printWriter.println(listener.toString());
+                }
+            }
+        }
+
+        if (mPackageModeWatchers.size() > 0 && dumpOp < 0) {
+            boolean printedHeader = false;
+            for (int i = 0; i < mPackageModeWatchers.size(); i++) {
+                if (dumpPackage != null
+                        && !dumpPackage.equals(mPackageModeWatchers.keyAt(i))) {
+                    continue;
+                }
+                needSep = true;
+                if (!printedHeader) {
+                    printWriter.println("  Package mode watchers:");
+                    printedHeader = true;
+                }
+                printWriter.print("    Pkg "); printWriter.print(mPackageModeWatchers.keyAt(i));
+                printWriter.println(":");
+                ArraySet<OnOpModeChangedListener> modeChangedListenerSet =
+                        mPackageModeWatchers.valueAt(i);
+
+                for (int j = 0; j < modeChangedListenerSet.size(); j++) {
+                    printWriter.print("      #"); printWriter.print(j); printWriter.print(": ");
+                    printWriter.println(modeChangedListenerSet.valueAt(j).toString());
+                }
+            }
+        }
+        return needSep;
+    }
+
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/appop/OnOpModeChangedListener.java b/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
new file mode 100644
index 0000000..5ebe811
--- /dev/null
+++ b/services/core/java/com/android/server/appop/OnOpModeChangedListener.java
@@ -0,0 +1,102 @@
+/*
+ * 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.appop;
+
+import android.os.RemoteException;
+
+/**
+ * Listener for mode changes, encapsulates methods that should be triggered in the event of a mode
+ * change.
+ */
+abstract class OnOpModeChangedListener {
+
+    // Constant meaning that any UID should be matched when dispatching callbacks
+    private static final int UID_ANY = -2;
+
+    private int mWatchingUid;
+    private int mFlags;
+    private int mWatchedOpCode;
+    private int mCallingUid;
+    private int mCallingPid;
+
+    OnOpModeChangedListener(int watchingUid, int flags, int watchedOpCode, int callingUid,
+            int callingPid) {
+        this.mWatchingUid = watchingUid;
+        this.mFlags = flags;
+        this.mWatchedOpCode = watchedOpCode;
+        this.mCallingUid = callingUid;
+        this.mCallingPid = callingPid;
+    }
+
+    /**
+     * Returns the user id that is watching for the mode change.
+     */
+    public int getWatchingUid() {
+        return mWatchingUid;
+    }
+
+    /**
+     * Returns the flags associated with the mode change listener.
+     */
+    public int getFlags() {
+        return mFlags;
+    }
+
+    /**
+     * Get the app-op whose mode change should trigger the callback.
+     */
+    public int getWatchedOpCode() {
+        return mWatchedOpCode;
+    }
+
+    /**
+     * Get the user-id that triggered the app-op mode change to be watched.
+     */
+    public int getCallingUid() {
+        return mCallingUid;
+    }
+
+    /**
+     * Get the process-id that triggered the app-op mode change to be watched.
+     */
+    public int getCallingPid() {
+        return mCallingPid;
+    }
+
+    /**
+     * returns true if the user id passed in the param is the one that is watching for op mode
+     * changed.
+     */
+    public boolean isWatchingUid(int uid) {
+        return uid == UID_ANY || mWatchingUid < 0 || mWatchingUid == uid;
+    }
+
+    /**
+     * Method that should be triggered when the app-op's mode is changed.
+     * @param op app-op whose mode-change is being listened to.
+     * @param uid user-is associated with the app-op.
+     * @param packageName package name associated with the app-op.
+     */
+    public abstract void onOpModeChanged(int op, int uid, String packageName)
+            throws RemoteException;
+
+    /**
+     * Return human readable string representing the listener.
+     */
+    public abstract String toString();
+
+}
diff --git a/services/core/java/com/android/server/attention/TEST_MAPPING b/services/core/java/com/android/server/attention/TEST_MAPPING
new file mode 100644
index 0000000..35b8165
--- /dev/null
+++ b/services/core/java/com/android/server/attention/TEST_MAPPING
@@ -0,0 +1,24 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsVoiceInteractionTestCases",
+      "options": [
+        {
+          "include-filter": "android.voiceinteraction.cts.AlwaysOnHotwordDetectorTest"
+        },
+        {
+          "include-filter": "android.voiceinteraction.cts.HotwordDetectedResultTest"
+        },
+        {
+          "include-filter": "android.voiceinteraction.cts.HotwordDetectionServiceBasicTest"
+        },
+        {
+          "include-filter": "android.voiceinteraction.cts.HotwordDetectionServiceProximityTest"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 25211c8..c1f4969 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -376,7 +376,8 @@
                         makeLeAudioDeviceUnavailable(address, btInfo.mAudioSystemDevice);
                     } else if (switchToAvailable) {
                         makeLeAudioDeviceAvailable(address, BtHelper.getName(btInfo.mDevice),
-                                streamType, btInfo.mAudioSystemDevice, "onSetBtActiveDevice");
+                                streamType, btInfo.mVolume, btInfo.mAudioSystemDevice,
+                                "onSetBtActiveDevice");
                     }
                     break;
                 default: throw new IllegalArgumentException("Invalid profile "
@@ -1176,8 +1177,8 @@
     }
 
     @GuardedBy("mDevicesLock")
-    private void makeLeAudioDeviceAvailable(String address, String name, int streamType, int device,
-            String eventSource) {
+    private void makeLeAudioDeviceAvailable(String address, String name, int streamType,
+            int volumeIndex, int device, String eventSource) {
         if (device != AudioSystem.DEVICE_NONE) {
             /* Audio Policy sees Le Audio similar to A2DP. Let's make sure
              * AUDIO_POLICY_FORCE_NO_BT_A2DP is not set
@@ -1198,7 +1199,9 @@
             return;
         }
 
-        final int leAudioVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType, device);
+        final int leAudioVolIndex = (volumeIndex == -1)
+                ? mDeviceBroker.getVssVolumeForDevice(streamType, device)
+                : volumeIndex;
         final int maxIndex = mDeviceBroker.getMaxVssVolumeForStream(streamType);
         mDeviceBroker.postSetLeAudioVolumeIndex(leAudioVolIndex, maxIndex, streamType);
         mDeviceBroker.postApplyVolumeOnDevice(streamType, device, "makeLeAudioDeviceAvailable");
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 8356134..aedbe4e 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -571,7 +571,9 @@
     // There may be different devices with the same device type (aliasing).
     // We always send the full device state info on each change.
     private void logDeviceState(SADeviceState deviceState, String event) {
-        final String deviceName = AudioSystem.getDeviceName(deviceState.mDeviceType);
+        final int deviceType = AudioDeviceInfo.convertDeviceTypeToInternalDevice(
+                deviceState.mDeviceType);
+        final String deviceName = AudioSystem.getDeviceName(deviceType);
         new MediaMetrics.Item(METRICS_DEVICE_PREFIX + deviceName)
             .set(MediaMetrics.Property.ADDRESS, deviceState.mDeviceAddress)
             .set(MediaMetrics.Property.ENABLED, deviceState.mEnabled ? "true" : "false")
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/BiometricServiceProvider.java
new file mode 100644
index 0000000..0f1fe68
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricServiceProvider.java
@@ -0,0 +1,61 @@
+/*
+ * 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.biometrics.sensors;
+
+import android.annotation.NonNull;
+import android.hardware.biometrics.SensorPropertiesInternal;
+import android.util.proto.ProtoOutputStream;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Common attributes for all biometric service providers.
+ *
+ * @param <T> Internal settings type.
+ */
+public interface BiometricServiceProvider<T extends SensorPropertiesInternal> {
+
+    /** Checks if the specified sensor is owned by this provider. */
+    boolean containsSensor(int sensorId);
+
+    /** All sensor properties. */
+    @NonNull
+    List<T> getSensorProperties();
+
+    /** Properties for the given sensor id. */
+    @NonNull
+    T getSensorProperties(int sensorId);
+
+    boolean isHardwareDetected(int sensorId);
+
+    /** If the user has any enrollments for the given sensor. */
+    boolean hasEnrollments(int sensorId, int userId);
+
+    long getAuthenticatorId(int sensorId, int userId);
+
+    @LockoutTracker.LockoutMode
+    int getLockoutModeForUser(int sensorId, int userId);
+
+    void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto,
+            boolean clearSchedulerBuffer);
+
+    void dumpProtoMetrics(int sensorId, @NonNull FileDescriptor fd);
+
+    void dumpInternal(int sensorId, @NonNull PrintWriter pw);
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricServiceRegistry.java b/services/core/java/com/android/server/biometrics/sensors/BiometricServiceRegistry.java
new file mode 100644
index 0000000..4779f6f
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricServiceRegistry.java
@@ -0,0 +1,243 @@
+/*
+ * 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.biometrics.sensors;
+
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.biometrics.IBiometricAuthenticator;
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.biometrics.SensorPropertiesInternal;
+import android.os.Handler;
+import android.os.IInterface;
+import android.os.Process;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.ServiceThread;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Container for all BiometricServiceProvider implementations.
+ *
+ * @param <T> The service provider type.
+ * @param <P> The internal properties type.
+ * @param <C> The registration callback for {@link #invokeRegisteredCallback(IInterface, List)}.
+ */
+public abstract class BiometricServiceRegistry<T extends BiometricServiceProvider<P>,
+        P extends SensorPropertiesInternal,
+        C extends IInterface> {
+
+    private static final String TAG = "BiometricServiceRegistry";
+
+    // Volatile so they can be read without a lock once all services are registered.
+    // But, ideally remove this and provide immutable copies via the callback instead.
+    @Nullable
+    private volatile List<T> mServiceProviders;
+    @Nullable
+    private volatile List<P> mAllProps;
+
+    @NonNull
+    private final Supplier<IBiometricService> mBiometricServiceSupplier;
+    @NonNull
+    private final RemoteCallbackList<C> mRegisteredCallbacks = new RemoteCallbackList<>();
+
+    public BiometricServiceRegistry(@NonNull Supplier<IBiometricService> biometricSupplier) {
+        mBiometricServiceSupplier = biometricSupplier;
+    }
+
+    /**
+     * Register an implementation by creating a new authenticator and initializing it via
+     * {@link IBiometricService#registerAuthenticator(int, int, int, IBiometricAuthenticator)}
+     * using the given properties.
+     *
+     * @param service service to register with
+     * @param props   internal properties to initialize the authenticator
+     */
+    protected abstract void registerService(@NonNull IBiometricService service, @NonNull P props);
+
+    /**
+     * Invoke the callback to notify clients that all authenticators have been registered.
+     *
+     * @param callback callback to invoke
+     * @param allProps properties of all authenticators
+     */
+    protected abstract void invokeRegisteredCallback(@NonNull C callback,
+            @NonNull List<P> allProps) throws RemoteException;
+
+    /**
+     * Register all authenticators in a background thread.
+     *
+     * @param serviceProvider Supplier function that will be invoked on the background thread.
+     */
+    public void registerAll(Supplier<List<T>> serviceProvider) {
+        // Some HAL might not be started before the system service and will cause the code below
+        // to wait, and some of the operations below might take a significant amount of time to
+        // complete (calls to the HALs). To avoid blocking the rest of system server we put
+        // this on a background thread.
+        final ServiceThread thread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND,
+                true /* allowIo */);
+        thread.start();
+        final Handler handler = new Handler(thread.getLooper());
+        handler.post(() -> registerAllInBackground(serviceProvider));
+        thread.quitSafely();
+    }
+
+    /** Register authenticators now, only called by {@link #registerAll(Supplier).} */
+    @VisibleForTesting
+    public void registerAllInBackground(Supplier<List<T>> serviceProvider) {
+        List<T> providers = serviceProvider.get();
+        if (providers == null) {
+            providers = new ArrayList<>();
+        }
+
+        final IBiometricService biometricService = mBiometricServiceSupplier.get();
+        if (biometricService == null) {
+            throw new IllegalStateException("biometric service cannot be null");
+        }
+
+        // Register each sensor individually with BiometricService
+        final List<P> allProps = new ArrayList<>();
+        for (T provider : providers) {
+            final List<P> props = provider.getSensorProperties();
+            for (P prop : props) {
+                registerService(biometricService, prop);
+            }
+            allProps.addAll(props);
+        }
+
+        finishRegistration(providers, allProps);
+    }
+
+    private synchronized void finishRegistration(
+            @NonNull List<T> providers, @NonNull List<P> allProps) {
+        mServiceProviders = Collections.unmodifiableList(providers);
+        mAllProps = Collections.unmodifiableList(allProps);
+        broadcastAllAuthenticatorsRegistered();
+    }
+
+    /**
+     * Add a callback that will be invoked once the work from {@link #registerAll(Supplier)}
+     * has finished registering all providers (executes immediately if already done).
+     *
+     * @param callback registration callback
+     */
+    public synchronized void addAllRegisteredCallback(@Nullable C callback) {
+        if (callback == null) {
+            Slog.e(TAG, "addAllRegisteredCallback, callback is null");
+            return;
+        }
+
+        final boolean registered = mRegisteredCallbacks.register(callback);
+        final boolean allRegistered = mServiceProviders != null;
+        if (registered && allRegistered) {
+            broadcastAllAuthenticatorsRegistered();
+        } else if (!registered) {
+            Slog.e(TAG, "addAllRegisteredCallback failed to register callback");
+        }
+    }
+
+    private synchronized void broadcastAllAuthenticatorsRegistered() {
+        final int n = mRegisteredCallbacks.beginBroadcast();
+        for (int i = 0; i < n; ++i) {
+            final C cb = mRegisteredCallbacks.getBroadcastItem(i);
+            try {
+                invokeRegisteredCallback(cb, mAllProps);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception in broadcastAllAuthenticatorsRegistered", e);
+            } finally {
+                mRegisteredCallbacks.unregister(cb);
+            }
+        }
+        mRegisteredCallbacks.finishBroadcast();
+    }
+
+    /**
+     * Get a list of registered providers.
+     *
+     * Undefined until {@link #registerAll(Supplier)} has fired the completion callback.
+     */
+    @NonNull
+    public List<T> getProviders() {
+        return mServiceProviders != null ? mServiceProviders : Collections.emptyList();
+    }
+
+    /**
+     * Gets the provider for given sensor id or null if not registered.
+     *
+     * Undefined until {@link #registerAll(Supplier)} has fired the completion callback.
+     */
+    @Nullable
+    public T getProviderForSensor(int sensorId) {
+        if (mServiceProviders != null) {
+            for (T provider : mServiceProviders) {
+                if (provider.containsSensor(sensorId)) {
+                    return provider;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Finds the provider for devices with only a single sensor.
+     *
+     * If no providers returns null. If multiple sensors are present this method
+     * will return the first one that is found (this is a legacy for test devices that
+     * use aidl/hidl concurrently and should not occur on real devices).
+     *
+     * Undefined until {@link #registerAll(Supplier)} has fired the completion callback.
+     */
+    @Nullable
+    public Pair<Integer, T> getSingleProvider() {
+        if (mAllProps == null || mAllProps.isEmpty()) {
+            Slog.e(TAG, "No sensors found");
+            return null;
+        }
+
+        if (mAllProps.size() > 1) {
+            Slog.e(TAG, "getSingleProvider() called but multiple sensors present: "
+                    + mAllProps.size());
+        }
+
+        final int sensorId = mAllProps.get(0).sensorId;
+        final T provider = getProviderForSensor(sensorId);
+        if (provider != null) {
+            return new Pair<>(sensorId, provider);
+        }
+
+        Slog.e(TAG, "Single sensor: " + sensorId + ", but provider not found");
+        return null;
+    }
+
+    /**
+     * Get the properties for all providers.
+     *
+     * Undefined until {@link #registerAll(Supplier)} has fired the completion callback.
+     */
+    @NonNull
+    public List<P> getAllProperties() {
+        return mAllProps != null ? mAllProps : Collections.emptyList();
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java b/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java
index 0d789f7..f854316 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricStateCallback.java
@@ -23,32 +23,64 @@
 import static android.hardware.biometrics.BiometricStateListener.STATE_KEYGUARD_AUTH;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.UserInfo;
 import android.hardware.biometrics.BiometricStateListener;
 import android.hardware.biometrics.IBiometricStateListener;
+import android.hardware.biometrics.SensorPropertiesInternal;
 import android.os.RemoteException;
+import android.os.UserManager;
 import android.util.Slog;
 
 import com.android.server.biometrics.Utils;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
  * A callback for receiving notifications about biometric sensor state changes.
+ *
+ * @param <T> service provider type
+ * @param <P> internal property type
  */
-public class BiometricStateCallback implements ClientMonitorCallback {
+public class BiometricStateCallback<T extends BiometricServiceProvider<P>,
+        P extends SensorPropertiesInternal> implements ClientMonitorCallback {
 
     private static final String TAG = "BiometricStateCallback";
 
     @NonNull
-    private final CopyOnWriteArrayList<IBiometricStateListener>
-            mBiometricStateListeners = new CopyOnWriteArrayList<>();
+    private final CopyOnWriteArrayList<IBiometricStateListener> mBiometricStateListeners =
+            new CopyOnWriteArrayList<>();
+    @NonNull
+    private final UserManager mUserManager;
+    @BiometricStateListener.State
+    private int mBiometricState;
+    @NonNull
+    private List<T> mProviders = List.of();
 
-    private @BiometricStateListener.State int mBiometricState;
-
-    public BiometricStateCallback() {
+    /**
+     * Create a new callback that must be {@link #start(List)}ed.
+     *
+     * @param userManager user manager
+     */
+    public BiometricStateCallback(@NonNull UserManager userManager) {
         mBiometricState = STATE_IDLE;
+        mUserManager = userManager;
     }
 
+    /**
+     * This should be called when the service has been initialized and all providers are ready.
+     *
+     * @param allProviders all registered biometric service providers
+     */
+    public synchronized void start(@NonNull List<T> allProviders) {
+        mProviders = Collections.unmodifiableList(allProviders);
+        broadcastCurrentEnrollmentState(null /* listener */);
+    }
+
+    /** Get the current state. */
+    @BiometricStateListener.State
     public int getBiometricState() {
         return mBiometricState;
     }
@@ -120,23 +152,43 @@
     }
 
     /**
-     * This should be invoked when:
-     * 1) Enrolled --> None-enrolled
-     * 2) None-enrolled --> enrolled
-     * 3) HAL becomes ready
-     * 4) Listener is registered
+     * Enables clients to register a BiometricStateListener. For example, this is used to forward
+     * fingerprint sensor state changes to SideFpsEventHandler.
+     *
+     * @param listener listener to register
      */
-    public void notifyAllEnrollmentStateChanged(int userId, int sensorId,
+    public synchronized void registerBiometricStateListener(
+            @NonNull IBiometricStateListener listener) {
+        mBiometricStateListeners.add(listener);
+        broadcastCurrentEnrollmentState(listener);
+    }
+
+    private synchronized void broadcastCurrentEnrollmentState(
+            @Nullable IBiometricStateListener listener) {
+        for (T provider : mProviders) {
+            for (SensorPropertiesInternal prop : provider.getSensorProperties()) {
+                for (UserInfo userInfo : mUserManager.getAliveUsers()) {
+                    final boolean enrolled = provider.hasEnrollments(prop.sensorId, userInfo.id);
+                    if (listener != null) {
+                        notifyEnrollmentStateChanged(
+                                listener, userInfo.id, prop.sensorId, enrolled);
+                    } else {
+                        notifyAllEnrollmentStateChanged(
+                                userInfo.id, prop.sensorId, enrolled);
+                    }
+                }
+            }
+        }
+    }
+
+    private void notifyAllEnrollmentStateChanged(int userId, int sensorId,
             boolean hasEnrollments) {
         for (IBiometricStateListener listener : mBiometricStateListeners) {
             notifyEnrollmentStateChanged(listener, userId, sensorId, hasEnrollments);
         }
     }
 
-    /**
-     * Notifies the listener of enrollment state changes.
-     */
-    public void notifyEnrollmentStateChanged(@NonNull IBiometricStateListener listener,
+    private void notifyEnrollmentStateChanged(@NonNull IBiometricStateListener listener,
             int userId, int sensorId, boolean hasEnrollments) {
         try {
             listener.onEnrollmentsChanged(userId, sensorId, hasEnrollments);
@@ -144,14 +196,4 @@
             Slog.e(TAG, "Remote exception", e);
         }
     }
-
-    /**
-     * Enables clients to register a BiometricStateListener. For example, this is used to forward
-     * fingerprint sensor state changes to SideFpsEventHandler.
-     *
-     * @param listener
-     */
-    public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) {
-        mBiometricStateListeners.add(listener);
-    }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 79e65cc..271bce9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -17,20 +17,18 @@
 package com.android.server.biometrics.sensors.face;
 
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
-import static android.Manifest.permission.MANAGE_BIOMETRIC;
 import static android.Manifest.permission.MANAGE_FACE;
 import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
-import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.content.Context;
-import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.IBiometricSensorReceiver;
 import android.hardware.biometrics.IBiometricService;
 import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
+import android.hardware.biometrics.IBiometricStateListener;
 import android.hardware.biometrics.IInvalidationCallback;
 import android.hardware.biometrics.ITestSession;
 import android.hardware.biometrics.ITestSessionCallback;
@@ -39,18 +37,18 @@
 import android.hardware.face.Face;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.face.FaceServiceReceiver;
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
 import android.hardware.face.IFaceService;
 import android.hardware.face.IFaceServiceReceiver;
 import android.os.Binder;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.NativeHandle;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
 import android.os.ShellCallback;
 import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
@@ -58,10 +56,10 @@
 
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.LockPatternUtils;
-import com.android.server.ServiceThread;
 import com.android.server.SystemService;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
 import com.android.server.biometrics.sensors.LockoutTracker;
@@ -88,51 +86,10 @@
     private final LockoutResetDispatcher mLockoutResetDispatcher;
     private final LockPatternUtils mLockPatternUtils;
     @NonNull
-    private final List<ServiceProvider> mServiceProviders;
-
-    @Nullable
-    private ServiceProvider getProviderForSensor(int sensorId) {
-        for (ServiceProvider provider : mServiceProviders) {
-            if (provider.containsSensor(sensorId)) {
-                return provider;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * For devices with only a single provider, returns that provider. If no providers, or multiple
-     * providers exist, returns null.
-     */
-    @Nullable
-    private Pair<Integer, ServiceProvider> getSingleProvider() {
-        final List<FaceSensorPropertiesInternal> properties = getSensorProperties();
-        if (properties.size() != 1) {
-            Slog.e(TAG, "Multiple sensors found: " + properties.size());
-            return null;
-        }
-
-        // Theoretically we can just return the first provider, but maybe this is easier to
-        // understand.
-        final int sensorId = properties.get(0).sensorId;
-        for (ServiceProvider provider : mServiceProviders) {
-            if (provider.containsSensor(sensorId)) {
-                return new Pair<>(sensorId, provider);
-            }
-        }
-
-        Slog.e(TAG, "Single sensor, but provider not found");
-        return null;
-    }
-
+    private final FaceServiceRegistry mRegistry;
     @NonNull
-    private List<FaceSensorPropertiesInternal> getSensorProperties() {
-        final List<FaceSensorPropertiesInternal> properties = new ArrayList<>();
-        for (ServiceProvider provider : mServiceProviders) {
-            properties.addAll(provider.getSensorProperties());
-        }
-        return properties;
-    }
+    private final BiometricStateCallback<ServiceProvider, FaceSensorPropertiesInternal>
+            mBiometricStateCallback;
 
     /**
      * Receives the incoming binder calls from FaceManager.
@@ -142,8 +99,7 @@
         @Override
         public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
                 @NonNull String opPackageName) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
 
             if (provider == null) {
                 Slog.w(TAG, "Null provider for createTestSession, sensorId: " + sensorId);
@@ -156,9 +112,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public byte[] dumpSensorServiceStateProto(int sensorId, boolean clearSchedulerBuffer) {
-
             final ProtoOutputStream proto = new ProtoOutputStream();
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider != null) {
                 provider.dumpProtoState(sensorId, proto, clearSchedulerBuffer);
             }
@@ -170,16 +125,14 @@
         @Override // Binder call
         public List<FaceSensorPropertiesInternal> getSensorPropertiesInternal(
                 String opPackageName) {
-
-            return FaceService.this.getSensorProperties();
+            return mRegistry.getAllProperties();
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public FaceSensorPropertiesInternal getSensorProperties(int sensorId,
                 @NonNull String opPackageName) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for getSensorProperties, sensorId: " + sensorId
                         + ", caller: " + opPackageName);
@@ -193,8 +146,7 @@
         @Override // Binder call
         public void generateChallenge(IBinder token, int sensorId, int userId,
                 IFaceServiceReceiver receiver, String opPackageName) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for generateChallenge, sensorId: " + sensorId);
                 return;
@@ -207,8 +159,7 @@
         @Override // Binder call
         public void revokeChallenge(IBinder token, int sensorId, int userId, String opPackageName,
                 long challenge) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for revokeChallenge, sensorId: " + sensorId);
                 return;
@@ -222,8 +173,7 @@
         public long enroll(int userId, final IBinder token, final byte[] hardwareAuthToken,
                 final IFaceServiceReceiver receiver, final String opPackageName,
                 final int[] disabledFeatures, Surface previewSurface, boolean debugConsent) {
-
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for enroll");
                 return -1;
@@ -245,8 +195,7 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC)
         @Override // Binder call
         public void cancelEnrollment(final IBinder token, long requestId) {
-
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelEnrollment");
                 return;
@@ -260,7 +209,6 @@
         public long authenticate(final IBinder token, final long operationId, int userId,
                 final IFaceServiceReceiver receiver, final String opPackageName,
                 boolean isKeyguardBypassEnabled) {
-
             // TODO(b/152413782): If the sensor supports face detect and the device is encrypted or
             //  lockdown, something wrong happened. See similar path in FingerprintService.
 
@@ -273,7 +221,7 @@
             // permission.
             final boolean isKeyguard = Utils.isKeyguard(getContext(), opPackageName);
 
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for authenticate");
                 return -1;
@@ -301,7 +249,7 @@
                 return -1;
             }
 
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for detectFace");
                 return -1;
@@ -318,8 +266,7 @@
                 IBinder token, long operationId, int userId,
                 IBiometricSensorReceiver sensorReceiver, String opPackageName, long requestId,
                 int cookie, boolean allowBackgroundAuthentication) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for prepareForAuthentication");
                 return;
@@ -336,8 +283,7 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public void startPreparedClient(int sensorId, int cookie) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for startPreparedClient");
                 return;
@@ -350,8 +296,7 @@
         @Override // Binder call
         public void cancelAuthentication(final IBinder token, final String opPackageName,
                 final long requestId) {
-
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelAuthentication");
                 return;
@@ -370,7 +315,7 @@
                 return;
             }
 
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelFaceDetect");
                 return;
@@ -383,8 +328,7 @@
         @Override // Binder call
         public void cancelAuthenticationFromService(int sensorId, final IBinder token,
                 final String opPackageName, final long requestId) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelAuthenticationFromService");
                 return;
@@ -397,8 +341,7 @@
         @Override // Binder call
         public void remove(final IBinder token, final int faceId, final int userId,
                 final IFaceServiceReceiver receiver, final String opPackageName) {
-
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for remove");
                 return;
@@ -412,7 +355,6 @@
         @Override // Binder call
         public void removeAll(final IBinder token, final int userId,
                 final IFaceServiceReceiver receiver, final String opPackageName) {
-
             final FaceServiceReceiver internalReceiver = new FaceServiceReceiver() {
                 int sensorsFinishedRemoving = 0;
                 final int numSensors = getSensorPropertiesInternal(
@@ -432,7 +374,7 @@
 
             // This effectively iterates through all sensors, but has to do so by finding all
             // sensors under each provider.
-            for (ServiceProvider provider : mServiceProviders) {
+            for (ServiceProvider provider : mRegistry.getProviders()) {
                 List<FaceSensorPropertiesInternal> props = provider.getSensorProperties();
                 for (FaceSensorPropertiesInternal prop : props) {
                     provider.scheduleRemoveAll(prop.sensorId, token, userId, internalReceiver,
@@ -467,27 +409,27 @@
             try {
                 if (args.length > 1 && "--proto".equals(args[0]) && "--state".equals(args[1])) {
                     final ProtoOutputStream proto = new ProtoOutputStream(fd);
-                    for (ServiceProvider provider : mServiceProviders) {
+                    for (ServiceProvider provider : mRegistry.getProviders()) {
                         for (FaceSensorPropertiesInternal props : provider.getSensorProperties()) {
                             provider.dumpProtoState(props.sensorId, proto, false);
                         }
                     }
                     proto.flush();
                 } else if (args.length > 0 && "--proto".equals(args[0])) {
-                    for (ServiceProvider provider : mServiceProviders) {
+                    for (ServiceProvider provider : mRegistry.getProviders()) {
                         for (FaceSensorPropertiesInternal props : provider.getSensorProperties()) {
                             provider.dumpProtoMetrics(props.sensorId, fd);
                         }
                     }
                 } else if (args.length > 1 && "--hal".equals(args[0])) {
-                    for (ServiceProvider provider : mServiceProviders) {
+                    for (ServiceProvider provider : mRegistry.getProviders()) {
                         for (FaceSensorPropertiesInternal props : provider.getSensorProperties()) {
                             provider.dumpHal(props.sensorId, fd,
                                     Arrays.copyOfRange(args, 1, args.length, args.getClass()));
                         }
                     }
                 } else {
-                    for (ServiceProvider provider : mServiceProviders) {
+                    for (ServiceProvider provider : mRegistry.getProviders()) {
                         for (FaceSensorPropertiesInternal props : provider.getSensorProperties()) {
                             pw.println("Dumping for sensorId: " + props.sensorId
                                     + ", provider: " + provider.getClass().getSimpleName());
@@ -504,10 +446,9 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public boolean isHardwareDetected(int sensorId, String opPackageName) {
-
             final long token = Binder.clearCallingIdentity();
             try {
-                final ServiceProvider provider = getProviderForSensor(sensorId);
+                final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
                 if (provider == null) {
                     Slog.w(TAG, "Null provider for isHardwareDetected, caller: " + opPackageName);
                     return false;
@@ -521,12 +462,11 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public List<Face> getEnrolledFaces(int sensorId, int userId, String opPackageName) {
-
             if (userId != UserHandle.getCallingUserId()) {
                 Utils.checkPermission(getContext(), INTERACT_ACROSS_USERS);
             }
 
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for getEnrolledFaces, caller: " + opPackageName);
                 return Collections.emptyList();
@@ -538,12 +478,11 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public boolean hasEnrolledFaces(int sensorId, int userId, String opPackageName) {
-
             if (userId != UserHandle.getCallingUserId()) {
                 Utils.checkPermission(getContext(), INTERACT_ACROSS_USERS);
             }
 
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for hasEnrolledFaces, caller: " + opPackageName);
                 return false;
@@ -555,8 +494,7 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public @LockoutTracker.LockoutMode int getLockoutModeForUser(int sensorId, int userId) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for getLockoutModeForUser");
                 return LockoutTracker.LOCKOUT_NONE;
@@ -569,8 +507,7 @@
         @Override
         public void invalidateAuthenticatorId(int sensorId, int userId,
                 IInvalidationCallback callback) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for invalidateAuthenticatorId");
                 return;
@@ -582,7 +519,7 @@
         @Override // Binder call
         public long getAuthenticatorId(int sensorId, int userId) {
 
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for getAuthenticatorId");
                 return 0;
@@ -595,8 +532,7 @@
         @Override // Binder call
         public void resetLockout(IBinder token, int sensorId, int userId, byte[] hardwareAuthToken,
                 String opPackageName) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for resetLockout, caller: " + opPackageName);
                 return;
@@ -610,8 +546,7 @@
         public void setFeature(final IBinder token, int userId, int feature, boolean enabled,
                 final byte[] hardwareAuthToken, IFaceServiceReceiver receiver,
                 final String opPackageName) {
-
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for setFeature");
                 return;
@@ -625,8 +560,7 @@
         @Override
         public void getFeature(final IBinder token, int userId, int feature,
                 IFaceServiceReceiver receiver, final String opPackageName) {
-
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for getFeature");
                 return;
@@ -636,18 +570,14 @@
                     new ClientMonitorCallbackConverter(receiver), opPackageName);
         }
 
-        private void addHidlProviders(@NonNull List<FaceSensorPropertiesInternal> hidlSensors) {
-            for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) {
-                mServiceProviders.add(
-                        Face10.newInstance(getContext(), hidlSensor, mLockoutResetDispatcher));
-            }
-        }
+        private List<ServiceProvider> getAidlProviders() {
+            final List<ServiceProvider> providers = new ArrayList<>();
 
-        private void addAidlProviders() {
             final String[] instances = ServiceManager.getDeclaredInstances(IFace.DESCRIPTOR);
             if (instances == null || instances.length == 0) {
-                return;
+                return providers;
             }
+
             for (String instance : instances) {
                 final String fqName = IFace.DESCRIPTOR + "/" + instance;
                 final IFace face = IFace.Stub.asInterface(
@@ -660,53 +590,41 @@
                     final SensorProps[] props = face.getSensorProps();
                     final FaceProvider provider = new FaceProvider(getContext(), props, instance,
                             mLockoutResetDispatcher, BiometricContext.getInstance(getContext()));
-                    mServiceProviders.add(provider);
+                    providers.add(provider);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
                 }
             }
+
+            return providers;
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public void registerAuthenticators(
                 @NonNull List<FaceSensorPropertiesInternal> hidlSensors) {
-
-            // Some HAL might not be started before the system service and will cause the code below
-            // to wait, and some of the operations below might take a significant amount of time to
-            // complete (calls to the HALs). To avoid blocking the rest of system server we put
-            // this on a background thread.
-            final ServiceThread thread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND,
-                    true /* allowIo */);
-            thread.start();
-            final Handler handler = new Handler(thread.getLooper());
-
-            handler.post(() -> {
-                addHidlProviders(hidlSensors);
-                addAidlProviders();
-
-                final IBiometricService biometricService = IBiometricService.Stub.asInterface(
-                        ServiceManager.getService(Context.BIOMETRIC_SERVICE));
-
-                // Register each sensor individually with BiometricService
-                for (ServiceProvider provider : mServiceProviders) {
-                    final List<FaceSensorPropertiesInternal> props = provider.getSensorProperties();
-                    for (FaceSensorPropertiesInternal prop : props) {
-                        final int sensorId = prop.sensorId;
-                        final @BiometricManager.Authenticators.Types int strength =
-                                Utils.propertyStrengthToAuthenticatorStrength(prop.sensorStrength);
-                        final FaceAuthenticator authenticator = new FaceAuthenticator(
-                                mServiceWrapper, sensorId);
-                        try {
-                            biometricService.registerAuthenticator(sensorId, TYPE_FACE, strength,
-                                    authenticator);
-                        } catch (RemoteException e) {
-                            Slog.e(TAG, "Remote exception when registering sensorId: " + sensorId);
-                        }
-                    }
+            mRegistry.registerAll(() -> {
+                final List<ServiceProvider> providers = new ArrayList<>();
+                for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) {
+                    providers.add(
+                            Face10.newInstance(getContext(), hidlSensor, mLockoutResetDispatcher));
                 }
+                providers.addAll(getAidlProviders());
+                return providers;
             });
         }
+
+        @Override
+        public void addAuthenticatorsRegisteredCallback(
+                IFaceAuthenticatorsRegisteredCallback callback) {
+            Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL);
+            mRegistry.addAllRegisteredCallback(callback);
+        }
+
+        @Override
+        public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) {
+            mBiometricStateCallback.registerBiometricStateListener(listener);
+        }
     }
 
     public FaceService(Context context) {
@@ -714,7 +632,16 @@
         mServiceWrapper = new FaceServiceWrapper();
         mLockoutResetDispatcher = new LockoutResetDispatcher(context);
         mLockPatternUtils = new LockPatternUtils(context);
-        mServiceProviders = new ArrayList<>();
+        mBiometricStateCallback = new BiometricStateCallback<>(UserManager.get(context));
+        mRegistry = new FaceServiceRegistry(mServiceWrapper,
+                () -> IBiometricService.Stub.asInterface(
+                        ServiceManager.getService(Context.BIOMETRIC_SERVICE)));
+        mRegistry.addAllRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() {
+            @Override
+            public void onAllAuthenticatorsRegistered(List<FaceSensorPropertiesInternal> sensors) {
+                mBiometricStateCallback.start(mRegistry.getProviders());
+            }
+        });
     }
 
     @Override
@@ -752,7 +679,7 @@
         if (Utils.isVirtualEnabled(getContext())) {
             Slog.i(TAG, "Sync virtual enrollments");
             final int userId = ActivityManager.getCurrentUser();
-            for (ServiceProvider provider : mServiceProviders) {
+            for (ServiceProvider provider : mRegistry.getProviders()) {
                 for (FaceSensorPropertiesInternal props : provider.getSensorProperties()) {
                     provider.scheduleInternalCleanup(props.sensorId, userId, null /* callback */,
                             true /* favorHalEnrollments */);
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java
new file mode 100644
index 0000000..0f0a81d
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceServiceRegistry.java
@@ -0,0 +1,71 @@
+/*
+ * 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.biometrics.sensors.face;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.face.FaceSensorPropertiesInternal;
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
+import android.hardware.face.IFaceService;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.server.biometrics.Utils;
+import com.android.server.biometrics.sensors.BiometricServiceRegistry;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+/** Registry for {@link IFaceService} providers. */
+public class FaceServiceRegistry extends BiometricServiceRegistry<ServiceProvider,
+        FaceSensorPropertiesInternal, IFaceAuthenticatorsRegisteredCallback> {
+
+    private static final String TAG = "FaceServiceRegistry";
+
+    @NonNull
+    private final IFaceService mService;
+
+    /** Creates a new registry tied to the given service. */
+    public FaceServiceRegistry(@NonNull IFaceService service,
+            @Nullable Supplier<IBiometricService> biometricSupplier) {
+        super(biometricSupplier);
+        mService = service;
+    }
+
+    @Override
+    protected void registerService(@NonNull IBiometricService service,
+            @NonNull FaceSensorPropertiesInternal props) {
+        @BiometricManager.Authenticators.Types final int strength =
+                Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength);
+        try {
+            service.registerAuthenticator(props.sensorId, TYPE_FACE, strength,
+                    new FaceAuthenticator(mService, props.sensorId));
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Remote exception when registering sensorId: " + props.sensorId);
+        }
+    }
+
+    @Override
+    protected void invokeRegisteredCallback(@NonNull IFaceAuthenticatorsRegisteredCallback callback,
+            @NonNull List<FaceSensorPropertiesInternal> allProps) throws RemoteException {
+        callback.onAllAuthenticatorsRegistered(allProps);
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
index 6f98365..4efaedb 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/ServiceProvider.java
@@ -26,15 +26,13 @@
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.face.IFaceServiceReceiver;
 import android.os.IBinder;
-import android.util.proto.ProtoOutputStream;
 import android.view.Surface;
 
+import com.android.server.biometrics.sensors.BiometricServiceProvider;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
-import com.android.server.biometrics.sensors.LockoutTracker;
 
 import java.io.FileDescriptor;
-import java.io.PrintWriter;
 import java.util.List;
 
 /**
@@ -56,24 +54,11 @@
  * to check (e.g. via {@link FaceManager#getSensorPropertiesInternal()}) that the code path isn't
  * taken. ServiceProviders will provide a no-op for unsupported operations to fail safely.
  */
-public interface ServiceProvider {
-    /**
-     * Checks if the specified sensor is owned by this provider.
-     */
-    boolean containsSensor(int sensorId);
-
-    @NonNull
-    List<FaceSensorPropertiesInternal> getSensorProperties();
-
-    @NonNull
-    FaceSensorPropertiesInternal getSensorProperties(int sensorId);
+public interface ServiceProvider extends BiometricServiceProvider<FaceSensorPropertiesInternal> {
 
     @NonNull
     List<Face> getEnrolledFaces(int sensorId, int userId);
 
-    @LockoutTracker.LockoutMode
-    int getLockoutModeForUser(int sensorId, int userId);
-
     /**
      * Requests for the authenticatorId (whose source of truth is in the TEE or equivalent) to be
      * invalidated. See {@link com.android.server.biometrics.sensors.InvalidationRequesterClient}
@@ -84,10 +69,6 @@
                 + " this method");
     }
 
-    long getAuthenticatorId(int sensorId, int userId);
-
-    boolean isHardwareDetected(int sensorId);
-
     void scheduleGenerateChallenge(int sensorId, int userId, @NonNull IBinder token,
             @NonNull IFaceServiceReceiver receiver, String opPackageName);
 
@@ -142,13 +123,6 @@
     void scheduleInternalCleanup(int sensorId, int userId,
             @Nullable ClientMonitorCallback callback, boolean favorHalEnrollments);
 
-    void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto,
-            boolean clearSchedulerBuffer);
-
-    void dumpProtoMetrics(int sensorId, @NonNull FileDescriptor fd);
-
-    void dumpInternal(int sensorId, @NonNull PrintWriter pw);
-
     @NonNull
     ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
             @NonNull String opPackageName);
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 19d54c8..6bff179 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -285,6 +285,11 @@
     }
 
     @Override
+    public boolean hasEnrollments(int sensorId, int userId) {
+        return !getEnrolledFaces(sensorId, userId).isEmpty();
+    }
+
+    @Override
     public void scheduleInvalidateAuthenticatorId(int sensorId, int userId,
             @NonNull IInvalidationCallback callback) {
         mHandler.post(() -> {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
index 6528912..c0a119f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
@@ -484,6 +484,11 @@
     }
 
     @Override
+    public boolean hasEnrollments(int sensorId, int userId) {
+        return !getEnrolledFaces(sensorId, userId).isEmpty();
+    }
+
+    @Override
     @LockoutTracker.LockoutMode
     public int getLockoutModeForUser(int sensorId, int userId) {
         return mLockoutTracker.getLockoutModeForUser(userId);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index 2ba449a..7e2742e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -17,14 +17,11 @@
 package com.android.server.biometrics.sensors.fingerprint;
 
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
-import static android.Manifest.permission.MANAGE_BIOMETRIC;
 import static android.Manifest.permission.MANAGE_FINGERPRINT;
-import static android.Manifest.permission.RESET_FINGERPRINT_LOCKOUT;
 import static android.Manifest.permission.TEST_BIOMETRIC;
 import static android.Manifest.permission.USE_BIOMETRIC;
 import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
 import static android.Manifest.permission.USE_FINGERPRINT;
-import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_VENDOR;
@@ -36,8 +33,6 @@
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
-import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricPrompt;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.IBiometricSensorReceiver;
@@ -65,7 +60,6 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Process;
-import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
@@ -79,11 +73,9 @@
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.R;
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.LockPatternUtils;
-import com.android.server.ServiceThread;
 import com.android.server.SystemService;
 import com.android.server.biometrics.Utils;
 import com.android.server.biometrics.log.BiometricContext;
@@ -115,74 +107,32 @@
 
     protected static final String TAG = "FingerprintService";
 
-    private final Object mLock = new Object();
     private final AppOpsManager mAppOps;
     private final LockoutResetDispatcher mLockoutResetDispatcher;
     private final GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
     private final LockPatternUtils mLockPatternUtils;
-    @NonNull private final List<ServiceProvider> mServiceProviders;
-    @NonNull private final BiometricStateCallback mBiometricStateCallback;
-    @NonNull private final Handler mHandler;
-    @NonNull private final BiometricContext mBiometricContext;
-    @NonNull private final Supplier<IBiometricService> mBiometricServiceSupplier;
-    @NonNull private final Function<String, IFingerprint> mIFingerprintProvider;
+    @NonNull
+    private final BiometricContext mBiometricContext;
+    @NonNull
+    private final Supplier<String[]> mAidlInstanceNameSupplier;
+    @NonNull
+    private final Function<String, IFingerprint> mIFingerprintProvider;
+    @NonNull
+    private final BiometricStateCallback<ServiceProvider, FingerprintSensorPropertiesInternal>
+            mBiometricStateCallback;
+    @NonNull
+    private final Handler mHandler;
+    @NonNull
+    private final FingerprintServiceRegistry mRegistry;
 
-    @GuardedBy("mLock")
-    @NonNull private final RemoteCallbackList<IFingerprintAuthenticatorsRegisteredCallback>
-            mAuthenticatorsRegisteredCallbacks;
-
-    @GuardedBy("mLock")
-    @NonNull private final List<FingerprintSensorPropertiesInternal> mSensorProps;
-
-    /**
-     * Registers BiometricStateListener in list stored by FingerprintService
-     * @param listener new BiometricStateListener being added
-     */
-    public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) {
-        mBiometricStateCallback.registerBiometricStateListener(listener);
-        broadcastCurrentEnrollmentState(listener);
-    }
-
-    /**
-     * @param listener if non-null, notifies only this listener. if null, notifies all listeners
-     *                 in {@link BiometricStateCallback}. This is slightly ugly, but reduces
-     *                 redundant code.
-     */
-    private void broadcastCurrentEnrollmentState(@Nullable IBiometricStateListener listener) {
-        final UserManager um = UserManager.get(getContext());
-        synchronized (mLock) {
-            // Update the new listener with current state of all sensors
-            for (FingerprintSensorPropertiesInternal prop : mSensorProps) {
-                final ServiceProvider provider = getProviderForSensor(prop.sensorId);
-                for (UserInfo userInfo : um.getAliveUsers()) {
-                    final boolean enrolled = !provider
-                            .getEnrolledFingerprints(prop.sensorId, userInfo.id).isEmpty();
-
-                    // Defer this work and allow the loop to release the lock sooner
-                    mHandler.post(() -> {
-                        if (listener != null) {
-                            mBiometricStateCallback.notifyEnrollmentStateChanged(
-                                    listener, userInfo.id, prop.sensorId, enrolled);
-                        } else {
-                            mBiometricStateCallback.notifyAllEnrollmentStateChanged(
-                                    userInfo.id, prop.sensorId, enrolled);
-                        }
-                    });
-                }
-            }
-        }
-    }
-
-    /**
-     * Receives the incoming binder calls from FingerprintManager.
-     */
-    private final IFingerprintService.Stub mServiceWrapper = new IFingerprintService.Stub() {
+    /** Receives the incoming binder calls from FingerprintManager. */
+    @VisibleForTesting
+    final IFingerprintService.Stub mServiceWrapper = new IFingerprintService.Stub() {
         @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC)
         @Override
         public ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
                 @NonNull String opPackageName) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
 
             if (provider == null) {
                 Slog.w(TAG, "Null provider for createTestSession, sensorId: " + sensorId);
@@ -195,9 +145,8 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public byte[] dumpSensorServiceStateProto(int sensorId, boolean clearSchedulerBuffer) {
-
             final ProtoOutputStream proto = new ProtoOutputStream();
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider != null) {
                 provider.dumpProtoState(sensorId, proto, clearSchedulerBuffer);
             }
@@ -212,16 +161,14 @@
                     != PackageManager.PERMISSION_GRANTED) {
                 Utils.checkPermission(getContext(), TEST_BIOMETRIC);
             }
-
-            return FingerprintService.this.getSensorProperties();
+            return mRegistry.getAllProperties();
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public FingerprintSensorPropertiesInternal getSensorProperties(int sensorId,
                 @NonNull String opPackageName) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for getSensorProperties, sensorId: " + sensorId
                         + ", caller: " + opPackageName);
@@ -234,8 +181,7 @@
         @Override // Binder call
         public void generateChallenge(IBinder token, int sensorId, int userId,
                 IFingerprintServiceReceiver receiver, String opPackageName) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for generateChallenge, sensorId: " + sensorId);
                 return;
@@ -248,8 +194,7 @@
         @Override // Binder call
         public void revokeChallenge(IBinder token, int sensorId, int userId, String opPackageName,
                 long challenge) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching sensor for revokeChallenge, sensorId: " + sensorId);
                 return;
@@ -264,8 +209,7 @@
         public long enroll(final IBinder token, @NonNull final byte[] hardwareAuthToken,
                 final int userId, final IFingerprintServiceReceiver receiver,
                 final String opPackageName, @FingerprintManager.EnrollReason int enrollReason) {
-
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for enroll");
                 return -1;
@@ -278,8 +222,7 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_FINGERPRINT)
         @Override // Binder call
         public void cancelEnrollment(final IBinder token, long requestId) {
-
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelEnrollment");
                 return;
@@ -339,10 +282,10 @@
 
             final Pair<Integer, ServiceProvider> provider;
             if (sensorId == FingerprintManager.SENSOR_ID_ANY) {
-                provider = getSingleProvider();
+                provider = mRegistry.getSingleProvider();
             } else {
                 Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL);
-                provider = new Pair<>(sensorId, getProviderForSensor(sensorId));
+                provider = new Pair<>(sensorId, mRegistry.getProviderForSensor(sensorId));
             }
             if (provider == null) {
                 Slog.w(TAG, "Null provider for authenticate");
@@ -374,7 +317,6 @@
                 final IFingerprintServiceReceiver receiver,
                 final String opPackageName,
                 boolean ignoreEnrollmentState) throws PackageManager.NameNotFoundException {
-
             final Context context = getUiContext();
             final Context promptContext = context.createPackageContextAsUser(
                     opPackageName, 0 /* flags */, UserHandle.getUserHandleForUid(uId));
@@ -468,7 +410,7 @@
                 return -1;
             }
 
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for detectFingerprint");
                 return -1;
@@ -484,8 +426,7 @@
         public void prepareForAuthentication(int sensorId, IBinder token, long operationId,
                 int userId, IBiometricSensorReceiver sensorReceiver, String opPackageName,
                 long requestId, int cookie, boolean allowBackgroundAuthentication) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for prepareForAuthentication");
                 return;
@@ -501,8 +442,7 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_BIOMETRIC)
         @Override // Binder call
         public void startPreparedClient(int sensorId, int cookie) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for startPreparedClient");
                 return;
@@ -532,7 +472,7 @@
                 return;
             }
 
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelAuthentication");
                 return;
@@ -553,7 +493,7 @@
 
             // For IBiometricsFingerprint2.1, cancelling fingerprint detect is the same as
             // cancelling authentication.
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelFingerprintDetect");
                 return;
@@ -566,11 +506,9 @@
         @Override // Binder call
         public void cancelAuthenticationFromService(final int sensorId, final IBinder token,
                 final String opPackageName, final long requestId) {
-
-
             Slog.d(TAG, "cancelAuthenticationFromService, sensorId: " + sensorId);
 
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for cancelAuthenticationFromService");
                 return;
@@ -583,8 +521,7 @@
         @Override // Binder call
         public void remove(final IBinder token, final int fingerId, final int userId,
                 final IFingerprintServiceReceiver receiver, final String opPackageName) {
-
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for remove");
                 return;
@@ -617,7 +554,7 @@
 
             // This effectively iterates through all sensors, but has to do so by finding all
             // sensors under each provider.
-            for (ServiceProvider provider : mServiceProviders) {
+            for (ServiceProvider provider : mRegistry.getProviders()) {
                 List<FingerprintSensorPropertiesInternal> props = provider.getSensorProperties();
                 for (FingerprintSensorPropertiesInternal prop : props) {
                     provider.scheduleRemoveAll(prop.sensorId, token, internalReceiver, userId,
@@ -652,7 +589,7 @@
             try {
                 if (args.length > 1 && "--proto".equals(args[0]) && "--state".equals(args[1])) {
                     final ProtoOutputStream proto = new ProtoOutputStream(fd);
-                    for (ServiceProvider provider : mServiceProviders) {
+                    for (ServiceProvider provider : mRegistry.getProviders()) {
                         for (FingerprintSensorPropertiesInternal props
                                 : provider.getSensorProperties()) {
                             provider.dumpProtoState(props.sensorId, proto, false);
@@ -660,14 +597,14 @@
                     }
                     proto.flush();
                 } else if (args.length > 0 && "--proto".equals(args[0])) {
-                    for (ServiceProvider provider : mServiceProviders) {
+                    for (ServiceProvider provider : mRegistry.getProviders()) {
                         for (FingerprintSensorPropertiesInternal props
                                 : provider.getSensorProperties()) {
                             provider.dumpProtoMetrics(props.sensorId, fd);
                         }
                     }
                 } else {
-                    for (ServiceProvider provider : mServiceProviders) {
+                    for (ServiceProvider provider : mRegistry.getProviders()) {
                         for (FingerprintSensorPropertiesInternal props
                                 : provider.getSensorProperties()) {
                             pw.println("Dumping for sensorId: " + props.sensorId
@@ -698,7 +635,7 @@
 
             final long token = Binder.clearCallingIdentity();
             try {
-                final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+                final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
                 if (provider == null) {
                     Slog.w(TAG, "Null provider for isHardwareDetectedDeprecated, caller: "
                             + opPackageName);
@@ -713,8 +650,7 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public boolean isHardwareDetected(int sensorId, String opPackageName) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for isHardwareDetected, caller: " + opPackageName);
                 return false;
@@ -730,7 +666,7 @@
                 return;
             }
 
-            final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+            final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
             if (provider == null) {
                 Slog.w(TAG, "Null provider for rename");
                 return;
@@ -781,8 +717,7 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         public boolean hasEnrolledFingerprints(int sensorId, int userId, String opPackageName) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for hasEnrolledFingerprints, caller: " + opPackageName);
                 return false;
@@ -794,8 +729,7 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public @LockoutTracker.LockoutMode int getLockoutModeForUser(int sensorId, int userId) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for getLockoutModeForUser");
                 return LockoutTracker.LOCKOUT_NONE;
@@ -807,8 +741,7 @@
         @Override
         public void invalidateAuthenticatorId(int sensorId, int userId,
                 IInvalidationCallback callback) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for invalidateAuthenticatorId");
                 return;
@@ -819,8 +752,7 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override // Binder call
         public long getAuthenticatorId(int sensorId, int userId) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for getAuthenticatorId");
                 return 0;
@@ -832,8 +764,7 @@
         @Override // Binder call
         public void resetLockout(IBinder token, int sensorId, int userId,
                 @Nullable byte[] hardwareAuthToken, String opPackageName) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "Null provider for resetLockout, caller: " + opPackageName);
                 return;
@@ -864,55 +795,38 @@
         @Override // Binder call
         public void registerAuthenticators(
                 @NonNull List<FingerprintSensorPropertiesInternal> hidlSensors) {
-
-            // Some HAL might not be started before the system service and will cause the code below
-            // to wait, and some of the operations below might take a significant amount of time to
-            // complete (calls to the HALs). To avoid blocking the rest of system server we put
-            // this on a background thread.
-            final ServiceThread thread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND,
-                    true /* allowIo */);
-            thread.start();
-            final Handler handler = new Handler(thread.getLooper());
-            handler.post(() -> {
+            mRegistry.registerAll(() -> {
+                final List<ServiceProvider> providers = new ArrayList<>();
+                providers.addAll(getHidlProviders(hidlSensors));
                 List<String> aidlSensors = new ArrayList<>();
-                final String[] instances =
-                        ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR);
+                final String[] instances = mAidlInstanceNameSupplier.get();
                 if (instances != null) {
                     aidlSensors.addAll(Lists.newArrayList(instances));
                 }
-                registerAuthenticatorsForService(aidlSensors, hidlSensors);
+                providers.addAll(getAidlProviders(
+                        Utils.filterAvailableHalInstances(getContext(), aidlSensors)));
+                return providers;
             });
-            thread.quitSafely();
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void addAuthenticatorsRegisteredCallback(
                 IFingerprintAuthenticatorsRegisteredCallback callback) {
-            if (callback == null) {
-                Slog.e(TAG, "addAuthenticatorsRegisteredCallback, callback is null");
-                return;
-            }
+            mRegistry.addAllRegisteredCallback(callback);
+        }
 
-            final boolean registered;
-            final boolean hasSensorProps;
-            synchronized (mLock) {
-                registered = mAuthenticatorsRegisteredCallbacks.register(callback);
-                hasSensorProps = !mSensorProps.isEmpty();
-            }
-            if (registered && hasSensorProps) {
-                broadcastAllAuthenticatorsRegistered();
-            } else if (!registered) {
-                Slog.e(TAG, "addAuthenticatorsRegisteredCallback failed to register callback");
-            }
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+        @Override
+        public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) {
+            mBiometricStateCallback.registerBiometricStateListener(listener);
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void onPointerDown(long requestId, int sensorId, int x, int y,
                 float minor, float major) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching provider for onFingerDown, sensorId: " + sensorId);
                 return;
@@ -923,8 +837,7 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void onPointerUp(long requestId, int sensorId) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching provider for onFingerUp, sensorId: " + sensorId);
                 return;
@@ -935,8 +848,7 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void onUiReady(long requestId, int sensorId) {
-
-            final ServiceProvider provider = getProviderForSensor(sensorId);
+            final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);
             if (provider == null) {
                 Slog.w(TAG, "No matching provider for onUiReady, sensorId: " + sensorId);
                 return;
@@ -947,8 +859,7 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void setUdfpsOverlayController(@NonNull IUdfpsOverlayController controller) {
-
-            for (ServiceProvider provider : mServiceProviders) {
+            for (ServiceProvider provider : mRegistry.getProviders()) {
                 provider.setUdfpsOverlayController(controller);
             }
         }
@@ -956,22 +867,15 @@
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void setSidefpsController(@NonNull ISidefpsController controller) {
-
-            for (ServiceProvider provider : mServiceProviders) {
+            for (ServiceProvider provider : mRegistry.getProviders()) {
                 provider.setSidefpsController(controller);
             }
         }
 
-        @Override
-        public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) {
-            Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL);
-            FingerprintService.this.registerBiometricStateListener(listener);
-        }
-
+        @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void onPowerPressed() {
-            Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL);
-            for (ServiceProvider provider : mServiceProviders) {
+            for (ServiceProvider provider : mRegistry.getProviders()) {
                 provider.onPowerPressed();
             }
         }
@@ -981,6 +885,7 @@
         this(context, BiometricContext.getInstance(context),
                 () -> IBiometricService.Stub.asInterface(
                         ServiceManager.getService(Context.BIOMETRIC_SERVICE)),
+                () -> ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR),
                 (fqName) -> IFingerprint.Stub.asInterface(
                         Binder.allowBlocking(ServiceManager.waitForDeclaredService(fqName))));
     }
@@ -988,61 +893,34 @@
     @VisibleForTesting
     FingerprintService(Context context,
             BiometricContext biometricContext,
-            Supplier<IBiometricService> biometricServiceProvider,
+            Supplier<IBiometricService> biometricServiceSupplier,
+            Supplier<String[]> aidlInstanceNameSupplier,
             Function<String, IFingerprint> fingerprintProvider) {
         super(context);
         mBiometricContext = biometricContext;
-        mBiometricServiceSupplier = biometricServiceProvider;
+        mAidlInstanceNameSupplier = aidlInstanceNameSupplier;
         mIFingerprintProvider = fingerprintProvider;
         mAppOps = context.getSystemService(AppOpsManager.class);
         mGestureAvailabilityDispatcher = new GestureAvailabilityDispatcher();
         mLockoutResetDispatcher = new LockoutResetDispatcher(context);
         mLockPatternUtils = new LockPatternUtils(context);
-        mServiceProviders = new ArrayList<>();
-        mBiometricStateCallback = new BiometricStateCallback();
-        mAuthenticatorsRegisteredCallbacks = new RemoteCallbackList<>();
-        mSensorProps = new ArrayList<>();
+        mBiometricStateCallback = new BiometricStateCallback<>(UserManager.get(context));
         mHandler = new Handler(Looper.getMainLooper());
+        mRegistry = new FingerprintServiceRegistry(mServiceWrapper, biometricServiceSupplier);
+        mRegistry.addAllRegisteredCallback(new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
+            @Override
+            public void onAllAuthenticatorsRegistered(
+                    List<FingerprintSensorPropertiesInternal> sensors) {
+                mBiometricStateCallback.start(mRegistry.getProviders());
+            }
+        });
     }
 
-    @VisibleForTesting
-    void registerAuthenticatorsForService(@NonNull List<String> aidlInstanceNames,
+    @NonNull
+    private List<ServiceProvider> getHidlProviders(
             @NonNull List<FingerprintSensorPropertiesInternal> hidlSensors) {
-        addHidlProviders(hidlSensors);
-        addAidlProviders(Utils.filterAvailableHalInstances(getContext(), aidlInstanceNames));
+        final List<ServiceProvider> providers = new ArrayList<>();
 
-        final IBiometricService biometricService = mBiometricServiceSupplier.get();
-
-        // Register each sensor individually with BiometricService
-        for (ServiceProvider provider : mServiceProviders) {
-            final List<FingerprintSensorPropertiesInternal> props =
-                    provider.getSensorProperties();
-            for (FingerprintSensorPropertiesInternal prop : props) {
-                final int sensorId = prop.sensorId;
-                @BiometricManager.Authenticators.Types final int strength =
-                        Utils.propertyStrengthToAuthenticatorStrength(prop.sensorStrength);
-                final FingerprintAuthenticator authenticator = new FingerprintAuthenticator(
-                        mServiceWrapper, sensorId);
-                try {
-                    biometricService.registerAuthenticator(sensorId, TYPE_FINGERPRINT,
-                            strength, authenticator);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Remote exception when registering sensorId: " + sensorId);
-                }
-            }
-        }
-
-        synchronized (mLock) {
-            for (ServiceProvider provider : mServiceProviders) {
-                mSensorProps.addAll(provider.getSensorProperties());
-            }
-        }
-
-        broadcastCurrentEnrollmentState(null); // broadcasts to all listeners
-        broadcastAllAuthenticatorsRegistered();
-    }
-
-    private void addHidlProviders(List<FingerprintSensorPropertiesInternal> hidlSensors) {
         for (FingerprintSensorPropertiesInternal hidlSensor : hidlSensors) {
             final Fingerprint21 fingerprint21;
             if ((Build.IS_USERDEBUG || Build.IS_ENG)
@@ -1059,11 +937,16 @@
                         mBiometricStateCallback, hidlSensor, mHandler,
                         mLockoutResetDispatcher, mGestureAvailabilityDispatcher);
             }
-            mServiceProviders.add(fingerprint21);
+            providers.add(fingerprint21);
         }
+
+        return providers;
     }
 
-    private void addAidlProviders(List<String> instances) {
+    @NonNull
+    private List<ServiceProvider> getAidlProviders(@NonNull List<String> instances) {
+        final List<ServiceProvider> providers = new ArrayList<>();
+
         for (String instance : instances) {
             final String fqName = IFingerprint.DESCRIPTOR + "/" + instance;
             final IFingerprint fp = mIFingerprintProvider.apply(fqName);
@@ -1075,7 +958,7 @@
                             mLockoutResetDispatcher, mGestureAvailabilityDispatcher,
                             mBiometricContext);
                     Slog.i(TAG, "Adding AIDL provider: " + fqName);
-                    mServiceProviders.add(provider);
+                    providers.add(provider);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
                 }
@@ -1083,38 +966,8 @@
                 Slog.e(TAG, "Unable to get declared service: " + fqName);
             }
         }
-    }
 
-    // Notifies the callbacks that all of the authenticators have been registered and removes the
-    // invoked callbacks from the callback list.
-    private void broadcastAllAuthenticatorsRegistered() {
-        // Make a local copy of the data so it can be used outside of the synchronized block when
-        // making Binder calls.
-        final List<IFingerprintAuthenticatorsRegisteredCallback> callbacks = new ArrayList<>();
-        final List<FingerprintSensorPropertiesInternal> props;
-        synchronized (mLock) {
-            if (!mSensorProps.isEmpty()) {
-                props = new ArrayList<>(mSensorProps);
-            } else {
-                Slog.e(TAG, "mSensorProps is empty");
-                return;
-            }
-            final int n = mAuthenticatorsRegisteredCallbacks.beginBroadcast();
-            for (int i = 0; i < n; ++i) {
-                final IFingerprintAuthenticatorsRegisteredCallback cb =
-                        mAuthenticatorsRegisteredCallbacks.getBroadcastItem(i);
-                callbacks.add(cb);
-                mAuthenticatorsRegisteredCallbacks.unregister(cb);
-            }
-            mAuthenticatorsRegisteredCallbacks.finishBroadcast();
-        }
-        for (IFingerprintAuthenticatorsRegisteredCallback cb : callbacks) {
-            try {
-                cb.onAllAuthenticatorsRegistered(props);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Remote exception in onAllAuthenticatorsRegistered", e);
-            }
-        }
+        return providers;
     }
 
     @Override
@@ -1122,51 +975,9 @@
         publishBinderService(Context.FINGERPRINT_SERVICE, mServiceWrapper);
     }
 
-    @Nullable
-    private ServiceProvider getProviderForSensor(int sensorId) {
-        for (ServiceProvider provider : mServiceProviders) {
-            if (provider.containsSensor(sensorId)) {
-                return provider;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * For devices with only a single provider, returns that provider. If multiple providers,
-     * returns the first one. If no providers, returns null.
-     */
-    @Nullable
-    private Pair<Integer, ServiceProvider> getSingleProvider() {
-        final List<FingerprintSensorPropertiesInternal> properties = getSensorProperties();
-        if (properties.isEmpty()) {
-            Slog.e(TAG, "No providers found");
-            return null;
-        }
-
-        // Theoretically we can just return the first provider, but maybe this is easier to
-        // understand.
-        final int sensorId = properties.get(0).sensorId;
-        for (ServiceProvider provider : mServiceProviders) {
-            if (provider.containsSensor(sensorId)) {
-                return new Pair<>(sensorId, provider);
-            }
-        }
-
-        Slog.e(TAG, "Provider not found");
-        return null;
-    }
-
-    @NonNull
-    private List<FingerprintSensorPropertiesInternal> getSensorProperties() {
-        synchronized (mLock) {
-            return mSensorProps;
-        }
-    }
-
     @NonNull
     private List<Fingerprint> getEnrolledFingerprintsDeprecated(int userId, String opPackageName) {
-        final Pair<Integer, ServiceProvider> provider = getSingleProvider();
+        final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
         if (provider == null) {
             Slog.w(TAG, "Null provider for getEnrolledFingerprintsDeprecated, caller: "
                     + opPackageName);
@@ -1229,7 +1040,7 @@
         if (Utils.isVirtualEnabled(getContext())) {
             Slog.i(TAG, "Sync virtual enrollments");
             final int userId = ActivityManager.getCurrentUser();
-            for (ServiceProvider provider : mServiceProviders) {
+            for (ServiceProvider provider : mRegistry.getProviders()) {
                 for (FingerprintSensorPropertiesInternal props : provider.getSensorProperties()) {
                     provider.scheduleInternalCleanup(props.sensorId, userId, null /* callback */,
                             true /* favorHalEnrollments */);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java
new file mode 100644
index 0000000..33810b7
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistry.java
@@ -0,0 +1,72 @@
+/*
+ * 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.biometrics.sensors.fingerprint;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
+import android.hardware.fingerprint.IFingerprintService;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.server.biometrics.Utils;
+import com.android.server.biometrics.sensors.BiometricServiceRegistry;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+/** Registry for {@link IFingerprintService} providers. */
+public class FingerprintServiceRegistry extends BiometricServiceRegistry<ServiceProvider,
+        FingerprintSensorPropertiesInternal, IFingerprintAuthenticatorsRegisteredCallback> {
+
+    private static final String TAG = "FingerprintServiceRegistry";
+
+    @NonNull
+    private final IFingerprintService mService;
+
+    /** Creates a new registry tied to the given service. */
+    public FingerprintServiceRegistry(@NonNull IFingerprintService service,
+            @Nullable Supplier<IBiometricService> biometricSupplier) {
+        super(biometricSupplier);
+        mService = service;
+    }
+
+    @Override
+    protected void registerService(@NonNull IBiometricService service,
+            @NonNull FingerprintSensorPropertiesInternal props) {
+        @BiometricManager.Authenticators.Types final int strength =
+                Utils.propertyStrengthToAuthenticatorStrength(props.sensorStrength);
+        try {
+            service.registerAuthenticator(props.sensorId, TYPE_FINGERPRINT, strength,
+                    new FingerprintAuthenticator(mService, props.sensorId));
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Remote exception when registering sensorId: " + props.sensorId);
+        }
+    }
+
+    @Override
+    protected void invokeRegisteredCallback(
+            @NonNull IFingerprintAuthenticatorsRegisteredCallback callback,
+            @NonNull List<FingerprintSensorPropertiesInternal> allProps) throws RemoteException {
+        callback.onAllAuthenticatorsRegistered(allProps);
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
index 275d7e4..9075e7e 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java
@@ -28,14 +28,11 @@
 import android.hardware.fingerprint.ISidefpsController;
 import android.hardware.fingerprint.IUdfpsOverlayController;
 import android.os.IBinder;
-import android.util.proto.ProtoOutputStream;
 
+import com.android.server.biometrics.sensors.BiometricServiceProvider;
 import com.android.server.biometrics.sensors.ClientMonitorCallback;
 import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
-import com.android.server.biometrics.sensors.LockoutTracker;
 
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
 import java.util.List;
 
 /**
@@ -59,23 +56,8 @@
  * fail safely.
  */
 @SuppressWarnings("deprecation")
-public interface ServiceProvider {
-    /**
-     * Checks if the specified sensor is owned by this provider.
-     */
-    boolean containsSensor(int sensorId);
-
-    @NonNull
-    List<FingerprintSensorPropertiesInternal> getSensorProperties();
-
-    /**
-     * Returns the internal properties of the specified sensor, if owned by this provider.
-     *
-     * @param sensorId The ID of a fingerprint sensor, or -1 for any sensor.
-     * @return An object representing the internal properties of the specified sensor.
-     */
-    @Nullable
-    FingerprintSensorPropertiesInternal getSensorProperties(int sensorId);
+public interface ServiceProvider extends
+        BiometricServiceProvider<FingerprintSensorPropertiesInternal> {
 
     void scheduleResetLockout(int sensorId, int userId, @Nullable byte[] hardwareAuthToken);
 
@@ -126,16 +108,11 @@
     void scheduleInternalCleanup(int sensorId, int userId,
             @Nullable ClientMonitorCallback callback, boolean favorHalEnrollments);
 
-    boolean isHardwareDetected(int sensorId);
-
     void rename(int sensorId, int fingerId, int userId, @NonNull String name);
 
     @NonNull
     List<Fingerprint> getEnrolledFingerprints(int sensorId, int userId);
 
-    @LockoutTracker.LockoutMode
-    int getLockoutModeForUser(int sensorId, int userId);
-
     /**
      * Requests for the authenticatorId (whose source of truth is in the TEE or equivalent) to
      * be invalidated. See {@link com.android.server.biometrics.sensors.InvalidationRequesterClient}
@@ -143,7 +120,6 @@
     void scheduleInvalidateAuthenticatorId(int sensorId, int userId,
             @NonNull IInvalidationCallback callback);
 
-    long getAuthenticatorId(int sensorId, int userId);
 
     void onPointerDown(long requestId, int sensorId, int x, int y, float minor, float major);
 
@@ -161,13 +137,6 @@
      */
     void setSidefpsController(@NonNull ISidefpsController controller);
 
-    void dumpProtoState(int sensorId, @NonNull ProtoOutputStream proto,
-            boolean clearSchedulerBuffer);
-
-    void dumpProtoMetrics(int sensorId, @NonNull FileDescriptor fd);
-
-    void dumpInternal(int sensorId, @NonNull PrintWriter pw);
-
     @NonNull
     ITestSession createTestSession(int sensorId, @NonNull ITestSessionCallback callback,
             @NonNull String opPackageName);
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 2dc00520..3fe6332 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
@@ -565,6 +565,11 @@
     }
 
     @Override
+    public boolean hasEnrollments(int sensorId, int userId) {
+        return !getEnrolledFingerprints(sensorId, userId).isEmpty();
+    }
+
+    @Override
     public void scheduleInvalidateAuthenticatorId(int sensorId, int userId,
             @NonNull IInvalidationCallback callback) {
         mHandler.post(() -> {
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 ed482f0..0e6df8e 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
@@ -789,6 +789,11 @@
     }
 
     @Override
+    public boolean hasEnrollments(int sensorId, int userId) {
+        return !getEnrolledFingerprints(sensorId, userId).isEmpty();
+    }
+
+    @Override
     @LockoutTracker.LockoutMode public int getLockoutModeForUser(int sensorId, int userId) {
         return mLockoutTracker.getLockoutModeForUser(userId);
     }
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 16a060a..931c692 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -752,7 +752,7 @@
         return true;
     }
 
-    private boolean sendEventToVpnManagerApp(@NonNull String category, int errorClass,
+    private Intent buildVpnManagerEventIntent(@NonNull String category, int errorClass,
             int errorCode, @NonNull final String packageName, @Nullable final String sessionKey,
             @NonNull final VpnProfileState profileState, @Nullable final Network underlyingNetwork,
             @Nullable final NetworkCapabilities nc, @Nullable final LinkProperties lp) {
@@ -771,6 +771,20 @@
             intent.putExtra(VpnManager.EXTRA_ERROR_CODE, errorCode);
         }
 
+        return intent;
+    }
+
+    private boolean sendEventToVpnManagerApp(@NonNull String category, int errorClass,
+            int errorCode, @NonNull final String packageName, @Nullable final String sessionKey,
+            @NonNull final VpnProfileState profileState, @Nullable final Network underlyingNetwork,
+            @Nullable final NetworkCapabilities nc, @Nullable final LinkProperties lp) {
+        final Intent intent = buildVpnManagerEventIntent(category, errorClass, errorCode,
+                packageName, sessionKey, profileState, underlyingNetwork, nc, lp);
+        return sendEventToVpnManagerApp(intent, packageName);
+    }
+
+    private boolean sendEventToVpnManagerApp(@NonNull final Intent intent,
+            @NonNull final String packageName) {
         // Allow VpnManager app to temporarily run background services to handle this error.
         // If an app requires anything beyond this grace period, they MUST either declare
         // themselves as a foreground service, or schedule a job/workitem.
@@ -1182,12 +1196,25 @@
                 mContext.unbindService(mConnection);
                 cleanupVpnStateLocked();
             } else if (mVpnRunner != null) {
-                if (!VpnConfig.LEGACY_VPN.equals(mPackage)) {
-                    notifyVpnManagerVpnStopped(mPackage, mOwnerUID);
+                // Build intent first because the sessionKey will be reset after performing
+                // VpnRunner.exit(). Also, cache mOwnerUID even if ownerUID will not be changed in
+                // VpnRunner.exit() to prevent design being changed in the future.
+                // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
+                //  ConnectivityServiceTest.
+                final int ownerUid = mOwnerUID;
+                Intent intent = null;
+                if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) {
+                    intent = buildVpnManagerEventIntent(
+                            VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
+                            -1 /* errorClass */, -1 /* errorCode*/, mPackage,
+                            getSessionKeyLocked(), makeVpnProfileStateLocked(),
+                            null /* underlyingNetwork */, null /* nc */, null /* lp */);
                 }
-
                 // cleanupVpnStateLocked() is called from mVpnRunner.exit()
                 mVpnRunner.exit();
+                if (intent != null && isVpnApp(mPackage)) {
+                    notifyVpnManagerVpnStopped(mPackage, ownerUid, intent);
+                }
             }
 
             try {
@@ -2886,6 +2913,9 @@
                 final LinkProperties lp;
 
                 synchronized (Vpn.this) {
+                    // Ignore stale runner.
+                    if (mVpnRunner != this) return;
+
                     mInterface = interfaceName;
                     mConfig.mtu = maxMtu;
                     mConfig.interfaze = mInterface;
@@ -2987,6 +3017,9 @@
 
             try {
                 synchronized (Vpn.this) {
+                    // Ignore stale runner.
+                    if (mVpnRunner != this) return;
+
                     mConfig.underlyingNetworks = new Network[] {network};
                     mNetworkCapabilities =
                             new NetworkCapabilities.Builder(mNetworkCapabilities)
@@ -3076,7 +3109,12 @@
 
                 // Clear mInterface to prevent Ikev2VpnRunner being cleared when
                 // interfaceRemoved() is called.
-                mInterface = null;
+                synchronized (Vpn.this) {
+                    // Ignore stale runner.
+                    if (mVpnRunner != this) return;
+
+                    mInterface = null;
+                }
                 // Without MOBIKE, we have no way to seamlessly migrate. Close on old
                 // (non-default) network, and start the new one.
                 resetIkeState();
@@ -3261,6 +3299,9 @@
         /** Marks the state as FAILED, and disconnects. */
         private void markFailedAndDisconnect(Exception exception) {
             synchronized (Vpn.this) {
+                // Ignore stale runner.
+                if (mVpnRunner != this) return;
+
                 updateState(DetailedState.FAILED, exception.getMessage());
             }
 
@@ -3345,6 +3386,9 @@
             }
 
             synchronized (Vpn.this) {
+                // Ignore stale runner.
+                if (mVpnRunner != this) return;
+
                 // TODO(b/230548427): Remove SDK check once VPN related stuff are
                 //  decoupled from ConnectivityServiceTest.
                 if (SdkLevel.isAtLeastT() && category != null && isVpnApp(mPackage)) {
@@ -3371,6 +3415,9 @@
             Log.d(TAG, "Resetting state for token: " + mCurrentToken);
 
             synchronized (Vpn.this) {
+                // Ignore stale runner.
+                if (mVpnRunner != this) return;
+
                 // Since this method handles non-fatal errors only, set mInterface to null to
                 // prevent the NetworkManagementEventObserver from killing this VPN based on the
                 // interface going down (which we expect).
@@ -3993,6 +4040,7 @@
             mConfig.proxyInfo = profile.proxy;
             mConfig.requiresInternetValidation = profile.requiresInternetValidation;
             mConfig.excludeLocalRoutes = profile.excludeLocalRoutes;
+            mConfig.allowBypass = profile.isBypassable;
 
             switch (profile.type) {
                 case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
@@ -4042,13 +4090,23 @@
         // To stop the VPN profile, the caller must be the current prepared package and must be
         // running an Ikev2VpnProfile.
         if (isCurrentIkev2VpnLocked(packageName)) {
-            notifyVpnManagerVpnStopped(packageName, mOwnerUID);
+            // Build intent first because the sessionKey will be reset after performing
+            // VpnRunner.exit(). Also, cache mOwnerUID even if ownerUID will not be changed in
+            // VpnRunner.exit() to prevent design being changed in the future.
+            final int ownerUid = mOwnerUID;
+            final Intent intent = buildVpnManagerEventIntent(
+                    VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
+                    -1 /* errorClass */, -1 /* errorCode*/, packageName,
+                    getSessionKeyLocked(), makeVpnProfileStateLocked(),
+                    null /* underlyingNetwork */, null /* nc */, null /* lp */);
 
             mVpnRunner.exit();
+            notifyVpnManagerVpnStopped(packageName, ownerUid, intent);
         }
     }
 
-    private synchronized void notifyVpnManagerVpnStopped(String packageName, int ownerUID) {
+    private synchronized void notifyVpnManagerVpnStopped(String packageName, int ownerUID,
+            Intent intent) {
         mAppOpsManager.finishOp(
                 AppOpsManager.OPSTR_ESTABLISH_VPN_MANAGER, ownerUID, packageName, null);
         // The underlying network, NetworkCapabilities and LinkProperties are not
@@ -4057,10 +4115,7 @@
         // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
         //  ConnectivityServiceTest.
         if (SdkLevel.isAtLeastT()) {
-            sendEventToVpnManagerApp(VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
-                    -1 /* errorClass */, -1 /* errorCode*/, packageName,
-                    getSessionKeyLocked(), makeVpnProfileStateLocked(),
-                    null /* underlyingNetwork */, null /* nc */, null /* lp */);
+            sendEventToVpnManagerApp(intent, packageName);
         }
     }
 
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 8807e19..2476350 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -813,13 +813,7 @@
                 mDisplayStatsId = mUniqueDisplayId.hashCode();
                 mDisplayDeviceConfig = config;
                 loadFromDisplayDeviceConfig(token, info);
-                if (DEBUG) {
-                    Trace.beginAsyncSection("DisplayPowerController#updatePowerState", 0);
-                }
                 updatePowerState();
-                if (DEBUG) {
-                    Trace.endAsyncSection("DisplayPowerController#updatePowerState", 0);
-                }
             }
         });
     }
@@ -1147,6 +1141,16 @@
     }
 
     private void updatePowerState() {
+        if (DEBUG) {
+            Trace.beginSection("DisplayPowerController#updatePowerState");
+        }
+        updatePowerStateInternal();
+        if (DEBUG) {
+            Trace.endSection();
+        }
+    }
+
+    private void updatePowerStateInternal() {
         // Update the power state request.
         final boolean mustNotify;
         final int previousPolicy;
@@ -1645,11 +1649,11 @@
         mTempBrightnessEvent.setHbmMax(mHbmController.getCurrentBrightnessMax());
         mTempBrightnessEvent.setHbmMode(mHbmController.getHighBrightnessMode());
         mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()
-                | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0));
-        mTempBrightnessEvent.setRbcStrength((mCdsi != null && mCdsi.isReduceBrightColorsActivated())
+                | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)
+                | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0));
+        mTempBrightnessEvent.setRbcStrength(mCdsi != null
                 ? mCdsi.getReduceBrightColorsStrength() : -1);
-        mTempBrightnessEvent.setPowerFactor(
-                mPowerRequest.lowPowerMode ? mPowerRequest.screenLowPowerBrightnessFactor : 1.0f);
+        mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor);
         // Temporary is what we use during slider interactions. We avoid logging those so that
         // we don't spam logcat when the slider is being used.
         boolean tempToTempTransition =
@@ -1662,10 +1666,10 @@
             mTempBrightnessEvent.setInitialBrightness(lastBrightness);
             mTempBrightnessEvent.setFastAmbientLux(
                     mAutomaticBrightnessController == null
-                        ? -1 : mAutomaticBrightnessController.getFastAmbientLux());
+                        ? -1f : mAutomaticBrightnessController.getFastAmbientLux());
             mTempBrightnessEvent.setSlowAmbientLux(
                     mAutomaticBrightnessController == null
-                        ? -1 : mAutomaticBrightnessController.getSlowAmbientLux());
+                        ? -1f : mAutomaticBrightnessController.getSlowAmbientLux());
             mTempBrightnessEvent.setAutomaticBrightnessEnabled(mPowerRequest.useAutoBrightness);
             mLastBrightnessEvent.copyFrom(mTempBrightnessEvent);
             BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent);
@@ -2769,12 +2773,13 @@
     }
 
     private void logManualBrightnessEvent(BrightnessEvent event) {
-        float hbmMaxNits =
+        float appliedLowPowerMode = event.isLowPowerModeSet() ? event.getPowerFactor() : -1f;
+        int appliedRbcStrength  = event.isRbcEnabled() ? event.getRbcStrength() : -1;
+        float appliedHbmMaxNits =
                 event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF
                 ? -1f : convertToNits(event.getHbmMax());
-
         // thermalCapNits set to -1 if not currently capping max brightness
-        float thermalCapNits =
+        float appliedThermalCapNits =
                 event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
                 ? -1f : convertToNits(event.getThermalMax());
 
@@ -2784,10 +2789,10 @@
                 event.getSlowAmbientLux(),
                 event.getPhysicalDisplayId(),
                 event.isShortTermModelActive(),
-                event.getPowerFactor(),
-                event.getRbcStrength(),
-                hbmMaxNits,
-                thermalCapNits,
+                appliedLowPowerMode,
+                appliedRbcStrength,
+                appliedHbmMaxNits,
+                appliedThermalCapNits,
                 event.isAutomaticBrightnessEnabled(),
                 FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED__REASON__REASON_MANUAL);
     }
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
index 70415a3..e3fa622 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
@@ -21,6 +21,8 @@
 import android.os.SystemClock;
 import android.util.TimeUtils;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 /**
  * Represents a particular brightness change event.
  */
@@ -29,7 +31,8 @@
     public static final int FLAG_INVALID_LUX = 0x2;
     public static final int FLAG_DOZE_SCALE = 0x4;
     public static final int FLAG_USER_SET = 0x8;
-    public static final int FLAG_IDLE_CURVE = 0x16;
+    public static final int FLAG_IDLE_CURVE = 0x10;
+    public static final int FLAG_LOW_POWER_MODE = 0x20;
 
     private BrightnessReason mReason = new BrightnessReason();
     private int mDisplayId;
@@ -317,6 +320,10 @@
         this.mRbcStrength = mRbcStrength;
     }
 
+    public boolean isRbcEnabled() {
+        return (mFlags & FLAG_RBC) != 0;
+    }
+
     public float getThermalMax() {
         return mThermalMax;
     }
@@ -333,6 +340,10 @@
         this.mPowerFactor = mPowerFactor;
     }
 
+    public boolean isLowPowerModeSet() {
+        return (mFlags & FLAG_LOW_POWER_MODE) != 0;
+    }
+
     public int getFlags() {
         return mFlags;
     }
@@ -361,11 +372,17 @@
         this.mAutomaticBrightnessEnabled = mAutomaticBrightnessEnabled;
     }
 
-    private String flagsToString() {
+    /**
+     * A utility to stringify flags from a BrightnessEvent
+     * @return Stringified flags from BrightnessEvent
+     */
+    @VisibleForTesting
+    public String flagsToString() {
         return ((mFlags & FLAG_USER_SET) != 0 ? "user_set " : "")
                 + ((mFlags & FLAG_RBC) != 0 ? "rbc " : "")
                 + ((mFlags & FLAG_INVALID_LUX) != 0 ? "invalid_lux " : "")
                 + ((mFlags & FLAG_DOZE_SCALE) != 0 ? "doze_scale " : "")
-                + ((mFlags & FLAG_IDLE_CURVE) != 0 ? "idle_curve " : "");
+                + ((mFlags & FLAG_IDLE_CURVE) != 0 ? "idle_curve " : "")
+                + ((mFlags & FLAG_LOW_POWER_MODE) != 0 ? "low_power_mode " : "");
     }
 }
diff --git a/services/core/java/com/android/server/hdmi/CecMessageBuffer.java b/services/core/java/com/android/server/hdmi/CecMessageBuffer.java
index 8f971fd..0c73582 100644
--- a/services/core/java/com/android/server/hdmi/CecMessageBuffer.java
+++ b/services/core/java/com/android/server/hdmi/CecMessageBuffer.java
@@ -48,6 +48,12 @@
             case Constants.MESSAGE_SYSTEM_AUDIO_MODE_REQUEST:
                 bufferSystemAudioModeRequest(message);
                 return true;
+            case Constants.MESSAGE_ROUTING_CHANGE:
+                bufferRoutingChange(message);
+                return true;
+            case Constants.MESSAGE_SET_STREAM_PATH:
+                bufferSetStreamPath(message);
+                return true;
             // Add here if new message that needs to buffer
             default:
                 // Do not need to buffer messages other than above
@@ -89,6 +95,22 @@
         }
     }
 
+    private void bufferRoutingChange(HdmiCecMessage message) {
+        if (!replaceMessageIfBuffered(message, Constants.MESSAGE_ROUTING_CHANGE)) {
+            mBuffer.add(message);
+        }
+    }
+
+    private void bufferSetStreamPath(HdmiCecMessage message) {
+        if (!replaceMessageIfBuffered(message, Constants.MESSAGE_SET_STREAM_PATH)) {
+            mBuffer.add(message);
+        }
+    }
+
+    public List<HdmiCecMessage> getBuffer() {
+        return new ArrayList<>(mBuffer);
+    }
+
     // Returns true if the message is replaced
     private boolean replaceMessageIfBuffered(HdmiCecMessage message, int opcode) {
         for (int i = 0; i < mBuffer.size(); i++) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecController.java b/services/core/java/com/android/server/hdmi/HdmiCecController.java
index 97e9c64..5aa3fa4 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecController.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecController.java
@@ -147,6 +147,9 @@
 
     private final HdmiCecAtomWriter mHdmiCecAtomWriter;
 
+    // This variable is used for testing, in order to delay the logical address allocation.
+    private long mLogicalAddressAllocationDelay = 0;
+
     // Private constructor.  Use HdmiCecController.create().
     private HdmiCecController(
             HdmiControlService service, NativeWrapper nativeWrapper, HdmiCecAtomWriter atomWriter) {
@@ -215,12 +218,12 @@
             final AllocateAddressCallback callback) {
         assertRunOnServiceThread();
 
-        runOnIoThread(new Runnable() {
+        mIoHandler.postDelayed(new Runnable() {
             @Override
             public void run() {
                 handleAllocateLogicalAddress(deviceType, preferredAddress, callback);
             }
-        });
+        }, mLogicalAddressAllocationDelay);
     }
 
     /**
@@ -386,6 +389,14 @@
     }
 
     /**
+     * This method is used for testing, in order to delay the logical address allocation.
+     */
+    @VisibleForTesting
+    void setLogicalAddressAllocationDelay(long delay) {
+        mLogicalAddressAllocationDelay = delay;
+    }
+
+    /**
      * Returns true if the language code is well-formed.
      */
     @VisibleForTesting static boolean isLanguage(String language) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index 1de1a7a..2622cef 100755
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -649,6 +649,13 @@
         return Constants.NOT_HANDLED;
     }
 
+    /**
+     * Called after logical address allocation is finished, allowing a local device to react to
+     * messages in the buffer before they are processed. This method may be used to cancel deferred
+     * actions.
+     */
+    protected void preprocessBufferedMessages(List<HdmiCecMessage> bufferedMessages) {}
+
     @Constants.RcProfile
     protected abstract int getRcProfile();
 
@@ -963,8 +970,10 @@
     }
 
     @ServiceThreadOnly
-    final void handleAddressAllocated(int logicalAddress, int reason) {
+    final void handleAddressAllocated(
+            int logicalAddress, List<HdmiCecMessage> bufferedMessages, int reason) {
         assertRunOnServiceThread();
+        preprocessBufferedMessages(bufferedMessages);
         mPreferredAddress = logicalAddress;
         updateDeviceFeatures();
         if (mService.getCecVersion() >= HdmiControlManager.HDMI_CEC_VERSION_2_0) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
index 5cfe27a..ea54b30 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevicePlayback.java
@@ -530,6 +530,25 @@
         }
     }
 
+    /**
+     * Called after logical address allocation is finished, allowing a local device to react to
+     * messages in the buffer before they are processed. This method may be used to cancel deferred
+     * actions.
+     */
+    @Override
+    protected void preprocessBufferedMessages(List<HdmiCecMessage> bufferedMessages) {
+        for (HdmiCecMessage message: bufferedMessages) {
+            // Prevent the device from broadcasting <Active Source> message if the active path
+            // changed during address allocation.
+            if (message.getOpcode() == Constants.MESSAGE_ROUTING_CHANGE
+                    || message.getOpcode() == Constants.MESSAGE_SET_STREAM_PATH
+                    || message.getOpcode() == Constants.MESSAGE_ACTIVE_SOURCE) {
+                removeAction(ActiveSourceAction.class);
+                return;
+            }
+        }
+    }
+
     @Override
     protected int findKeyReceiverAddress() {
         return Constants.ADDR_TV;
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 6c37bf1..fa8b5c1 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1079,9 +1079,10 @@
     @ServiceThreadOnly
     private void notifyAddressAllocated(ArrayList<HdmiCecLocalDevice> devices, int initiatedBy) {
         assertRunOnServiceThread();
+        List<HdmiCecMessage> bufferedMessages = mCecMessageBuffer.getBuffer();
         for (HdmiCecLocalDevice device : devices) {
             int address = device.getDeviceInfo().getLogicalAddress();
-            device.handleAddressAllocated(address, initiatedBy);
+            device.handleAddressAllocated(address, bufferedMessages, initiatedBy);
         }
         if (isTvDeviceEnabled()) {
             tv().setSelectRequestBuffer(mSelectRequestBuffer);
@@ -1361,8 +1362,9 @@
 
         @Constants.HandleMessageResult int handleMessageResult =
                 dispatchMessageToLocalDevice(message);
-        if (handleMessageResult == Constants.NOT_HANDLED
-                && !mAddressAllocated
+        // mAddressAllocated is false during address allocation, meaning there is no device to
+        // handle the message, so it should be buffered, if possible.
+        if (!mAddressAllocated
                 && mCecMessageBuffer.bufferMessage(message)) {
             return Constants.HANDLED;
         }
@@ -3286,7 +3288,8 @@
     }
 
     @ServiceThreadOnly
-    private void onWakeUp(@WakeReason final int wakeUpAction) {
+    @VisibleForTesting
+    protected void onWakeUp(@WakeReason final int wakeUpAction) {
         assertRunOnServiceThread();
         mPowerStatusController.setPowerStatus(HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON,
                 false);
diff --git a/services/core/java/com/android/server/location/LocationPermissions.java b/services/core/java/com/android/server/location/LocationPermissions.java
index ca2ff60..f7da0d8 100644
--- a/services/core/java/com/android/server/location/LocationPermissions.java
+++ b/services/core/java/com/android/server/location/LocationPermissions.java
@@ -26,8 +26,10 @@
 import android.content.Context;
 import android.os.Binder;
 
+import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 /** Utility class for dealing with location permissions. */
 public final class LocationPermissions {
@@ -49,6 +51,7 @@
      */
     public static final int PERMISSION_FINE = 2;
 
+    @Target(ElementType.TYPE_USE)
     @IntDef({PERMISSION_NONE, PERMISSION_COARSE, PERMISSION_FINE})
     @Retention(RetentionPolicy.SOURCE)
     public @interface PermissionLevel {}
diff --git a/services/core/java/com/android/server/location/geofence/GeofenceKey.java b/services/core/java/com/android/server/location/geofence/GeofenceKey.java
deleted file mode 100644
index bbfa68f..0000000
--- a/services/core/java/com/android/server/location/geofence/GeofenceKey.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2020 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.location.geofence;
-
-import android.app.PendingIntent;
-import android.location.Geofence;
-
-import com.android.server.location.listeners.PendingIntentListenerRegistration;
-
-import java.util.Objects;
-
-// geofencing unfortunately allows multiple geofences under the same pending intent, even though
-// this makes no real sense. therefore we manufacture an artificial key to use (pendingintent +
-// geofence) instead of (pendingintent).
-final class GeofenceKey  implements PendingIntentListenerRegistration.PendingIntentKey {
-
-    private final PendingIntent mPendingIntent;
-    private final Geofence mGeofence;
-
-    GeofenceKey(PendingIntent pendingIntent, Geofence geofence) {
-        mPendingIntent = Objects.requireNonNull(pendingIntent);
-        mGeofence = Objects.requireNonNull(geofence);
-    }
-
-    @Override
-    public PendingIntent getPendingIntent() {
-        return mPendingIntent;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (!(o instanceof GeofenceKey)) {
-            return false;
-        }
-        GeofenceKey that = (GeofenceKey) o;
-        return mPendingIntent.equals(that.mPendingIntent) && mGeofence.equals(that.mGeofence);
-    }
-
-    @Override
-    public int hashCode() {
-        return mPendingIntent.hashCode();
-    }
-}
diff --git a/services/core/java/com/android/server/location/geofence/GeofenceManager.java b/services/core/java/com/android/server/location/geofence/GeofenceManager.java
index b6342a4..0f5e3d4 100644
--- a/services/core/java/com/android/server/location/geofence/GeofenceManager.java
+++ b/services/core/java/com/android/server/location/geofence/GeofenceManager.java
@@ -59,8 +59,8 @@
  * Manages all geofences.
  */
 public class GeofenceManager extends
-        ListenerMultiplexer<GeofenceKey, PendingIntent, GeofenceManager.GeofenceRegistration,
-                LocationRequest> implements
+        ListenerMultiplexer<GeofenceManager.GeofenceKey, PendingIntent,
+                GeofenceManager.GeofenceRegistration, LocationRequest> implements
         LocationListener {
 
     private static final String TAG = "GeofenceManager";
@@ -73,13 +73,49 @@
     private static final long MAX_LOCATION_AGE_MS = 5 * 60 * 1000L; // five minutes
     private static final long MAX_LOCATION_INTERVAL_MS = 2 * 60 * 60 * 1000; // two hours
 
-    protected final class GeofenceRegistration extends
-            PendingIntentListenerRegistration<Geofence, PendingIntent> {
+    // geofencing unfortunately allows multiple geofences under the same pending intent, even though
+    // this makes no real sense. therefore we manufacture an artificial key to use (pendingintent +
+    // geofence) instead of (pendingintent).
+    static class GeofenceKey {
+
+        private final PendingIntent mPendingIntent;
+        private final Geofence mGeofence;
+
+        GeofenceKey(PendingIntent pendingIntent, Geofence geofence) {
+            mPendingIntent = Objects.requireNonNull(pendingIntent);
+            mGeofence = Objects.requireNonNull(geofence);
+        }
+
+        public PendingIntent getPendingIntent() {
+            return mPendingIntent;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof GeofenceKey) {
+                GeofenceKey that = (GeofenceKey) o;
+                return mPendingIntent.equals(that.mPendingIntent) && mGeofence.equals(
+                        that.mGeofence);
+            }
+
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return mPendingIntent.hashCode();
+        }
+    }
+
+    protected class GeofenceRegistration extends
+            PendingIntentListenerRegistration<GeofenceKey, PendingIntent> {
 
         private static final int STATE_UNKNOWN = 0;
         private static final int STATE_INSIDE = 1;
         private static final int STATE_OUTSIDE = 2;
 
+        private final Geofence mGeofence;
+        private final CallerIdentity mIdentity;
         private final Location mCenter;
         private final PowerManager.WakeLock mWakeLock;
 
@@ -89,13 +125,15 @@
         // spam us, and because checking the values may be more expensive
         private boolean mPermitted;
 
-        private @Nullable Location mCachedLocation;
+        @Nullable private Location mCachedLocation;
         private float mCachedLocationDistanceM;
 
-        protected GeofenceRegistration(Geofence geofence, CallerIdentity identity,
+        GeofenceRegistration(Geofence geofence, CallerIdentity identity,
                 PendingIntent pendingIntent) {
-            super(geofence, identity, pendingIntent);
+            super(pendingIntent);
 
+            mGeofence = geofence;
+            mIdentity = identity;
             mCenter = new Location("");
             mCenter.setLatitude(geofence.getLatitude());
             mCenter.setLongitude(geofence.getLongitude());
@@ -107,16 +145,36 @@
             mWakeLock.setWorkSource(identity.addToWorkSource(null));
         }
 
+        public Geofence getGeofence() {
+            return mGeofence;
+        }
+
+        public CallerIdentity getIdentity() {
+            return mIdentity;
+        }
+
+        @Override
+        public String getTag() {
+            return TAG;
+        }
+
+        @Override
+        protected PendingIntent getPendingIntentFromKey(GeofenceKey geofenceKey) {
+            return geofenceKey.getPendingIntent();
+        }
+
         @Override
         protected GeofenceManager getOwner() {
             return GeofenceManager.this;
         }
 
         @Override
-        protected void onPendingIntentListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             mGeofenceState = STATE_UNKNOWN;
             mPermitted = mLocationPermissionsHelper.hasLocationPermissions(PERMISSION_FINE,
-                    getIdentity());
+                    mIdentity);
         }
 
         @Override
@@ -132,7 +190,7 @@
         }
 
         boolean onLocationPermissionsChanged(@Nullable String packageName) {
-            if (packageName == null || getIdentity().getPackageName().equals(packageName)) {
+            if (packageName == null || mIdentity.getPackageName().equals(packageName)) {
                 return onLocationPermissionsChanged();
             }
 
@@ -140,7 +198,7 @@
         }
 
         boolean onLocationPermissionsChanged(int uid) {
-            if (getIdentity().getUid() == uid) {
+            if (mIdentity.getUid() == uid) {
                 return onLocationPermissionsChanged();
             }
 
@@ -149,7 +207,7 @@
 
         private boolean onLocationPermissionsChanged() {
             boolean permitted = mLocationPermissionsHelper.hasLocationPermissions(PERMISSION_FINE,
-                    getIdentity());
+                    mIdentity);
             if (permitted != mPermitted) {
                 mPermitted = permitted;
                 return true;
@@ -164,12 +222,12 @@
                 mCachedLocationDistanceM = mCenter.distanceTo(mCachedLocation);
             }
 
-            return Math.abs(getRequest().getRadius() - mCachedLocationDistanceM);
+            return Math.abs(mGeofence.getRadius() - mCachedLocationDistanceM);
         }
 
         ListenerOperation<PendingIntent> onLocationChanged(Location location) {
             // remove expired fences
-            if (getRequest().isExpired()) {
+            if (mGeofence.isExpired()) {
                 remove();
                 return null;
             }
@@ -178,7 +236,7 @@
             mCachedLocationDistanceM = mCenter.distanceTo(mCachedLocation);
 
             int oldState = mGeofenceState;
-            float radius = Math.max(getRequest().getRadius(), location.getAccuracy());
+            float radius = Math.max(mGeofence.getRadius(), location.getAccuracy());
             if (mCachedLocationDistanceM <= radius) {
                 mGeofenceState = STATE_INSIDE;
                 if (oldState != STATE_INSIDE) {
@@ -206,14 +264,14 @@
                         null, null, PendingIntentUtils.createDontSendToRestrictedAppsBundle(null));
             } catch (PendingIntent.CanceledException e) {
                 mWakeLock.release();
-                removeRegistration(new GeofenceKey(pendingIntent, getRequest()), this);
+                removeRegistration(new GeofenceKey(pendingIntent, mGeofence), this);
             }
         }
 
         @Override
         public String toString() {
             StringBuilder builder = new StringBuilder();
-            builder.append(getIdentity());
+            builder.append(mIdentity);
 
             ArraySet<String> flags = new ArraySet<>(1);
             if (!mPermitted) {
@@ -223,7 +281,7 @@
                 builder.append(" ").append(flags);
             }
 
-            builder.append(" ").append(getRequest());
+            builder.append(" ").append(mGeofence);
             return builder.toString();
         }
     }
@@ -258,10 +316,10 @@
     protected final LocationUsageLogger mLocationUsageLogger;
 
     @GuardedBy("mLock")
-    private @Nullable LocationManager mLocationManager;
+    @Nullable private LocationManager mLocationManager;
 
     @GuardedBy("mLock")
-    private @Nullable Location mLastLocation;
+    @Nullable private Location mLastLocation;
 
     public GeofenceManager(Context context, Injector injector) {
         mContext = context.createAttributionContext(ATTRIBUTION_TAG);
@@ -271,11 +329,6 @@
         mLocationUsageLogger = injector.getLocationUsageLogger();
     }
 
-    @Override
-    public String getTag() {
-        return TAG;
-    }
-
     private LocationManager getLocationManager() {
         synchronized (mLock) {
             if (mLocationManager == null) {
@@ -375,7 +428,7 @@
                 /* LocationRequest= */ null,
                 /* hasListener= */ false,
                 true,
-                registration.getRequest(), true);
+                registration.getGeofence(), true);
     }
 
     @Override
@@ -389,7 +442,7 @@
                 /* LocationRequest= */ null,
                 /* hasListener= */ false,
                 true,
-                registration.getRequest(), true);
+                registration.getGeofence(), true);
     }
 
     @Override
@@ -417,7 +470,7 @@
         WorkSource workSource = null;
         double minFenceDistanceM = Double.MAX_VALUE;
         for (GeofenceRegistration registration : registrations) {
-            if (registration.getRequest().isExpired(realtimeMs)) {
+            if (registration.getGeofence().isExpired(realtimeMs)) {
                 continue;
             }
 
diff --git a/services/core/java/com/android/server/location/gnss/GnssAntennaInfoProvider.java b/services/core/java/com/android/server/location/gnss/GnssAntennaInfoProvider.java
index e375007..62ab22a 100644
--- a/services/core/java/com/android/server/location/gnss/GnssAntennaInfoProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssAntennaInfoProvider.java
@@ -16,6 +16,7 @@
 
 package com.android.server.location.gnss;
 
+import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
 import static com.android.server.location.gnss.GnssManagerService.TAG;
 
 import android.annotation.Nullable;
@@ -25,6 +26,7 @@
 import android.os.Binder;
 import android.os.IBinder;
 
+import com.android.server.FgThread;
 import com.android.server.location.gnss.hal.GnssNative;
 import com.android.server.location.listeners.BinderListenerRegistration;
 import com.android.server.location.listeners.ListenerMultiplexer;
@@ -45,17 +47,35 @@
      * Registration object for GNSS listeners.
      */
     protected class AntennaInfoListenerRegistration extends
-            BinderListenerRegistration<Void, IGnssAntennaInfoListener> {
+            BinderListenerRegistration<IBinder, IGnssAntennaInfoListener> {
 
-        protected AntennaInfoListenerRegistration(CallerIdentity callerIdentity,
+        private final CallerIdentity mIdentity;
+
+        protected AntennaInfoListenerRegistration(CallerIdentity identity,
                 IGnssAntennaInfoListener listener) {
-            super(null, callerIdentity, listener);
+            super(identity.isMyProcess() ? FgThread.getExecutor() : DIRECT_EXECUTOR, listener);
+            mIdentity = identity;
+        }
+
+        @Override
+        protected String getTag() {
+            return TAG;
         }
 
         @Override
         protected GnssAntennaInfoProvider getOwner() {
             return GnssAntennaInfoProvider.this;
         }
+
+        @Override
+        protected IBinder getBinderFromKey(IBinder key) {
+            return key;
+        }
+
+        @Override
+        public String toString() {
+            return mIdentity.toString();
+        }
     }
 
     private final GnssNative mGnssNative;
@@ -72,11 +92,6 @@
         return mAntennaInfos;
     }
 
-    @Override
-    public String getTag() {
-        return TAG;
-    }
-
     public boolean isSupported() {
         return mGnssNative.isAntennaInfoSupported();
     }
diff --git a/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java b/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java
index a540476..82bcca2 100644
--- a/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java
+++ b/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java
@@ -18,6 +18,7 @@
 
 import static android.location.LocationManager.GPS_PROVIDER;
 
+import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
 import static com.android.server.location.LocationPermissions.PERMISSION_FINE;
 import static com.android.server.location.gnss.GnssManagerService.TAG;
 
@@ -33,6 +34,7 @@
 import android.util.ArraySet;
 
 import com.android.internal.util.Preconditions;
+import com.android.server.FgThread;
 import com.android.server.LocalServices;
 import com.android.server.location.injector.AppForegroundHelper;
 import com.android.server.location.injector.Injector;
@@ -67,16 +69,34 @@
      * Registration object for GNSS listeners.
      */
     protected class GnssListenerRegistration extends
-            BinderListenerRegistration<TRequest, TListener> {
+            BinderListenerRegistration<IBinder, TListener> {
+
+        private final TRequest mRequest;
+        private final CallerIdentity mIdentity;
 
         // we store these values because we don't trust the listeners not to give us dupes, not to
         // spam us, and because checking the values may be more expensive
         private boolean mForeground;
         private boolean mPermitted;
 
-        protected GnssListenerRegistration(@Nullable TRequest request,
-                CallerIdentity callerIdentity, TListener listener) {
-            super(request, callerIdentity, listener);
+        protected GnssListenerRegistration(TRequest request, CallerIdentity identity,
+                TListener listener) {
+            super(identity.isMyProcess() ? FgThread.getExecutor() : DIRECT_EXECUTOR, listener);
+            mRequest = request;
+            mIdentity = identity;
+        }
+
+        public final TRequest getRequest() {
+            return mRequest;
+        }
+
+        public final CallerIdentity getIdentity() {
+            return mIdentity;
+        }
+
+        @Override
+        public String getTag() {
+            return TAG;
         }
 
         @Override
@@ -84,6 +104,11 @@
             return GnssListenerMultiplexer.this;
         }
 
+        @Override
+        protected IBinder getBinderFromKey(IBinder key) {
+            return key;
+        }
+
         /**
          * Returns true if this registration is currently in the foreground.
          */
@@ -96,31 +121,16 @@
         }
 
         @Override
-        protected final void onBinderListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             mPermitted = mLocationPermissionsHelper.hasLocationPermissions(PERMISSION_FINE,
-                    getIdentity());
-            mForeground = mAppForegroundHelper.isAppForeground(getIdentity().getUid());
-
-            onGnssListenerRegister();
+                    mIdentity);
+            mForeground = mAppForegroundHelper.isAppForeground(mIdentity.getUid());
         }
 
-        @Override
-        protected final void onBinderListenerUnregister() {
-            onGnssListenerUnregister();
-        }
-
-        /**
-         * May be overridden in place of {@link #onBinderListenerRegister()}.
-         */
-        protected void onGnssListenerRegister() {}
-
-        /**
-         * May be overridden in place of {@link #onBinderListenerUnregister()}.
-         */
-        protected void onGnssListenerUnregister() {}
-
         boolean onLocationPermissionsChanged(@Nullable String packageName) {
-            if (packageName == null || getIdentity().getPackageName().equals(packageName)) {
+            if (packageName == null || mIdentity.getPackageName().equals(packageName)) {
                 return onLocationPermissionsChanged();
             }
 
@@ -128,7 +138,7 @@
         }
 
         boolean onLocationPermissionsChanged(int uid) {
-            if (getIdentity().getUid() == uid) {
+            if (mIdentity.getUid() == uid) {
                 return onLocationPermissionsChanged();
             }
 
@@ -137,7 +147,7 @@
 
         private boolean onLocationPermissionsChanged() {
             boolean permitted = mLocationPermissionsHelper.hasLocationPermissions(PERMISSION_FINE,
-                    getIdentity());
+                    mIdentity);
             if (permitted != mPermitted) {
                 mPermitted = permitted;
                 return true;
@@ -147,7 +157,7 @@
         }
 
         boolean onForegroundChanged(int uid, boolean foreground) {
-            if (getIdentity().getUid() == uid && foreground != mForeground) {
+            if (mIdentity.getUid() == uid && foreground != mForeground) {
                 mForeground = foreground;
                 return true;
             }
@@ -158,7 +168,7 @@
         @Override
         public String toString() {
             StringBuilder builder = new StringBuilder();
-            builder.append(getIdentity());
+            builder.append(mIdentity);
 
             ArraySet<String> flags = new ArraySet<>(2);
             if (!mForeground) {
@@ -171,8 +181,8 @@
                 builder.append(" ").append(flags);
             }
 
-            if (getRequest() != null) {
-                builder.append(" ").append(getRequest());
+            if (mRequest != null) {
+                builder.append(" ").append(mRequest);
             }
             return builder.toString();
         }
@@ -218,11 +228,6 @@
                 LocalServices.getService(LocationManagerInternal.class));
     }
 
-    @Override
-    public String getTag() {
-        return TAG;
-    }
-
     /**
      * May be overridden by subclasses to return whether the service is supported or not. This value
      * should never change for the lifetime of the multiplexer. If the service is unsupported, all
diff --git a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
index e4e9d01..9f2a9cf 100644
--- a/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssMeasurementsProvider.java
@@ -40,10 +40,7 @@
 import java.util.Collection;
 
 /**
- * An base implementation for GNSS measurements provider. It abstracts out the responsibility of
- * handling listeners, while still allowing technology specific implementations to be built.
- *
- * @hide
+ * GNSS measurements HAL module and listener multiplexer.
  */
 public final class GnssMeasurementsProvider extends
         GnssListenerMultiplexer<GnssMeasurementRequest, IGnssMeasurementsListener,
@@ -61,7 +58,9 @@
         }
 
         @Override
-        protected void onGnssListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             executeOperation(listener -> listener.onStatusChanged(
                     GnssMeasurementsEvent.Callback.STATUS_READY));
         }
diff --git a/services/core/java/com/android/server/location/gnss/GnssNavigationMessageProvider.java b/services/core/java/com/android/server/location/gnss/GnssNavigationMessageProvider.java
index e9fce05..63134bb 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNavigationMessageProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNavigationMessageProvider.java
@@ -32,11 +32,7 @@
 import java.util.Collection;
 
 /**
- * An base implementation for GPS navigation messages provider.
- * It abstracts out the responsibility of handling listeners, while still allowing technology
- * specific implementations to be built.
- *
- * @hide
+ * GNSS navigation message HAL module and listener multiplexer.
  */
 public class GnssNavigationMessageProvider extends
         GnssListenerMultiplexer<Void, IGnssNavigationMessageListener, Void> implements
@@ -51,7 +47,9 @@
         }
 
         @Override
-        protected void onGnssListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             executeOperation(listener -> listener.onStatusChanged(
                     GnssNavigationMessage.Callback.STATUS_READY));
         }
diff --git a/services/core/java/com/android/server/location/gnss/GnssNmeaProvider.java b/services/core/java/com/android/server/location/gnss/GnssNmeaProvider.java
index bfef978..d4e38b6a 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNmeaProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNmeaProvider.java
@@ -34,7 +34,7 @@
 import java.util.function.Function;
 
 /**
- * Implementation of a handler for {@link IGnssNmeaListener}.
+ * GNSS NMEA HAL module and listener multiplexer.
  */
 class GnssNmeaProvider extends GnssListenerMultiplexer<Void, IGnssNmeaListener, Void> implements
         GnssNative.BaseCallbacks, GnssNative.NmeaCallbacks {
@@ -97,7 +97,7 @@
                         ListenerExecutor.ListenerOperation<IGnssNmeaListener>>() {
 
                     // only read in the nmea string if we need to
-                    private @Nullable String mNmea;
+                    @Nullable private String mNmea;
 
                     @Override
                     public ListenerExecutor.ListenerOperation<IGnssNmeaListener> apply(
diff --git a/services/core/java/com/android/server/location/gnss/GnssStatusProvider.java b/services/core/java/com/android/server/location/gnss/GnssStatusProvider.java
index 0ce36d6..41fa7a1 100644
--- a/services/core/java/com/android/server/location/gnss/GnssStatusProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssStatusProvider.java
@@ -35,7 +35,7 @@
 import java.util.Collection;
 
 /**
- * Implementation of a handler for {@link IGnssStatusListener}.
+ * GNSS status HAL module and listener multiplexer.
  */
 public class GnssStatusProvider extends
         GnssListenerMultiplexer<Void, IGnssStatusListener, Void> implements
diff --git a/services/core/java/com/android/server/location/listeners/BinderListenerRegistration.java b/services/core/java/com/android/server/location/listeners/BinderListenerRegistration.java
index 709e236..5555aeb 100644
--- a/services/core/java/com/android/server/location/listeners/BinderListenerRegistration.java
+++ b/services/core/java/com/android/server/location/listeners/BinderListenerRegistration.java
@@ -16,71 +16,59 @@
 
 package com.android.server.location.listeners;
 
-import android.annotation.Nullable;
-import android.location.util.identity.CallerIdentity;
-import android.os.Binder;
 import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
 import android.os.RemoteException;
 import android.util.Log;
 
+import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
+
 /**
  * A registration that works with IBinder keys, and registers a DeathListener to automatically
- * remove the registration if the binder dies. The key for this registration must either be an
- * {@link IBinder} or a {@link BinderKey}.
+ * remove the registration if the binder dies.
  *
- * @param <TRequest>  request type
+ * @param <TKey>      key type
  * @param <TListener> listener type
  */
-public abstract class BinderListenerRegistration<TRequest, TListener> extends
-        RemoteListenerRegistration<TRequest, TListener> implements Binder.DeathRecipient {
+public abstract class BinderListenerRegistration<TKey, TListener> extends
+        RemovableListenerRegistration<TKey, TListener> implements DeathRecipient {
 
-    /**
-     * Interface to allow binder retrieval when keys are not themselves IBinders.
-     */
-    public interface BinderKey {
-        /**
-         * Returns the binder associated with this key.
-         */
-        IBinder getBinder();
+    protected BinderListenerRegistration(Executor executor, TListener listener) {
+        super(executor, listener);
     }
 
-    protected BinderListenerRegistration(@Nullable TRequest request, CallerIdentity callerIdentity,
-            TListener listener) {
-        super(request, callerIdentity, listener);
-    }
+    protected abstract IBinder getBinderFromKey(TKey key);
 
     @Override
-    protected final void onRemovableListenerRegister() {
-        IBinder binder = getBinderFromKey(getKey());
+    protected void onRegister() {
+        super.onRegister();
+
         try {
-            binder.linkToDeath(this, 0);
+            getBinderFromKey(getKey()).linkToDeath(this, 0);
         } catch (RemoteException e) {
             remove();
         }
-
-        onBinderListenerRegister();
     }
 
     @Override
-    protected final void onRemovableListenerUnregister() {
-        onBinderListenerUnregister();
-        getBinderFromKey(getKey()).unlinkToDeath(this, 0);
+    protected void onUnregister() {
+        try {
+            getBinderFromKey(getKey()).unlinkToDeath(this, 0);
+        } catch (NoSuchElementException e) {
+            // the only way this exception can occur should be if another exception has been thrown
+            // prior to registration completing, and that exception is currently unwinding the call
+            // stack and causing this cleanup. since that exception should crash us anyways, drop
+            // this exception so we're not hiding the original exception.
+            Log.w(getTag(), "failed to unregister binder death listener", e);
+        }
+
+        super.onUnregister();
     }
 
-    /**
-     * May be overridden in place of {@link #onRemovableListenerRegister()}.
-     */
-    protected void onBinderListenerRegister() {}
-
-    /**
-     * May be overridden in place of {@link #onRemovableListenerUnregister()}.
-     */
-    protected void onBinderListenerUnregister() {}
-
-    @Override
     public void onOperationFailure(ListenerOperation<TListener> operation, Exception e) {
         if (e instanceof RemoteException) {
-            Log.w(getOwner().getTag(), "registration " + this + " removed", e);
+            Log.w(getTag(), "registration " + this + " removed", e);
             remove();
         } else {
             super.onOperationFailure(operation, e);
@@ -90,9 +78,10 @@
     @Override
     public void binderDied() {
         try {
-            if (Log.isLoggable(getOwner().getTag(), Log.DEBUG)) {
-                Log.d(getOwner().getTag(), "binder registration " + getIdentity() + " died");
+            if (Log.isLoggable(getTag(), Log.DEBUG)) {
+                Log.d(getTag(), "binder registration " + this + " died");
             }
+
             remove();
         } catch (RuntimeException e) {
             // the caller may swallow runtime exceptions, so we rethrow as assertion errors to
@@ -100,14 +89,4 @@
             throw new AssertionError(e);
         }
     }
-
-    private static IBinder getBinderFromKey(Object key) {
-        if (key instanceof IBinder) {
-            return (IBinder) key;
-        } else if (key instanceof BinderKey) {
-            return ((BinderKey) key).getBinder();
-        } else {
-            throw new IllegalArgumentException("key must be IBinder or BinderKey");
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java b/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java
index 33b08d4..67ae265 100644
--- a/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java
+++ b/services/core/java/com/android/server/location/listeners/ListenerMultiplexer.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.os.Build;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
@@ -37,40 +36,48 @@
 import java.util.function.Predicate;
 
 /**
- * A base class to multiplex client listener registrations within system server. Every listener is
+ * A base class to multiplex some event source to multiple listener registrations. Every listener is
  * represented by a registration object which stores all required state for a listener. Keys are
  * used to uniquely identify every registration. Listener operations may be executed on
  * registrations in order to invoke the represented listener.
  *
- * Registrations are divided into two categories, active registrations and inactive registrations,
- * as defined by {@link #isActive(ListenerRegistration)}. If a registration's active state changes,
- * {@link #updateRegistrations(Predicate)} must be invoked and return true for any registration
- * whose active state may have changed. Listeners will only be invoked for active registrations.
+ * <p>Registrations are divided into two categories, active registrations and inactive
+ * registrations, as defined by {@link #isActive(ListenerRegistration)}. The set of active
+ * registrations is combined into a single merged registration, which is submitted to the backing
+ * event source when necessary in order to register with the event source. The merged registration
+ * is updated whenever the set of active registration changes. Listeners will only be invoked for
+ * active registrations.
  *
- * The set of active registrations is combined into a single merged registration, which is submitted
- * to the backing service when necessary in order to register the service. The merged registration
- * is updated whenever the set of active registration changes.
+ * <p>In order to inform the multiplexer of state changes, if a registration's active state changes,
+ * or if the merged registration changes, {@link #updateRegistrations(Predicate)} or {@link
+ * #updateRegistration(Object, Predicate)} must be invoked and return true for any registration
+ * whose state may have changed in such a way that the active state or merged registration state has
+ * changed. It is acceptable to return true from a predicate even if nothing has changed, though
+ * this may result in extra pointless work.
  *
- * Callbacks invoked for various changes will always be ordered according to this lifecycle list:
+ * <p>Callbacks invoked for various changes will always be ordered according to this lifecycle list:
  *
  * <ul>
- * <li>{@link #onRegister()}</li>
- * <li>{@link ListenerRegistration#onRegister(Object)}</li>
- * <li>{@link #onRegistrationAdded(Object, ListenerRegistration)}</li>
- * <li>{@link #onRegistrationReplaced(Object, ListenerRegistration, ListenerRegistration)} (only
- * invoked if this registration is replacing a prior registration)</li>
- * <li>{@link #onActive()}</li>
- * <li>{@link ListenerRegistration#onActive()}</li>
- * <li>{@link ListenerRegistration#onInactive()}</li>
- * <li>{@link #onInactive()}</li>
- * <li>{@link #onRegistrationRemoved(Object, ListenerRegistration)}</li>
- * <li>{@link ListenerRegistration#onUnregister()}</li>
- * <li>{@link #onUnregister()}</li>
+ *   <li>{@link #onRegister()}
+ *   <li>{@link ListenerRegistration#onRegister(Object)}
+ *   <li>{@link #onRegistrationAdded(Object, ListenerRegistration)}
+ *   <li>{@link #onActive()}
+ *   <li>{@link ListenerRegistration#onActive()}
+ *   <li>{@link ListenerRegistration#onInactive()}
+ *   <li>{@link #onInactive()}
+ *   <li>{@link #onRegistrationRemoved(Object, ListenerRegistration)}
+ *   <li>{@link ListenerRegistration#onUnregister()}
+ *   <li>{@link #onUnregister()}
  * </ul>
  *
- * Adding registrations is not allowed to be called re-entrantly (ie, while in the middle of some
- * other operation or callback. Removal is allowed re-entrantly, however only via
- * {@link #removeRegistration(Object, ListenerRegistration)}, not via any other removal method. This
+ * <p>If one registration replaces another, then {@link #onRegistrationReplaced(Object,
+ * ListenerRegistration, Object, ListenerRegistration)} is invoked instead of {@link
+ * #onRegistrationRemoved(Object, ListenerRegistration)} and {@link #onRegistrationAdded(Object,
+ * ListenerRegistration)}.
+ *
+ * <p>Adding registrations is not allowed to be called re-entrantly (ie, while in the middle of some
+ * other operation or callback). Removal is allowed re-entrantly, however only via {@link
+ * #removeRegistration(Object, ListenerRegistration)}, not via any other removal method. This
  * ensures re-entrant removal does not accidentally remove the incorrect registration.
  *
  * @param <TKey>                key type
@@ -81,30 +88,31 @@
 public abstract class ListenerMultiplexer<TKey, TListener,
         TRegistration extends ListenerRegistration<TListener>, TMergedRegistration> {
 
-    @GuardedBy("mRegistrations")
+    /**
+     * The lock object used by the multiplexer. Acquiring this lock allows for multiple operations
+     * on the multiplexer to be completed atomically. Otherwise, it is not required to hold this
+     * lock. This lock is held while invoking all lifecycle callbacks on both the multiplexer and
+     * any registrations.
+     */
+    protected final Object mMultiplexerLock = new Object();
+
+    @GuardedBy("mMultiplexerLock")
     private final ArrayMap<TKey, TRegistration> mRegistrations = new ArrayMap<>();
 
-    @GuardedBy("mRegistrations")
     private final UpdateServiceBuffer mUpdateServiceBuffer = new UpdateServiceBuffer();
 
-    @GuardedBy("mRegistrations")
     private final ReentrancyGuard mReentrancyGuard = new ReentrancyGuard();
 
-    @GuardedBy("mRegistrations")
+    @GuardedBy("mMultiplexerLock")
     private int mActiveRegistrationsCount = 0;
 
-    @GuardedBy("mRegistrations")
+    @GuardedBy("mMultiplexerLock")
     private boolean mServiceRegistered = false;
 
-    @GuardedBy("mRegistrations")
+    @GuardedBy("mMultiplexerLock")
     @Nullable private TMergedRegistration mMerged;
 
     /**
-     * Should be implemented to return a unique identifying tag that may be used for logging, etc...
-     */
-    public abstract @NonNull String getTag();
-
-    /**
      * Should be implemented to register with the backing service with the given merged
      * registration, and should return true if a matching call to {@link #unregisterWithService()}
      * is required to unregister (ie, if registration succeeds). The set of registrations passed in
@@ -120,6 +128,7 @@
      * @see #mergeRegistrations(Collection)
      * @see #reregisterWithService(Object, Object, Collection)
      */
+    @GuardedBy("mMultiplexerLock")
     protected abstract boolean registerWithService(TMergedRegistration merged,
             @NonNull Collection<TRegistration> registrations);
 
@@ -130,6 +139,7 @@
      *
      * @see #registerWithService(Object, Collection)
      */
+    @GuardedBy("mMultiplexerLock")
     protected boolean reregisterWithService(TMergedRegistration oldMerged,
             TMergedRegistration newMerged, @NonNull Collection<TRegistration> registrations) {
         return registerWithService(newMerged, registrations);
@@ -138,6 +148,7 @@
     /**
      * Should be implemented to unregister from the backing service.
      */
+    @GuardedBy("mMultiplexerLock")
     protected abstract void unregisterWithService();
 
     /**
@@ -147,6 +158,7 @@
      * {@link #updateRegistrations(Predicate)} must be invoked with a function that returns true for
      * any registrations that may have changed their active state.
      */
+    @GuardedBy("mMultiplexerLock")
     protected abstract boolean isActive(@NonNull TRegistration registration);
 
     /**
@@ -157,7 +169,8 @@
      * {@link #reregisterWithService(Object, Object, Collection)} will be invoked with the new
      * merged registration so that the backing service can be updated.
      */
-    protected abstract @Nullable TMergedRegistration mergeRegistrations(
+    @GuardedBy("mMultiplexerLock")
+    protected abstract TMergedRegistration mergeRegistrations(
             @NonNull Collection<TRegistration> registrations);
 
     /**
@@ -166,6 +179,7 @@
      * present while there are any registrations. Invoked while holding the multiplexer's internal
      * lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onRegister() {}
 
     /**
@@ -174,28 +188,38 @@
      * present while there are any registrations. Invoked while holding the multiplexer's internal
      * lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onUnregister() {}
 
     /**
      * Invoked when a registration is added. Invoked while holding the multiplexer's internal lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onRegistrationAdded(@NonNull TKey key, @NonNull TRegistration registration) {}
 
     /**
-     * Invoked instead of {@link #onRegistrationAdded(Object, ListenerRegistration)} if a
-     * registration is replacing an old registration. The old registration will have already been
-     * unregistered. Invoked while holding the multiplexer's internal lock. The default behavior is
-     * simply to call into {@link #onRegistrationAdded(Object, ListenerRegistration)}.
+     * Invoked when one registration replaces another (through {@link #replaceRegistration(Object,
+     * Object, ListenerRegistration)}). The old registration has already been unregistered at this
+     * point. Invoked while holding the multiplexer's internal lock.
+     *
+     * <p>The default behavior is simply to call first {@link #onRegistrationRemoved(Object,
+     * ListenerRegistration)} and then {@link #onRegistrationAdded(Object, ListenerRegistration)}.
      */
-    protected void onRegistrationReplaced(@NonNull TKey key, @NonNull TRegistration oldRegistration,
+    @GuardedBy("mMultiplexerLock")
+    protected void onRegistrationReplaced(
+            @NonNull TKey oldKey,
+            @NonNull TRegistration oldRegistration,
+            @NonNull TKey newKey,
             @NonNull TRegistration newRegistration) {
-        onRegistrationAdded(key, newRegistration);
+        onRegistrationRemoved(oldKey, oldRegistration);
+        onRegistrationAdded(newKey, newRegistration);
     }
 
     /**
      * Invoked when a registration is removed. Invoked while holding the multiplexer's internal
      * lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onRegistrationRemoved(@NonNull TKey key, @NonNull TRegistration registration) {}
 
     /**
@@ -204,6 +228,7 @@
      * need to be present while there are active registrations. Invoked while holding the
      * multiplexer's internal lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onActive() {}
 
     /**
@@ -212,6 +237,7 @@
      * need to be present while there are active registrations. Invoked while holding the
      * multiplexer's internal lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected void onInactive() {}
 
     /**
@@ -224,13 +250,12 @@
 
     /**
      * Atomically removes the registration with the old key and adds a new registration with the
-     * given key. If there was a registration for the old key,
-     * {@link #onRegistrationReplaced(Object, ListenerRegistration, ListenerRegistration)} will be
-     * invoked for the new registration and key instead of
-     * {@link #onRegistrationAdded(Object, ListenerRegistration)}, even though they may not share
-     * the same key. The old key may be the same value as the new key, in which case this function
-     * is equivalent to {@link #putRegistration(Object, ListenerRegistration)}. This method cannot
-     * be called to add a registration re-entrantly.
+     * given key. If there was a registration for the old key, {@link
+     * #onRegistrationReplaced(Object, ListenerRegistration, Object, ListenerRegistration)} will be
+     * invoked instead of {@link #onRegistrationAdded(Object, ListenerRegistration)}, even if they
+     * share the same key. The old key may be the same value as the new key, in which case this
+     * function is equivalent to {@link #putRegistration(Object, ListenerRegistration)}. This method
+     * cannot be called to add a registration re-entrantly.
      */
     protected final void replaceRegistration(@NonNull TKey oldKey, @NonNull TKey key,
             @NonNull TRegistration registration) {
@@ -238,7 +263,7 @@
         Objects.requireNonNull(key);
         Objects.requireNonNull(registration);
 
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             // adding listeners reentrantly is not supported
             Preconditions.checkState(!mReentrancyGuard.isReentrant());
 
@@ -257,12 +282,18 @@
                 boolean wasEmpty = mRegistrations.isEmpty();
 
                 TRegistration oldRegistration = null;
-                int index = mRegistrations.indexOfKey(oldKey);
-                if (index >= 0) {
-                    oldRegistration = removeRegistration(index, oldKey != key);
+                int oldIndex = mRegistrations.indexOfKey(oldKey);
+                if (oldIndex >= 0) {
+                    // remove ourselves instead of using remove(), to balance registration callbacks
+                    oldRegistration = mRegistrations.valueAt(oldIndex);
+                    unregister(oldRegistration);
+                    oldRegistration.onUnregister();
+                    if (oldKey != key) {
+                        mRegistrations.removeAt(oldIndex);
+                    }
                 }
-                if (oldKey == key && index >= 0) {
-                    mRegistrations.setValueAt(index, registration);
+                if (oldKey == key && oldIndex >= 0) {
+                    mRegistrations.setValueAt(oldIndex, registration);
                 } else {
                     mRegistrations.put(key, registration);
                 }
@@ -274,7 +305,7 @@
                 if (oldRegistration == null) {
                     onRegistrationAdded(key, registration);
                 } else {
-                    onRegistrationReplaced(key, oldRegistration, registration);
+                    onRegistrationReplaced(oldKey, oldRegistration, key, registration);
                 }
                 onRegistrationActiveChanged(registration);
             }
@@ -282,29 +313,11 @@
     }
 
     /**
-     * Removes the registration with the given key. This method cannot be called to remove a
-     * registration re-entrantly.
-     */
-    protected final void removeRegistration(@NonNull Object key) {
-        synchronized (mRegistrations) {
-            // this method does not support removing listeners reentrantly
-            Preconditions.checkState(!mReentrancyGuard.isReentrant());
-
-            int index = mRegistrations.indexOfKey(key);
-            if (index < 0) {
-                return;
-            }
-
-            removeRegistration(index, true);
-        }
-    }
-
-    /**
      * Removes all registrations with keys that satisfy the given predicate. This method cannot be
      * called to remove a registration re-entrantly.
      */
     protected final void removeRegistrationIf(@NonNull Predicate<TKey> predicate) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             // this method does not support removing listeners reentrantly
             Preconditions.checkState(!mReentrancyGuard.isReentrant());
 
@@ -329,13 +342,31 @@
     }
 
     /**
+     * Removes the registration with the given key. This method cannot be called to remove a
+     * registration re-entrantly.
+     */
+    protected final void removeRegistration(TKey key) {
+        synchronized (mMultiplexerLock) {
+            // this method does not support removing listeners reentrantly
+            Preconditions.checkState(!mReentrancyGuard.isReentrant());
+
+            int index = mRegistrations.indexOfKey(key);
+            if (index < 0) {
+                return;
+            }
+
+            removeRegistration(index);
+        }
+    }
+
+    /**
      * Removes the given registration with the given key. If the given key has a different
      * registration at the time this method is called, nothing happens. This method allows for
      * re-entrancy, and may be called to remove a registration re-entrantly.
      */
-    protected final void removeRegistration(@NonNull Object key,
+    protected final void removeRegistration(@NonNull TKey key,
             @NonNull ListenerRegistration<?> registration) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             int index = mRegistrations.indexOfKey(key);
             if (index < 0) {
                 return;
@@ -350,17 +381,13 @@
                 unregister(typedRegistration);
                 mReentrancyGuard.markForRemoval(key, typedRegistration);
             } else {
-                removeRegistration(index, true);
+                removeRegistration(index);
             }
         }
     }
 
-    @GuardedBy("mRegistrations")
-    private TRegistration removeRegistration(int index, boolean removeEntry) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mRegistrations));
-        }
-
+    @GuardedBy("mMultiplexerLock")
+    private void removeRegistration(int index) {
         TKey key = mRegistrations.keyAt(index);
         TRegistration registration = mRegistrations.valueAt(index);
 
@@ -376,15 +403,11 @@
             unregister(registration);
             onRegistrationRemoved(key, registration);
             registration.onUnregister();
-            if (removeEntry) {
-                mRegistrations.removeAt(index);
-                if (mRegistrations.isEmpty()) {
-                    onUnregister();
-                }
+            mRegistrations.removeAt(index);
+            if (mRegistrations.isEmpty()) {
+                onUnregister();
             }
         }
-
-        return registration;
     }
 
     /**
@@ -392,14 +415,14 @@
      * registration accordingly.
      */
     protected final void updateService() {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             if (mUpdateServiceBuffer.isBuffered()) {
                 mUpdateServiceBuffer.markUpdateServiceRequired();
                 return;
             }
 
-            ArrayList<TRegistration> actives = new ArrayList<>(mRegistrations.size());
             final int size = mRegistrations.size();
+            ArrayList<TRegistration> actives = new ArrayList<>(size);
             for (int i = 0; i < size; i++) {
                 TRegistration registration = mRegistrations.valueAt(i);
                 if (registration.isActive()) {
@@ -413,17 +436,17 @@
                     mServiceRegistered = false;
                     unregisterWithService();
                 }
-                return;
-            }
-
-            TMergedRegistration merged = mergeRegistrations(actives);
-            if (!mServiceRegistered || !Objects.equals(merged, mMerged)) {
+            } else {
+                TMergedRegistration merged = mergeRegistrations(actives);
                 if (mServiceRegistered) {
-                    mServiceRegistered = reregisterWithService(mMerged, merged, actives);
+                    if (!Objects.equals(merged, mMerged)) {
+                        mServiceRegistered = reregisterWithService(mMerged, merged, actives);
+                        mMerged = mServiceRegistered ? merged : null;
+                    }
                 } else {
                     mServiceRegistered = registerWithService(merged, actives);
+                    mMerged = mServiceRegistered ? merged : null;
                 }
-                mMerged = mServiceRegistered ? merged : null;
             }
         }
     }
@@ -437,7 +460,7 @@
      * reinitialized.
      */
     protected final void resetService() {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             if (mServiceRegistered) {
                 mMerged = null;
                 mServiceRegistered = false;
@@ -453,7 +476,31 @@
      * buffering {@code updateService()} until after multiple adds/removes/updates occur.
      */
     public UpdateServiceLock newUpdateServiceLock() {
-        return new UpdateServiceLock(mUpdateServiceBuffer.acquire());
+        return new UpdateServiceLock(mUpdateServiceBuffer);
+    }
+
+    /**
+     * Evaluates the predicate on all registrations until the predicate returns true, at which point
+     * evaluation will cease. Returns true if the predicate ever returned true, and returns false
+     * otherwise.
+     */
+    protected final boolean findRegistration(Predicate<TRegistration> predicate) {
+        synchronized (mMultiplexerLock) {
+            // we only acquire a reentrancy guard in case of removal while iterating. this method
+            // does not directly affect active state or merged state, so there is no advantage to
+            // acquiring an update source buffer.
+            try (ReentrancyGuard ignored = mReentrancyGuard.acquire()) {
+                final int size = mRegistrations.size();
+                for (int i = 0; i < size; i++) {
+                    TRegistration registration = mRegistrations.valueAt(i);
+                    if (predicate.test(registration)) {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
     }
 
     /**
@@ -463,7 +510,7 @@
      * the resulting changes.
      */
     protected final void updateRegistrations(@NonNull Predicate<TRegistration> predicate) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             // since updating a registration can invoke a variety of callbacks, we need to ensure
             // those callbacks themselves do not re-enter, as this could lead to out-of-order
             // callbacks. note that try-with-resources ordering is meaningful here as well. we want
@@ -492,7 +539,7 @@
      */
     protected final boolean updateRegistration(@NonNull Object key,
             @NonNull Predicate<TRegistration> predicate) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             // since updating a registration can invoke a variety of callbacks, we need to ensure
             // those callbacks themselves do not re-enter, as this could lead to out-of-order
             // callbacks. note that try-with-resources ordering is meaningful here as well. we want
@@ -515,12 +562,8 @@
         }
     }
 
-    @GuardedBy("mRegistrations")
+    @GuardedBy("mMultiplexerLock")
     private void onRegistrationActiveChanged(TRegistration registration) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mRegistrations));
-        }
-
         boolean active = registration.isRegistered() && isActive(registration);
         boolean changed = registration.setActive(active);
         if (changed) {
@@ -547,7 +590,7 @@
      */
     protected final void deliverToListeners(
             @NonNull Function<TRegistration, ListenerOperation<TListener>> function) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             try (ReentrancyGuard ignored = mReentrancyGuard.acquire()) {
                 final int size = mRegistrations.size();
                 for (int i = 0; i < size; i++) {
@@ -571,7 +614,7 @@
      * </pre>
      */
     protected final void deliverToListeners(@NonNull ListenerOperation<TListener> operation) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             try (ReentrancyGuard ignored = mReentrancyGuard.acquire()) {
                 final int size = mRegistrations.size();
                 for (int i = 0; i < size; i++) {
@@ -584,6 +627,7 @@
         }
     }
 
+    @GuardedBy("mMultiplexerLock")
     private void unregister(TRegistration registration) {
         registration.unregisterInternal();
         onRegistrationActiveChanged(registration);
@@ -593,7 +637,7 @@
      * Dumps debug information.
      */
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
-        synchronized (mRegistrations) {
+        synchronized (mMultiplexerLock) {
             pw.print("service: ");
             pw.print(getServiceState());
             pw.println();
@@ -620,6 +664,7 @@
      * May be overridden to provide additional details on service state when dumping the manager
      * state. Invoked while holding the multiplexer's internal lock.
      */
+    @GuardedBy("mMultiplexerLock")
     protected String getServiceState() {
         if (mServiceRegistered) {
             if (mMerged != null) {
@@ -643,61 +688,63 @@
      */
     private final class ReentrancyGuard implements AutoCloseable {
 
-        @GuardedBy("mRegistrations")
+        @GuardedBy("mMultiplexerLock")
         private int mGuardCount;
-        @GuardedBy("mRegistrations")
-        private @Nullable ArraySet<Entry<Object, ListenerRegistration<?>>> mScheduledRemovals;
+
+        @GuardedBy("mMultiplexerLock")
+        @Nullable private ArraySet<Entry<TKey, ListenerRegistration<?>>> mScheduledRemovals;
 
         ReentrancyGuard() {
             mGuardCount = 0;
             mScheduledRemovals = null;
         }
 
-        @GuardedBy("mRegistrations")
         boolean isReentrant() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mRegistrations));
+            synchronized (mMultiplexerLock) {
+                return mGuardCount != 0;
             }
-            return mGuardCount != 0;
         }
 
-        @GuardedBy("mRegistrations")
-        void markForRemoval(Object key, ListenerRegistration<?> registration) {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mRegistrations));
-            }
-            Preconditions.checkState(isReentrant());
+        void markForRemoval(TKey key, ListenerRegistration<?> registration) {
+            synchronized (mMultiplexerLock) {
+                Preconditions.checkState(isReentrant());
 
-            if (mScheduledRemovals == null) {
-                mScheduledRemovals = new ArraySet<>(mRegistrations.size());
+                if (mScheduledRemovals == null) {
+                    mScheduledRemovals = new ArraySet<>(mRegistrations.size());
+                }
+                mScheduledRemovals.add(new AbstractMap.SimpleImmutableEntry<>(key, registration));
             }
-            mScheduledRemovals.add(new AbstractMap.SimpleImmutableEntry<>(key, registration));
         }
 
         ReentrancyGuard acquire() {
-            ++mGuardCount;
-            return this;
+            synchronized (mMultiplexerLock) {
+                ++mGuardCount;
+                return this;
+            }
         }
 
         @Override
         public void close() {
-            ArraySet<Entry<Object, ListenerRegistration<?>>> scheduledRemovals = null;
+            synchronized (mMultiplexerLock) {
+                Preconditions.checkState(mGuardCount > 0);
 
-            Preconditions.checkState(mGuardCount > 0);
-            if (--mGuardCount == 0) {
-                scheduledRemovals = mScheduledRemovals;
-                mScheduledRemovals = null;
-            }
+                ArraySet<Entry<TKey, ListenerRegistration<?>>> scheduledRemovals = null;
 
-            if (scheduledRemovals == null) {
-                return;
-            }
+                if (--mGuardCount == 0) {
+                    scheduledRemovals = mScheduledRemovals;
+                    mScheduledRemovals = null;
+                }
 
-            try (UpdateServiceBuffer ignored = mUpdateServiceBuffer.acquire()) {
-                final int size = scheduledRemovals.size();
-                for (int i = 0; i < size; i++) {
-                    Entry<Object, ListenerRegistration<?>> entry = scheduledRemovals.valueAt(i);
-                    removeRegistration(entry.getKey(), entry.getValue());
+                if (scheduledRemovals == null) {
+                    return;
+                }
+
+                try (UpdateServiceBuffer ignored = mUpdateServiceBuffer.acquire()) {
+                    final int size = scheduledRemovals.size();
+                    for (int i = 0; i < size; i++) {
+                        Entry<TKey, ListenerRegistration<?>> entry = scheduledRemovals.valueAt(i);
+                        removeRegistration(entry.getKey(), entry.getValue());
+                    }
                 }
             }
         }
@@ -721,6 +768,7 @@
 
         @GuardedBy("this")
         private int mBufferCount;
+
         @GuardedBy("this")
         private boolean mUpdateServiceRequired;
 
@@ -765,18 +813,18 @@
      * {@link #close()}ed. This can be used to save work by acquiring the lock before multiple calls
      * to updateService() are expected, and closing the lock after.
      */
-    public final class UpdateServiceLock implements AutoCloseable {
+    public static final class UpdateServiceLock implements AutoCloseable {
 
-        private @Nullable UpdateServiceBuffer mUpdateServiceBuffer;
+        @Nullable private ListenerMultiplexer<?, ?, ?, ?>.UpdateServiceBuffer mUpdateServiceBuffer;
 
-        UpdateServiceLock(UpdateServiceBuffer updateServiceBuffer) {
-            mUpdateServiceBuffer = updateServiceBuffer;
+        UpdateServiceLock(ListenerMultiplexer<?, ?, ?, ?>.UpdateServiceBuffer updateServiceBuffer) {
+            mUpdateServiceBuffer = updateServiceBuffer.acquire();
         }
 
         @Override
         public void close() {
             if (mUpdateServiceBuffer != null) {
-                UpdateServiceBuffer buffer = mUpdateServiceBuffer;
+                ListenerMultiplexer<?, ?, ?, ?>.UpdateServiceBuffer buffer = mUpdateServiceBuffer;
                 mUpdateServiceBuffer = null;
                 buffer.close();
             }
diff --git a/services/core/java/com/android/server/location/listeners/ListenerRegistration.java b/services/core/java/com/android/server/location/listeners/ListenerRegistration.java
index 711dde8..fcb2a9b 100644
--- a/services/core/java/com/android/server/location/listeners/ListenerRegistration.java
+++ b/services/core/java/com/android/server/location/listeners/ListenerRegistration.java
@@ -35,7 +35,7 @@
 
     private boolean mActive;
 
-    private volatile @Nullable TListener mListener;
+    @Nullable private volatile TListener mListener;
 
     protected ListenerRegistration(Executor executor, TListener listener) {
         mExecutor = Objects.requireNonNull(executor);
@@ -43,6 +43,13 @@
         mListener = Objects.requireNonNull(listener);
     }
 
+    /**
+     * Returns a tag to use for logging. Should be overridden by subclasses.
+     */
+    protected String getTag() {
+        return "ListenerRegistration";
+    }
+
     protected final Executor getExecutor() {
         return mExecutor;
     }
@@ -50,26 +57,36 @@
     /**
      * May be overridden by subclasses. Invoked when registration occurs. Invoked while holding the
      * owning multiplexer's internal lock.
+     *
+     * <p>If overridden you must ensure the superclass method is invoked (usually as the first thing
+     * in the overridden method).
      */
     protected void onRegister(Object key) {}
 
     /**
      * May be overridden by subclasses. Invoked when unregistration occurs. Invoked while holding
      * the owning multiplexer's internal lock.
+     *
+     * <p>If overridden you must ensure the superclass method is invoked (usually as the last thing
+     * in the overridden method).
      */
     protected void onUnregister() {}
 
     /**
-     * May be overridden by subclasses. Invoked when this registration becomes active. If this
-     * returns a non-null operation, that operation will be invoked for the listener. Invoked
-     * while holding the owning multiplexer's internal lock.
+     * May be overridden by subclasses. Invoked when this registration becomes active. Invoked while
+     * holding the owning multiplexer's internal lock.
+     *
+     * <p>If overridden you must ensure the superclass method is invoked (usually as the first thing
+     * in the overridden method).
      */
     protected void onActive() {}
 
     /**
-     * May be overridden by subclasses. Invoked when registration becomes inactive. If this returns
-     * a non-null operation, that operation will be invoked for the listener. Invoked while holding
-     * the owning multiplexer's internal lock.
+     * May be overridden by subclasses. Invoked when registration becomes inactive. Invoked while
+     * holding the owning multiplexer's internal lock.
+     *
+     * <p>If overridden you must ensure the superclass method is invoked (usually as the last thing
+     * in the overridden method).
      */
     protected void onInactive() {}
 
diff --git a/services/core/java/com/android/server/location/listeners/PendingIntentListenerRegistration.java b/services/core/java/com/android/server/location/listeners/PendingIntentListenerRegistration.java
index 240ac01..c976601 100644
--- a/services/core/java/com/android/server/location/listeners/PendingIntentListenerRegistration.java
+++ b/services/core/java/com/android/server/location/listeners/PendingIntentListenerRegistration.java
@@ -16,63 +16,47 @@
 
 package com.android.server.location.listeners;
 
-import android.annotation.Nullable;
+import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
+
 import android.app.PendingIntent;
-import android.location.util.identity.CallerIdentity;
 import android.util.Log;
 
 /**
  * A registration that works with PendingIntent keys, and registers a CancelListener to
- * automatically remove the registration if the PendingIntent is canceled. The key for this
- * registration must either be a {@link PendingIntent} or a {@link PendingIntentKey}.
+ * automatically remove the registration if the PendingIntent is canceled.
  *
- * @param <TRequest>  request type
+ * @param <TKey>      key type
  * @param <TListener> listener type
  */
-public abstract class PendingIntentListenerRegistration<TRequest, TListener> extends
-        RemoteListenerRegistration<TRequest, TListener> implements PendingIntent.CancelListener {
+public abstract class PendingIntentListenerRegistration<TKey, TListener> extends
+        RemovableListenerRegistration<TKey, TListener> implements PendingIntent.CancelListener {
 
-    /**
-     * Interface to allowed pending intent retrieval when keys are not themselves PendingIntents.
-     */
-    public interface PendingIntentKey {
-        /**
-         * Returns the pending intent associated with this key.
-         */
-        PendingIntent getPendingIntent();
+    protected PendingIntentListenerRegistration(TListener listener) {
+        super(DIRECT_EXECUTOR, listener);
     }
 
-    protected PendingIntentListenerRegistration(@Nullable TRequest request,
-            CallerIdentity callerIdentity, TListener listener) {
-        super(request, callerIdentity, listener);
+    protected abstract PendingIntent getPendingIntentFromKey(TKey key);
+
+    @Override
+    protected void onRegister() {
+        super.onRegister();
+
+        if (!getPendingIntentFromKey(getKey()).addCancelListener(DIRECT_EXECUTOR, this)) {
+            remove();
+        }
     }
 
     @Override
-    protected final void onRemovableListenerRegister() {
-        getPendingIntentFromKey(getKey()).registerCancelListener(this);
-        onPendingIntentListenerRegister();
+    protected void onUnregister() {
+        getPendingIntentFromKey(getKey()).removeCancelListener(this);
+
+        super.onUnregister();
     }
 
     @Override
-    protected final void onRemovableListenerUnregister() {
-        onPendingIntentListenerUnregister();
-        getPendingIntentFromKey(getKey()).unregisterCancelListener(this);
-    }
-
-    /**
-     * May be overridden in place of {@link #onRemovableListenerRegister()}.
-     */
-    protected void onPendingIntentListenerRegister() {}
-
-    /**
-     * May be overridden in place of {@link #onRemovableListenerUnregister()}.
-     */
-    protected void onPendingIntentListenerUnregister() {}
-
-    @Override
     protected void onOperationFailure(ListenerOperation<TListener> operation, Exception e) {
         if (e instanceof PendingIntent.CanceledException) {
-            Log.w(getOwner().getTag(), "registration " + this + " removed", e);
+            Log.w(getTag(), "registration " + this + " removed", e);
             remove();
         } else {
             super.onOperationFailure(operation, e);
@@ -81,21 +65,10 @@
 
     @Override
     public void onCanceled(PendingIntent intent) {
-        if (Log.isLoggable(getOwner().getTag(), Log.DEBUG)) {
-            Log.d(getOwner().getTag(),
-                    "pending intent registration " + getIdentity() + " canceled");
+        if (Log.isLoggable(getTag(), Log.DEBUG)) {
+            Log.d(getTag(), "pending intent registration " + this + " canceled");
         }
 
         remove();
     }
-
-    private PendingIntent getPendingIntentFromKey(Object key) {
-        if (key instanceof PendingIntent) {
-            return (PendingIntent) key;
-        } else if (key instanceof PendingIntentKey) {
-            return ((PendingIntentKey) key).getPendingIntent();
-        } else {
-            throw new IllegalArgumentException("key must be PendingIntent or PendingIntentKey");
-        }
-    }
-}
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/location/listeners/RemoteListenerRegistration.java b/services/core/java/com/android/server/location/listeners/RemoteListenerRegistration.java
deleted file mode 100644
index 4eca577..0000000
--- a/services/core/java/com/android/server/location/listeners/RemoteListenerRegistration.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2020 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.location.listeners;
-
-
-import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR;
-
-import android.annotation.Nullable;
-import android.location.util.identity.CallerIdentity;
-import android.os.Process;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.FgThread;
-
-import java.util.Objects;
-import java.util.concurrent.Executor;
-
-/**
- * A listener registration representing a remote (possibly from a different process) listener.
- * Listeners from a different process will be run on a direct executor, since the x-process listener
- * invocation should already be asynchronous. Listeners from the same process will be run on a
- * normal executor, since in-process listener invocation may be synchronous.
- *
- * @param <TRequest>           request type
- * @param <TListener>          listener type
- */
-public abstract class RemoteListenerRegistration<TRequest, TListener> extends
-        RemovableListenerRegistration<TRequest, TListener> {
-
-    @VisibleForTesting
-    public static final Executor IN_PROCESS_EXECUTOR = FgThread.getExecutor();
-
-    private static Executor chooseExecutor(CallerIdentity identity) {
-        // if a client is in the same process as us, binder calls will execute synchronously and
-        // we shouldn't run callbacks directly since they might be run under lock and deadlock
-        if (identity.getPid() == Process.myPid()) {
-            // there's a slight loophole here for pending intents - pending intent callbacks can
-            // always be run on the direct executor since they're always asynchronous, but honestly
-            // you shouldn't be using pending intent callbacks within the same process anyways
-            return IN_PROCESS_EXECUTOR;
-        } else {
-            return DIRECT_EXECUTOR;
-        }
-    }
-
-    private final CallerIdentity mIdentity;
-
-    protected RemoteListenerRegistration(@Nullable TRequest request, CallerIdentity identity,
-            TListener listener) {
-        super(chooseExecutor(identity), request, listener);
-        mIdentity = Objects.requireNonNull(identity);
-    }
-
-    /**
-     * Returns the listener identity.
-     */
-    public final CallerIdentity getIdentity() {
-        return mIdentity;
-    }
-}
-
diff --git a/services/core/java/com/android/server/location/listeners/RemovableListenerRegistration.java b/services/core/java/com/android/server/location/listeners/RemovableListenerRegistration.java
index 618ff24..3c302fb 100644
--- a/services/core/java/com/android/server/location/listeners/RemovableListenerRegistration.java
+++ b/services/core/java/com/android/server/location/listeners/RemovableListenerRegistration.java
@@ -20,22 +20,23 @@
 
 import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * A listener registration that stores its own key, and thus can remove itself. By default it will
  * remove itself if any checked exception occurs on listener execution.
  *
- * @param <TRequest>           request type
+ * @param <TKey>               key type
  * @param <TListener>          listener type
  */
-public abstract class RemovableListenerRegistration<TRequest, TListener> extends
-        RequestListenerRegistration<TRequest, TListener> {
+public abstract class RemovableListenerRegistration<TKey, TListener> extends
+        ListenerRegistration<TListener> {
 
-    private volatile @Nullable Object mKey;
+    @Nullable private volatile TKey mKey;
+    private final AtomicBoolean mRemoved = new AtomicBoolean(false);
 
-    protected RemovableListenerRegistration(Executor executor, @Nullable TRequest request,
-            TListener listener) {
-        super(executor, request, listener);
+    protected RemovableListenerRegistration(Executor executor, TListener listener) {
+        super(executor, listener);
     }
 
     /**
@@ -43,46 +44,76 @@
      * with. Often this is easiest to accomplish by defining registration subclasses as non-static
      * inner classes of the multiplexer they are to be used with.
      */
-    protected abstract ListenerMultiplexer<?, ? super TListener, ?, ?> getOwner();
+    protected abstract ListenerMultiplexer<TKey, ? super TListener, ?, ?> getOwner();
 
     /**
      * Returns the key associated with this registration. May not be invoked before
      * {@link #onRegister(Object)} or after {@link #onUnregister()}.
      */
-    protected final Object getKey() {
+    protected final TKey getKey() {
         return Objects.requireNonNull(mKey);
     }
 
     /**
-     * Removes this registration. Does nothing if invoked before {@link #onRegister(Object)} or
-     * after {@link #onUnregister()}. It is safe to invoke this from within either function.
+     * Convenience method equivalent to invoking {@link #remove(boolean)} with the
+     * {@code immediately} parameter set to true.
      */
     public final void remove() {
-        Object key = mKey;
-        if (key != null) {
-            getOwner().removeRegistration(key, this);
+        remove(true);
+    }
+
+    /**
+     * Removes this registration. If the {@code immediately} parameter is true, all pending listener
+     * invocations will fail. If the {@code immediately} parameter is false, listener invocations
+     * that were scheduled before remove was invoked (including invocations scheduled within {@link
+     * #onRemove(boolean)}) will continue, but any listener invocations scheduled after remove was
+     * invoked will fail.
+     *
+     * <p>Only the first call to this method will ever go through (and so {@link #onRemove(boolean)}
+     * will only ever be invoked once).
+     *
+     * <p>Does nothing if invoked before {@link #onRegister()} or after {@link #onUnregister()}.
+     */
+    public final void remove(boolean immediately) {
+        TKey key = mKey;
+        if (key != null && !mRemoved.getAndSet(true)) {
+            onRemove(immediately);
+            if (immediately) {
+                getOwner().removeRegistration(key, this);
+            } else {
+                executeOperation(listener -> getOwner().removeRegistration(key, this));
+            }
         }
     }
 
+    /**
+     * Invoked just before this registration is removed due to {@link #remove(boolean)}, on the same
+     * thread as the responsible {@link #remove(boolean)} call.
+     *
+     * <p>This method will only ever be invoked once, no matter how many calls to {@link
+     * #remove(boolean)} are made, as any registration can only be removed once.
+     */
+    protected void onRemove(boolean immediately) {}
+
     @Override
     protected final void onRegister(Object key) {
-        mKey = Objects.requireNonNull(key);
-        onRemovableListenerRegister();
+        super.onRegister(key);
+        mKey = (TKey) Objects.requireNonNull(key);
+        onRegister();
     }
 
+    /**
+     * May be overridden by subclasses. Invoked when registration occurs. Invoked while holding the
+     * owning multiplexer's internal lock.
+     *
+     * <p>If overridden you must ensure the superclass method is invoked (usually as the first thing
+     * in the overridden method).
+     */
+    protected void onRegister() {}
+
     @Override
-    protected final void onUnregister() {
-        onRemovableListenerUnregister();
+    protected void onUnregister() {
         mKey = null;
+        super.onUnregister();
     }
-
-    /**
-     * May be overridden in place of {@link #onRegister(Object)}.
-     */
-    protected void onRemovableListenerRegister() {}
-
-    /**
-     * May be overridden in place of {@link #onUnregister()}.
-     */
-    protected void onRemovableListenerUnregister() {}
 }
diff --git a/services/core/java/com/android/server/location/listeners/RequestListenerRegistration.java b/services/core/java/com/android/server/location/listeners/RequestListenerRegistration.java
deleted file mode 100644
index 0c2fc91..0000000
--- a/services/core/java/com/android/server/location/listeners/RequestListenerRegistration.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2020 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.location.listeners;
-
-import java.util.concurrent.Executor;
-
-/**
- * A listener registration object which includes an associated request.
- *
- * @param <TRequest>           request type
- * @param <TListener>          listener type
- */
-public class RequestListenerRegistration<TRequest, TListener> extends
-        ListenerRegistration<TListener> {
-
-    private final TRequest mRequest;
-
-    protected RequestListenerRegistration(Executor executor, TRequest request,
-            TListener listener) {
-        super(executor, listener);
-        mRequest = request;
-    }
-
-    /**
-     * Returns the request associated with this listener, or null if one wasn't supplied.
-     */
-    public TRequest getRequest() {
-        return mRequest;
-    }
-
-    @Override
-    public String toString() {
-        if (mRequest == null) {
-            return "[]";
-        } else {
-            return mRequest.toString();
-        }
-    }
-}
-
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index 549fd49..a69a079 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -67,7 +67,6 @@
 import android.location.provider.ProviderRequest;
 import android.location.util.identity.CallerIdentity;
 import android.os.Binder;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.IBinder;
@@ -115,7 +114,7 @@
 import com.android.server.location.injector.UserInfoHelper;
 import com.android.server.location.injector.UserInfoHelper.UserListener;
 import com.android.server.location.listeners.ListenerMultiplexer;
-import com.android.server.location.listeners.RemoteListenerRegistration;
+import com.android.server.location.listeners.RemovableListenerRegistration;
 import com.android.server.location.settings.LocationSettings;
 import com.android.server.location.settings.LocationUserSettings;
 
@@ -124,8 +123,10 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
 import java.util.function.Predicate;
 
 /**
@@ -354,44 +355,60 @@
         public void deliverOnFlushComplete(int requestCode) {}
     }
 
-    protected abstract class Registration extends RemoteListenerRegistration<LocationRequest,
-            LocationTransport> {
+    protected abstract class Registration extends RemovableListenerRegistration<Object,
+                LocationTransport> {
 
+        private final LocationRequest mBaseRequest;
+        private final CallerIdentity mIdentity;
         private final @PermissionLevel int mPermissionLevel;
 
         // we cache these values because checking/calculating on the fly is more expensive
+        @GuardedBy("mMultiplexerLock")
         private boolean mPermitted;
+        @GuardedBy("mMultiplexerLock")
         private boolean mForeground;
+        @GuardedBy("mMultiplexerLock")
         private LocationRequest mProviderLocationRequest;
+        @GuardedBy("mMultiplexerLock")
         private boolean mIsUsingHighPower;
 
-        private @Nullable Location mLastLocation = null;
+        @Nullable private Location mLastLocation = null;
 
-        protected Registration(LocationRequest request, CallerIdentity identity,
+        protected Registration(LocationRequest request, CallerIdentity identity, Executor executor,
                 LocationTransport transport, @PermissionLevel int permissionLevel) {
-            super(Objects.requireNonNull(request), identity, transport);
+            super(executor, transport);
 
             Preconditions.checkArgument(identity.getListenerId() != null);
             Preconditions.checkArgument(permissionLevel > PERMISSION_NONE);
             Preconditions.checkArgument(!request.getWorkSource().isEmpty());
 
+            mBaseRequest = Objects.requireNonNull(request);
+            mIdentity = Objects.requireNonNull(identity);
             mPermissionLevel = permissionLevel;
             mProviderLocationRequest = request;
         }
 
-        @GuardedBy("mLock")
-        @Override
-        protected final void onRemovableListenerRegister() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
+        public final CallerIdentity getIdentity() {
+            return mIdentity;
+        }
+
+        public final LocationRequest getRequest() {
+            synchronized (mMultiplexerLock) {
+                return mProviderLocationRequest;
             }
+        }
+
+        @GuardedBy("mMultiplexerLock")
+        @Override
+        protected void onRegister() {
+            super.onRegister();
 
             if (D) {
                 Log.d(TAG, mName + " provider added registration from " + getIdentity() + " -> "
                         + getRequest());
             }
 
-            EVENT_LOG.logProviderClientRegistered(mName, getIdentity(), super.getRequest());
+            EVENT_LOG.logProviderClientRegistered(mName, getIdentity(), mBaseRequest);
 
             // initialization order is important as there are ordering dependencies
             mPermitted = mLocationPermissionsHelper.hasLocationPermissions(mPermissionLevel,
@@ -400,110 +417,72 @@
             mProviderLocationRequest = calculateProviderLocationRequest();
             mIsUsingHighPower = isUsingHighPower();
 
-            onProviderListenerRegister();
-
             if (mForeground) {
                 EVENT_LOG.logProviderClientForeground(mName, getIdentity());
             }
         }
 
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onRemovableListenerUnregister() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
-            onProviderListenerUnregister();
-
+        protected void onUnregister() {
             EVENT_LOG.logProviderClientUnregistered(mName, getIdentity());
 
             if (D) {
                 Log.d(TAG, mName + " provider removed registration from " + getIdentity());
             }
+
+            super.onUnregister();
         }
 
-        /**
-         * Subclasses may override this instead of {@link #onRemovableListenerRegister()}.
-         */
-        @GuardedBy("mLock")
-        protected void onProviderListenerRegister() {}
-
-        /**
-         * Subclasses may override this instead of {@link #onRemovableListenerUnregister()}.
-         */
-        @GuardedBy("mLock")
-        protected void onProviderListenerUnregister() {}
-
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onActive() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
+        protected void onActive() {
             EVENT_LOG.logProviderClientActive(mName, getIdentity());
 
             if (!getRequest().isHiddenFromAppOps()) {
                 mAppOpsHelper.startOpNoThrow(OP_MONITOR_LOCATION, getIdentity());
             }
             onHighPowerUsageChanged();
-
-            onProviderListenerActive();
         }
 
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onInactive() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
+        protected void onInactive() {
             onHighPowerUsageChanged();
             if (!getRequest().isHiddenFromAppOps()) {
                 mAppOpsHelper.finishOp(OP_MONITOR_LOCATION, getIdentity());
             }
 
-            onProviderListenerInactive();
-
             EVENT_LOG.logProviderClientInactive(mName, getIdentity());
         }
 
-        /**
-         * Subclasses may override this instead of {@link #onActive()}.
-         */
-        @GuardedBy("mLock")
-        protected void onProviderListenerActive() {}
-
-        /**
-         * Subclasses may override this instead of {@link #onInactive()} ()}.
-         */
-        @GuardedBy("mLock")
-        protected void onProviderListenerInactive() {}
-
-        @Override
-        public final LocationRequest getRequest() {
-            return mProviderLocationRequest;
-        }
-
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         final void setLastDeliveredLocation(@Nullable Location location) {
             mLastLocation = location;
         }
 
-        @GuardedBy("mLock")
         public final Location getLastDeliveredLocation() {
-            return mLastLocation;
+            synchronized (mMultiplexerLock) {
+                return mLastLocation;
+            }
         }
 
         public @PermissionLevel int getPermissionLevel() {
-            return mPermissionLevel;
+            synchronized (mMultiplexerLock) {
+                return mPermissionLevel;
+            }
         }
 
         public final boolean isForeground() {
-            return mForeground;
+            synchronized (mMultiplexerLock) {
+                return mForeground;
+            }
         }
 
         public final boolean isPermitted() {
-            return mPermitted;
+            synchronized (mMultiplexerLock) {
+                return mPermitted;
+            }
         }
 
         public final void flush(int requestCode) {
@@ -519,13 +498,14 @@
             return LocationProviderManager.this;
         }
 
-        @GuardedBy("mLock")
         final boolean onProviderPropertiesChanged() {
-            onHighPowerUsageChanged();
-            return false;
+            synchronized (mMultiplexerLock) {
+                onHighPowerUsageChanged();
+                return false;
+            }
         }
 
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         private void onHighPowerUsageChanged() {
             boolean isUsingHighPower = isUsingHighPower();
             if (isUsingHighPower != mIsUsingHighPower) {
@@ -541,12 +521,7 @@
             }
         }
 
-        @GuardedBy("mLock")
         private boolean isUsingHighPower() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
             ProviderProperties properties = getProperties();
             if (properties == null) {
                 return false;
@@ -557,30 +532,28 @@
                     && properties.getPowerUsage() == ProviderProperties.POWER_USAGE_HIGH;
         }
 
-        @GuardedBy("mLock")
         final boolean onLocationPermissionsChanged(@Nullable String packageName) {
-            if (packageName == null || getIdentity().getPackageName().equals(packageName)) {
-                return onLocationPermissionsChanged();
-            }
+            synchronized (mMultiplexerLock) {
+                if (packageName == null || getIdentity().getPackageName().equals(packageName)) {
+                    return onLocationPermissionsChanged();
+                }
 
-            return false;
+                return false;
+            }
         }
 
-        @GuardedBy("mLock")
         final boolean onLocationPermissionsChanged(int uid) {
-            if (getIdentity().getUid() == uid) {
-                return onLocationPermissionsChanged();
-            }
+            synchronized (mMultiplexerLock) {
+                if (getIdentity().getUid() == uid) {
+                    return onLocationPermissionsChanged();
+                }
 
-            return false;
+                return false;
+            }
         }
 
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         private boolean onLocationPermissionsChanged() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
             boolean permitted = mLocationPermissionsHelper.hasLocationPermissions(mPermissionLevel,
                     getIdentity());
             if (permitted != mPermitted) {
@@ -603,82 +576,73 @@
             return false;
         }
 
-        @GuardedBy("mLock")
         final boolean onAdasGnssLocationEnabledChanged(int userId) {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
-            if (getIdentity().getUserId() == userId) {
-                return onProviderLocationRequestChanged();
-            }
-
-            return false;
-        }
-
-        @GuardedBy("mLock")
-        final boolean onForegroundChanged(int uid, boolean foreground) {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
-            if (getIdentity().getUid() == uid && foreground != mForeground) {
-                if (D) {
-                    Log.v(TAG, mName + " provider uid " + uid + " foreground = " + foreground);
+            synchronized (mMultiplexerLock) {
+                if (getIdentity().getUserId() == userId) {
+                    return onProviderLocationRequestChanged();
                 }
 
-                mForeground = foreground;
-
-                if (mForeground) {
-                    EVENT_LOG.logProviderClientForeground(mName, getIdentity());
-                } else {
-                    EVENT_LOG.logProviderClientBackground(mName, getIdentity());
-                }
-
-                // note that onProviderLocationRequestChanged() is always called
-                return onProviderLocationRequestChanged()
-                        || mLocationPowerSaveModeHelper.getLocationPowerSaveMode()
-                        == LOCATION_MODE_FOREGROUND_ONLY;
-            }
-
-            return false;
-        }
-
-        @GuardedBy("mLock")
-        final boolean onProviderLocationRequestChanged() {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
-            LocationRequest newRequest = calculateProviderLocationRequest();
-            if (mProviderLocationRequest.equals(newRequest)) {
                 return false;
             }
-
-            LocationRequest oldRequest = mProviderLocationRequest;
-            mProviderLocationRequest = newRequest;
-            onHighPowerUsageChanged();
-            updateService();
-
-            // if bypass state has changed then the active state may have changed
-            return oldRequest.isBypass() != newRequest.isBypass();
         }
 
+        final boolean onForegroundChanged(int uid, boolean foreground) {
+            synchronized (mMultiplexerLock) {
+                if (getIdentity().getUid() == uid && foreground != mForeground) {
+                    if (D) {
+                        Log.v(TAG, mName + " provider uid " + uid + " foreground = " + foreground);
+                    }
+
+                    mForeground = foreground;
+
+                    if (mForeground) {
+                        EVENT_LOG.logProviderClientForeground(mName, getIdentity());
+                    } else {
+                        EVENT_LOG.logProviderClientBackground(mName, getIdentity());
+                    }
+
+                    // note that onProviderLocationRequestChanged() is always called
+                    return onProviderLocationRequestChanged()
+                            || mLocationPowerSaveModeHelper.getLocationPowerSaveMode()
+                            == LOCATION_MODE_FOREGROUND_ONLY;
+                }
+
+                return false;
+            }
+        }
+
+        final boolean onProviderLocationRequestChanged() {
+            synchronized (mMultiplexerLock) {
+                LocationRequest newRequest = calculateProviderLocationRequest();
+                if (mProviderLocationRequest.equals(newRequest)) {
+                    return false;
+                }
+
+                LocationRequest oldRequest = mProviderLocationRequest;
+                mProviderLocationRequest = newRequest;
+                onHighPowerUsageChanged();
+                updateService();
+
+                // if bypass state has changed then the active state may have changed
+                return oldRequest.isBypass() != newRequest.isBypass();
+            }
+        }
+
+        @GuardedBy("mMultiplexerLock")
         private LocationRequest calculateProviderLocationRequest() {
-            LocationRequest baseRequest = super.getRequest();
-            LocationRequest.Builder builder = new LocationRequest.Builder(baseRequest);
+            LocationRequest.Builder builder = new LocationRequest.Builder(mBaseRequest);
 
             if (mPermissionLevel < PERMISSION_FINE) {
                 builder.setQuality(LocationRequest.QUALITY_LOW_POWER);
-                if (baseRequest.getIntervalMillis() < MIN_COARSE_INTERVAL_MS) {
+                if (mBaseRequest.getIntervalMillis() < MIN_COARSE_INTERVAL_MS) {
                     builder.setIntervalMillis(MIN_COARSE_INTERVAL_MS);
                 }
-                if (baseRequest.getMinUpdateIntervalMillis() < MIN_COARSE_INTERVAL_MS) {
+                if (mBaseRequest.getMinUpdateIntervalMillis() < MIN_COARSE_INTERVAL_MS) {
                     builder.setMinUpdateIntervalMillis(MIN_COARSE_INTERVAL_MS);
                 }
             }
 
-            boolean locationSettingsIgnored = baseRequest.isLocationSettingsIgnored();
+            boolean locationSettingsIgnored = mBaseRequest.isLocationSettingsIgnored();
             if (locationSettingsIgnored) {
                 // if we are not currently allowed use location settings ignored, disable it
                 if (!mSettingsHelper.getIgnoreSettingsAllowlist().contains(
@@ -690,7 +654,7 @@
                 builder.setLocationSettingsIgnored(locationSettingsIgnored);
             }
 
-            boolean adasGnssBypass = baseRequest.isAdasGnssBypass();
+            boolean adasGnssBypass = mBaseRequest.isAdasGnssBypass();
             if (adasGnssBypass) {
                 // if we are not currently allowed use adas gnss bypass, disable it
                 if (!GPS_PROVIDER.equals(mName)) {
@@ -710,7 +674,7 @@
             if (!locationSettingsIgnored && !isThrottlingExempt()) {
                 // throttle in the background
                 if (!mForeground) {
-                    builder.setIntervalMillis(max(baseRequest.getIntervalMillis(),
+                    builder.setIntervalMillis(max(mBaseRequest.getIntervalMillis(),
                             mSettingsHelper.getBackgroundThrottleIntervalMs()));
                 }
             }
@@ -727,8 +691,7 @@
             return mLocationManagerInternal.isProvider(null, getIdentity());
         }
 
-        @GuardedBy("mLock")
-        abstract @Nullable ListenerOperation<LocationTransport> acceptLocationChange(
+        @Nullable abstract ListenerOperation<LocationTransport> acceptLocationChange(
                 LocationResult fineLocationResult);
 
         @Override
@@ -769,13 +732,19 @@
         final ExternalWakeLockReleaser mWakeLockReleaser;
 
         private volatile ProviderTransport mProviderTransport;
+
+        @GuardedBy("mMultiplexerLock")
         private int mNumLocationsDelivered = 0;
+        @GuardedBy("mMultiplexerLock")
         private long mExpirationRealtimeMs = Long.MAX_VALUE;
 
         protected <TTransport extends LocationTransport & ProviderTransport> LocationRegistration(
-                LocationRequest request, CallerIdentity identity, TTransport transport,
+                LocationRequest request,
+                CallerIdentity identity,
+                Executor executor,
+                TTransport transport,
                 @PermissionLevel int permissionLevel) {
-            super(request, identity, transport, permissionLevel);
+            super(request, identity, executor, transport, permissionLevel);
             mProviderTransport = transport;
             mWakeLock = Objects.requireNonNull(mContext.getSystemService(PowerManager.class))
                     .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
@@ -789,9 +758,13 @@
             mProviderTransport = null;
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onProviderListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             long registerTimeMs = SystemClock.elapsedRealtime();
             mExpirationRealtimeMs = getRequest().getExpirationRealtimeMs(registerTimeMs);
 
@@ -810,8 +783,6 @@
             // start listening for provider enabled/disabled events
             addEnabledListener(this);
 
-            onLocationListenerRegister();
-
             // if the provider is currently disabled, let the client know immediately
             int userId = getIdentity().getUserId();
             if (!isEnabled(userId)) {
@@ -819,9 +790,11 @@
             }
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onProviderListenerUnregister() {
+        protected void onUnregister() {
             // stop listening for provider enabled/disabled events
             removeEnabledListener(this);
 
@@ -830,24 +803,16 @@
                 mAlarmHelper.cancel(this);
             }
 
-            onLocationListenerUnregister();
+            super.onUnregister();
         }
 
-        /**
-         * Subclasses may override this instead of {@link #onRemovableListenerRegister()}.
-         */
-        @GuardedBy("mLock")
-        protected void onLocationListenerRegister() {}
-
-        /**
-         * Subclasses may override this instead of {@link #onRemovableListenerUnregister()}.
-         */
-        @GuardedBy("mLock")
-        protected void onLocationListenerUnregister() {}
-
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected final void onProviderListenerActive() {
+        protected void onActive() {
+            super.onActive();
+
             // a new registration may not get a location immediately, the provider request may be
             // delayed. therefore we deliver a historical location if available. since delivering an
             // older location could be considered a breaking change for some applications, we only
@@ -883,21 +848,17 @@
                         + " expired at " + TimeUtils.formatRealtime(mExpirationRealtimeMs));
             }
 
-            synchronized (mLock) {
+            synchronized (mMultiplexerLock) {
                 // no need to remove alarm after it's fired
                 mExpirationRealtimeMs = Long.MAX_VALUE;
                 remove();
             }
         }
 
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         @Override
         @Nullable ListenerOperation<LocationTransport> acceptLocationChange(
                 LocationResult fineLocationResult) {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
             // check expiration time - alarm is not guaranteed to go off at the right time,
             // especially for short intervals
             if (SystemClock.elapsedRealtime() >= mExpirationRealtimeMs) {
@@ -1017,9 +978,7 @@
                                         + " finished after " + mNumLocationsDelivered + " updates");
                             }
 
-                            synchronized (mLock) {
-                                remove();
-                            }
+                            remove();
                         }
                     }
                 }
@@ -1049,12 +1008,18 @@
 
         LocationListenerRegistration(LocationRequest request, CallerIdentity identity,
                 LocationListenerTransport transport, @PermissionLevel int permissionLevel) {
-            super(request, identity, transport, permissionLevel);
+            super(request, identity,
+                    identity.isMyProcess() ? FgThread.getExecutor() : DIRECT_EXECUTOR, transport,
+                    permissionLevel);
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onLocationListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             try {
                 ((IBinder) getKey()).linkToDeath(this, 0);
             } catch (RemoteException e) {
@@ -1062,10 +1027,22 @@
             }
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onLocationListenerUnregister() {
-            ((IBinder) getKey()).unlinkToDeath(this, 0);
+        protected void onUnregister() {
+            try {
+                ((IBinder) getKey()).unlinkToDeath(this, 0);
+            } catch (NoSuchElementException e) {
+                // the only way this exception can occur should be if another exception has been
+                // thrown prior to registration completing, and that exception is currently
+                // unwinding the call stack and causing this cleanup. since that exception should
+                // crash us anyways, drop this exception so we're not hiding the original exception.
+                Log.w(getTag(), "failed to unregister binder death listener", e);
+            }
+
+            super.onUnregister();
         }
 
         @Override
@@ -1083,9 +1060,7 @@
         private void onTransportFailure(Exception e) {
             if (e instanceof RemoteException) {
                 Log.w(TAG, mName + " provider registration " + getIdentity() + " removed", e);
-                synchronized (mLock) {
-                    remove();
-                }
+                remove();
             } else {
                 throw new AssertionError(e);
             }
@@ -1098,9 +1073,7 @@
                     Log.d(TAG, mName + " provider registration " + getIdentity() + " died");
                 }
 
-                synchronized (mLock) {
-                    remove();
-                }
+                remove();
             } catch (RuntimeException e) {
                 // the caller may swallow runtime exceptions, so we rethrow as assertion errors to
                 // ensure the crash is seen
@@ -1115,21 +1088,27 @@
         LocationPendingIntentRegistration(LocationRequest request,
                 CallerIdentity identity, LocationPendingIntentTransport transport,
                 @PermissionLevel int permissionLevel) {
-            super(request, identity, transport, permissionLevel);
+            super(request, identity, DIRECT_EXECUTOR, transport, permissionLevel);
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onLocationListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
             if (!((PendingIntent) getKey()).addCancelListener(DIRECT_EXECUTOR, this)) {
                 remove();
             }
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onLocationListenerUnregister() {
+        protected void onUnregister() {
             ((PendingIntent) getKey()).removeCancelListener(this);
+            super.onUnregister();
         }
 
         @Override
@@ -1147,9 +1126,7 @@
         private void onTransportFailure(Exception e) {
             if (e instanceof PendingIntent.CanceledException) {
                 Log.w(TAG, mName + " provider registration " + getIdentity() + " removed", e);
-                synchronized (mLock) {
-                    remove();
-                }
+                remove();
             } else {
                 throw new AssertionError(e);
             }
@@ -1161,25 +1138,32 @@
                 Log.d(TAG, mName + " provider registration " + getIdentity() + " canceled");
             }
 
-            synchronized (mLock) {
-                remove();
-            }
+            remove();
         }
     }
 
     protected final class GetCurrentLocationListenerRegistration extends Registration implements
             IBinder.DeathRecipient, OnAlarmListener {
 
+        @GuardedBy("mMultiplexerLock")
         private long mExpirationRealtimeMs = Long.MAX_VALUE;
 
         GetCurrentLocationListenerRegistration(LocationRequest request,
                 CallerIdentity identity, LocationTransport transport, int permissionLevel) {
-            super(request, identity, transport, permissionLevel);
+            super(request,
+                    identity,
+                    identity.isMyProcess() ? FgThread.getExecutor() : DIRECT_EXECUTOR,
+                    transport,
+                    permissionLevel);
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onProviderListenerRegister() {
+        protected void onRegister() {
+            super.onRegister();
+
             try {
                 ((IBinder) getKey()).linkToDeath(this, 0);
             } catch (RemoteException e) {
@@ -1202,20 +1186,36 @@
             }
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onProviderListenerUnregister() {
+        protected void onUnregister() {
             // remove alarm for expiration
             if (mExpirationRealtimeMs < Long.MAX_VALUE) {
                 mAlarmHelper.cancel(this);
             }
 
-            ((IBinder) getKey()).unlinkToDeath(this, 0);
+            try {
+                ((IBinder) getKey()).unlinkToDeath(this, 0);
+            } catch (NoSuchElementException e) {
+                // the only way this exception can occur should be if another exception has been
+                // thrown prior to registration completing, and that exception is currently
+                // unwinding the call stack and causing this cleanup. since that exception should
+                // crash us anyways, drop this exception so we're not hiding the original exception.
+                Log.w(getTag(), "failed to unregister binder death listener", e);
+            }
+
+            super.onUnregister();
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onProviderListenerActive() {
+        protected void onActive() {
+            super.onActive();
+
             Location lastLocation = getLastLocationUnsafe(
                     getIdentity().getUserId(),
                     getPermissionLevel(),
@@ -1226,17 +1226,19 @@
             }
         }
 
-        @GuardedBy("mLock")
+        // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+        @SuppressWarnings("GuardedBy")
+        @GuardedBy("mMultiplexerLock")
         @Override
-        protected void onProviderListenerInactive() {
+        protected void onInactive() {
             // if we go inactive for any reason, fail immediately
             executeOperation(acceptLocationChange(null));
+            super.onInactive();
         }
 
+        @GuardedBy("mMultiplexerLock")
         void deliverNull() {
-            synchronized (mLock) {
-                executeOperation(acceptLocationChange(null));
-            }
+            executeOperation(acceptLocationChange(null));
         }
 
         @Override
@@ -1246,21 +1248,17 @@
                         + " expired at " + TimeUtils.formatRealtime(mExpirationRealtimeMs));
             }
 
-            synchronized (mLock) {
+            synchronized (mMultiplexerLock) {
                 // no need to remove alarm after it's fired
                 mExpirationRealtimeMs = Long.MAX_VALUE;
                 executeOperation(acceptLocationChange(null));
             }
         }
 
-        @GuardedBy("mLock")
+        @GuardedBy("mMultiplexerLock")
         @Override
         @Nullable ListenerOperation<LocationTransport> acceptLocationChange(
                 @Nullable LocationResult fineLocationResult) {
-            if (Build.IS_DEBUGGABLE) {
-                Preconditions.checkState(Thread.holdsLock(mLock));
-            }
-
             // check expiration time - alarm is not guaranteed to go off at the right time,
             // especially for short intervals
             if (SystemClock.elapsedRealtime() >= mExpirationRealtimeMs) {
@@ -1311,9 +1309,7 @@
                     // on failure we're automatically removed anyways, no need to attempt removal
                     // again
                     if (success) {
-                        synchronized (mLock) {
-                            remove();
-                        }
+                        remove();
                     }
                 }
             };
@@ -1324,9 +1320,7 @@
                 Exception e) {
             if (e instanceof RemoteException) {
                 Log.w(TAG, mName + " provider registration " + getIdentity() + " removed", e);
-                synchronized (mLock) {
-                    remove();
-                }
+                remove();
             } else {
                 throw new AssertionError(e);
             }
@@ -1339,9 +1333,7 @@
                     Log.d(TAG, mName + " provider registration " + getIdentity() + " died");
                 }
 
-                synchronized (mLock) {
-                    remove();
-                }
+                remove();
             } catch (RuntimeException e) {
                 // the caller may swallow runtime exceptions, so we rethrow as assertion errors to
                 // ensure the crash is seen
@@ -1350,23 +1342,21 @@
         }
     }
 
-    protected final Object mLock = new Object();
-
     protected final String mName;
-    private final @Nullable PassiveLocationProviderManager mPassiveManager;
+    @Nullable private final PassiveLocationProviderManager mPassiveManager;
 
     protected final Context mContext;
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private @State int mState;
 
     // maps of user id to value
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private final SparseBooleanArray mEnabled; // null or not present means unknown
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private final SparseArray<LastLocation> mLastLocations;
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private final ArrayList<ProviderEnabledListener> mEnabledListeners;
 
     private final CopyOnWriteArrayList<IProviderRequestListener> mProviderRequestListeners;
@@ -1418,14 +1408,14 @@
     private final ScreenInteractiveChangedListener mScreenInteractiveChangedListener =
             this::onScreenInteractiveChanged;
 
-    // acquiring mLock makes operations on mProvider atomic, but is otherwise unnecessary
+    // acquiring mMultiplexerLock makes operations on mProvider atomic, but is otherwise unnecessary
     protected final MockableLocationProvider mProvider;
 
-    @GuardedBy("mLock")
-    private @Nullable OnAlarmListener mDelayedRegister;
+    @GuardedBy("mMultiplexerLock")
+    @Nullable private OnAlarmListener mDelayedRegister;
 
-    @GuardedBy("mLock")
-    private @Nullable StateChangedListener mStateChangedListener;
+    @GuardedBy("mMultiplexerLock")
+    @Nullable private StateChangedListener mStateChangedListener;
 
     public LocationProviderManager(Context context, Injector injector,
             String name, @Nullable PassiveLocationProviderManager passiveManager) {
@@ -1453,19 +1443,14 @@
         mLocationUsageLogger = injector.getLocationUsageLogger();
         mLocationFudger = new LocationFudger(mSettingsHelper.getCoarseLocationAccuracyM());
 
-        mProvider = new MockableLocationProvider(mLock);
+        mProvider = new MockableLocationProvider(mMultiplexerLock);
 
         // set listener last, since this lets our reference escape
         mProvider.getController().setListener(this);
     }
 
-    @Override
-    public String getTag() {
-        return TAG;
-    }
-
     public void startManager(@Nullable StateChangedListener listener) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState == STATE_STOPPED);
             mState = STATE_STARTED;
             mStateChangedListener = listener;
@@ -1485,7 +1470,7 @@
     }
 
     public void stopManager() {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState == STATE_STARTED);
             mState = STATE_STOPPING;
 
@@ -1522,11 +1507,11 @@
         return mProvider.getState();
     }
 
-    public @Nullable CallerIdentity getProviderIdentity() {
+    @Nullable public CallerIdentity getProviderIdentity() {
         return mProvider.getState().identity;
     }
 
-    public @Nullable ProviderProperties getProperties() {
+    @Nullable public ProviderProperties getProperties() {
         return mProvider.getState().properties;
     }
 
@@ -1543,7 +1528,7 @@
 
         Preconditions.checkArgument(userId >= 0);
 
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             int index = mEnabled.indexOfKey(userId);
             if (index < 0) {
                 // this generally shouldn't occur, but might be possible due to race conditions
@@ -1558,14 +1543,14 @@
     }
 
     public void addEnabledListener(ProviderEnabledListener listener) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             mEnabledListeners.add(listener);
         }
     }
 
     public void removeEnabledListener(ProviderEnabledListener listener) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             mEnabledListeners.remove(listener);
         }
@@ -1582,7 +1567,7 @@
     }
 
     public void setRealProvider(@Nullable AbstractLocationProvider provider) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
 
             final long identity = Binder.clearCallingIdentity();
@@ -1595,7 +1580,7 @@
     }
 
     public void setMockProvider(@Nullable MockLocationProvider provider) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
 
             EVENT_LOG.logProviderMocked(mName, provider != null);
@@ -1622,7 +1607,7 @@
     }
 
     public void setMockProviderAllowed(boolean enabled) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             if (!mProvider.isMock()) {
                 throw new IllegalArgumentException(mName + " provider is not a test provider");
             }
@@ -1637,7 +1622,7 @@
     }
 
     public void setMockProviderLocation(Location location) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             if (!mProvider.isMock()) {
                 throw new IllegalArgumentException(mName + " provider is not a test provider");
             }
@@ -1659,7 +1644,7 @@
         }
     }
 
-    public @Nullable Location getLastLocation(LastLocationRequest request,
+    @Nullable public Location getLastLocation(LastLocationRequest request,
             CallerIdentity identity, @PermissionLevel int permissionLevel) {
         request = calculateLastLocationRequest(request, identity);
 
@@ -1732,7 +1717,7 @@
      * location, even if the permissionLevel is coarse. You are responsible for coarsening the
      * location if necessary.
      */
-    public @Nullable Location getLastLocationUnsafe(int userId,
+    @Nullable public Location getLastLocationUnsafe(int userId,
             @PermissionLevel int permissionLevel, boolean isBypass,
             long maximumAgeMs) {
         if (userId == UserHandle.USER_ALL) {
@@ -1756,7 +1741,7 @@
         Preconditions.checkArgument(userId >= 0);
 
         Location location;
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             LastLocation lastLocation = mLastLocations.get(userId);
             if (lastLocation == null) {
@@ -1778,7 +1763,7 @@
     }
 
     public void injectLastLocation(Location location, int userId) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             if (getLastLocationUnsafe(userId, PERMISSION_FINE, false, Long.MAX_VALUE) == null) {
                 setLastLocation(location, userId);
@@ -1800,7 +1785,7 @@
 
         Preconditions.checkArgument(userId >= 0);
 
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             LastLocation lastLocation = mLastLocations.get(userId);
             if (lastLocation == null) {
                 lastLocation = new LastLocation();
@@ -1814,7 +1799,7 @@
         }
     }
 
-    public @Nullable ICancellationSignal getCurrentLocation(LocationRequest request,
+    @Nullable public ICancellationSignal getCurrentLocation(LocationRequest request,
             CallerIdentity identity, int permissionLevel, ILocationCallback callback) {
         if (request.getDurationMillis() > MAX_GET_CURRENT_LOCATION_TIMEOUT_MS) {
             request = new LocationRequest.Builder(request)
@@ -1829,7 +1814,7 @@
                         new GetCurrentLocationTransport(callback),
                         permissionLevel);
 
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -1849,9 +1834,7 @@
                         () -> {
                             final long ident = Binder.clearCallingIdentity();
                             try {
-                                synchronized (mLock) {
-                                    removeRegistration(callback.asBinder(), registration);
-                                }
+                                removeRegistration(callback.asBinder(), registration);
                             } catch (RuntimeException e) {
                                 // since this is within a oneway binder transaction there is nowhere
                                 // for exceptions to go - move onto another thread to crash system
@@ -1885,7 +1868,7 @@
                 new LocationListenerTransport(listener),
                 permissionLevel);
 
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             final long ident = Binder.clearCallingIdentity();
             try {
@@ -1904,7 +1887,7 @@
                 new LocationPendingIntentTransport(mContext, pendingIntent),
                 permissionLevel);
 
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             final long identity = Binder.clearCallingIdentity();
             try {
@@ -1916,42 +1899,38 @@
     }
 
     public void flush(ILocationListener listener, int requestCode) {
-        synchronized (mLock) {
-            final long identity = Binder.clearCallingIdentity();
-            try {
-                boolean flushed = updateRegistration(listener.asBinder(), registration -> {
-                    registration.flush(requestCode);
-                    return false;
-                });
-                if (!flushed) {
-                    throw new IllegalArgumentException("unregistered listener cannot be flushed");
-                }
-            } finally {
-                Binder.restoreCallingIdentity(identity);
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            boolean flushed = updateRegistration(listener.asBinder(), registration -> {
+                registration.flush(requestCode);
+                return false;
+            });
+            if (!flushed) {
+                throw new IllegalArgumentException("unregistered listener cannot be flushed");
             }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
         }
     }
 
     public void flush(PendingIntent pendingIntent, int requestCode) {
-        synchronized (mLock) {
-            final long identity = Binder.clearCallingIdentity();
-            try {
-                boolean flushed = updateRegistration(pendingIntent, registration -> {
-                    registration.flush(requestCode);
-                    return false;
-                });
-                if (!flushed) {
-                    throw new IllegalArgumentException(
-                            "unregistered pending intent cannot be flushed");
-                }
-            } finally {
-                Binder.restoreCallingIdentity(identity);
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            boolean flushed = updateRegistration(pendingIntent, registration -> {
+                registration.flush(requestCode);
+                return false;
+            });
+            if (!flushed) {
+                throw new IllegalArgumentException(
+                        "unregistered pending intent cannot be flushed");
             }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
         }
     }
 
     public void unregisterLocationRequest(ILocationListener listener) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             final long identity = Binder.clearCallingIdentity();
             try {
@@ -1963,7 +1942,7 @@
     }
 
     public void unregisterLocationRequest(PendingIntent pendingIntent) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             Preconditions.checkState(mState != STATE_STOPPED);
             final long identity = Binder.clearCallingIdentity();
             try {
@@ -1974,13 +1953,9 @@
         }
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected void onRegister() {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         mSettingsHelper.addOnBackgroundThrottleIntervalChangedListener(
                 mBackgroundThrottleIntervalChangedListener);
         mSettingsHelper.addOnBackgroundThrottlePackageWhitelistChangedListener(
@@ -1997,13 +1972,9 @@
         mScreenInteractiveHelper.addListener(mScreenInteractiveChangedListener);
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected void onUnregister() {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         mSettingsHelper.removeOnBackgroundThrottleIntervalChangedListener(
                 mBackgroundThrottleIntervalChangedListener);
         mSettingsHelper.removeOnBackgroundThrottlePackageWhitelistChangedListener(
@@ -2019,13 +1990,9 @@
         mScreenInteractiveHelper.removeListener(mScreenInteractiveChangedListener);
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected void onRegistrationAdded(Object key, Registration registration) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         mLocationUsageLogger.logLocationApiUsage(
                 LocationStatsEnums.USAGE_STARTED,
                 LocationStatsEnums.API_REQUEST_LOCATION_UPDATES,
@@ -2038,23 +2005,21 @@
                 null, registration.isForeground());
     }
 
-    @GuardedBy("mLock")
+    // TODO: remove suppression when GuardedBy analysis can recognize lock from super class
+    @SuppressWarnings("GuardedBy")
+    @GuardedBy("mMultiplexerLock")
     @Override
-    protected void onRegistrationReplaced(Object key, Registration oldRegistration,
-            Registration newRegistration) {
+    protected void onRegistrationReplaced(Object oldKey, Registration oldRegistration,
+            Object newKey, Registration newRegistration) {
         // by saving the last delivered location state we are able to potentially delay the
         // resulting provider request longer and save additional power
         newRegistration.setLastDeliveredLocation(oldRegistration.getLastDeliveredLocation());
-        super.onRegistrationReplaced(key, oldRegistration, newRegistration);
+        super.onRegistrationReplaced(oldKey, oldRegistration, newKey, newRegistration);
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected void onRegistrationRemoved(Object key, Registration registration) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         mLocationUsageLogger.logLocationApiUsage(
                 LocationStatsEnums.USAGE_ENDED,
                 LocationStatsEnums.API_REQUEST_LOCATION_UPDATES,
@@ -2067,21 +2032,17 @@
                 null, registration.isForeground());
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected boolean registerWithService(ProviderRequest request,
             Collection<Registration> registrations) {
         return reregisterWithService(ProviderRequest.EMPTY_REQUEST, request, registrations);
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected boolean reregisterWithService(ProviderRequest oldRequest,
             ProviderRequest newRequest, Collection<Registration> registrations) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         // calculate how long the new request should be delayed before sending it off to the
         // provider, under the assumption that once we send the request off, the provider will
         // immediately attempt to deliver a new location satisfying that request.
@@ -2117,7 +2078,7 @@
             mDelayedRegister = new OnAlarmListener() {
                 @Override
                 public void onAlarm() {
-                    synchronized (mLock) {
+                    synchronized (mMultiplexerLock) {
                         if (mDelayedRegister == this) {
                             mDelayedRegister = null;
                             setProviderRequest(newRequest);
@@ -2135,17 +2096,13 @@
         return true;
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected void unregisterWithService() {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         setProviderRequest(ProviderRequest.EMPTY_REQUEST);
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     void setProviderRequest(ProviderRequest request) {
         if (mDelayedRegister != null) {
             mAlarmHelper.cancel(mDelayedRegister);
@@ -2166,13 +2123,9 @@
         });
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected boolean isActive(Registration registration) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         if (!registration.isPermitted()) {
             return false;
         }
@@ -2236,13 +2189,9 @@
         return true;
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     protected ProviderRequest mergeRegistrations(Collection<Registration> registrations) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         long intervalMs = ProviderRequest.INTERVAL_DISABLED;
         int quality = LocationRequest.QUALITY_LOW_POWER;
         long maxUpdateDelayMs = Long.MAX_VALUE;
@@ -2307,7 +2256,7 @@
                 .build();
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     protected long calculateRequestDelayMillis(long newIntervalMs,
             Collection<Registration> registrations) {
         // calculate the minimum delay across all registrations, ensuring that it is not more than
@@ -2349,7 +2298,7 @@
     }
 
     private void onUserChanged(int userId, int change) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             if (mState == STATE_STOPPED) {
                 return;
             }
@@ -2372,15 +2321,13 @@
     private void onLocationUserSettingsChanged(int userId, LocationUserSettings oldSettings,
             LocationUserSettings newSettings) {
         if (oldSettings.isAdasGnssLocationEnabled() != newSettings.isAdasGnssLocationEnabled()) {
-            synchronized (mLock) {
-                updateRegistrations(
-                        registration -> registration.onAdasGnssLocationEnabledChanged(userId));
-            }
+            updateRegistrations(
+                    registration -> registration.onAdasGnssLocationEnabledChanged(userId));
         }
     }
 
     private void onLocationEnabledChanged(int userId) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             if (mState == STATE_STOPPED) {
                 return;
             }
@@ -2390,88 +2337,64 @@
     }
 
     private void onScreenInteractiveChanged(boolean screenInteractive) {
-        synchronized (mLock) {
-            switch (mLocationPowerSaveModeHelper.getLocationPowerSaveMode()) {
-                case LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF:
-                    if (!GPS_PROVIDER.equals(mName)) {
-                        break;
-                    }
-                    // fall through
-                case LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF:
-                    // fall through
-                case LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF:
-                    updateRegistrations(registration -> true);
+        switch (mLocationPowerSaveModeHelper.getLocationPowerSaveMode()) {
+            case LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF:
+                if (!GPS_PROVIDER.equals(mName)) {
                     break;
-                default:
-                    break;
-            }
+                }
+                // fall through
+            case LOCATION_MODE_THROTTLE_REQUESTS_WHEN_SCREEN_OFF:
+                // fall through
+            case LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF:
+                updateRegistrations(registration -> true);
+                break;
+            default:
+                break;
         }
     }
 
     private void onBackgroundThrottlePackageWhitelistChanged() {
-        synchronized (mLock) {
-            updateRegistrations(Registration::onProviderLocationRequestChanged);
-        }
+        updateRegistrations(Registration::onProviderLocationRequestChanged);
     }
 
     private void onBackgroundThrottleIntervalChanged() {
-        synchronized (mLock) {
-            updateRegistrations(Registration::onProviderLocationRequestChanged);
-        }
+        updateRegistrations(Registration::onProviderLocationRequestChanged);
     }
 
     private void onLocationPowerSaveModeChanged(@LocationPowerSaveMode int locationPowerSaveMode) {
-        synchronized (mLock) {
-            // this is rare, just assume everything has changed to keep it simple
-            updateRegistrations(registration -> true);
-        }
+        // this is rare, just assume everything has changed to keep it simple
+        updateRegistrations(registration -> true);
     }
 
     private void onAppForegroundChanged(int uid, boolean foreground) {
-        synchronized (mLock) {
-            updateRegistrations(registration -> registration.onForegroundChanged(uid, foreground));
-        }
+        updateRegistrations(registration -> registration.onForegroundChanged(uid, foreground));
     }
 
     private void onAdasAllowlistChanged() {
-        synchronized (mLock) {
-            updateRegistrations(Registration::onProviderLocationRequestChanged);
-        }
+        updateRegistrations(Registration::onProviderLocationRequestChanged);
     }
 
     private void onIgnoreSettingsWhitelistChanged() {
-        synchronized (mLock) {
-            updateRegistrations(Registration::onProviderLocationRequestChanged);
-        }
+        updateRegistrations(Registration::onProviderLocationRequestChanged);
     }
 
     private void onLocationPackageBlacklistChanged(int userId) {
-        synchronized (mLock) {
-            updateRegistrations(registration -> registration.getIdentity().getUserId() == userId);
-        }
+        updateRegistrations(registration -> registration.getIdentity().getUserId() == userId);
     }
 
     private void onLocationPermissionsChanged(@Nullable String packageName) {
-        synchronized (mLock) {
-            updateRegistrations(
-                    registration -> registration.onLocationPermissionsChanged(packageName));
-        }
+        updateRegistrations(
+                registration -> registration.onLocationPermissionsChanged(packageName));
     }
 
     private void onLocationPermissionsChanged(int uid) {
-        synchronized (mLock) {
-            updateRegistrations(registration -> registration.onLocationPermissionsChanged(uid));
-        }
+        updateRegistrations(registration -> registration.onLocationPermissionsChanged(uid));
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     public void onStateChanged(
             AbstractLocationProvider.State oldState, AbstractLocationProvider.State newState) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         if (oldState.allowed != newState.allowed) {
             onEnabledChanged(UserHandle.USER_ALL);
         }
@@ -2487,13 +2410,9 @@
         }
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     @Override
     public void onReportLocation(LocationResult locationResult) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         LocationResult filtered;
         if (mPassiveManager != null) {
             filtered = locationResult.filter(location -> {
@@ -2549,12 +2468,8 @@
         }
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private void onUserStarted(int userId) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         if (userId == UserHandle.USER_NULL) {
             return;
         }
@@ -2572,12 +2487,8 @@
         }
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private void onUserStopped(int userId) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         if (userId == UserHandle.USER_NULL) {
             return;
         }
@@ -2592,12 +2503,8 @@
         }
     }
 
-    @GuardedBy("mLock")
+    @GuardedBy("mMultiplexerLock")
     private void onEnabledChanged(int userId) {
-        if (Build.IS_DEBUGGABLE) {
-            Preconditions.checkState(Thread.holdsLock(mLock));
-        }
-
         if (userId == UserHandle.USER_NULL) {
             // used during initialization - ignore since many lower level operations (checking
             // settings for instance) do not support the null user
@@ -2697,7 +2604,7 @@
     }
 
     public void dump(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             ipw.print(mName);
             ipw.print(" provider");
             if (mProvider.isMock()) {
@@ -2738,10 +2645,10 @@
 
     private static class LastLocation {
 
-        private @Nullable Location mFineLocation;
-        private @Nullable Location mCoarseLocation;
-        private @Nullable Location mFineBypassLocation;
-        private @Nullable Location mCoarseBypassLocation;
+        @Nullable private Location mFineLocation;
+        @Nullable private Location mCoarseLocation;
+        @Nullable private Location mFineBypassLocation;
+        @Nullable private Location mCoarseBypassLocation;
 
         LastLocation() {}
 
@@ -2765,7 +2672,7 @@
             mCoarseLocation = null;
         }
 
-        public @Nullable Location get(@PermissionLevel int permissionLevel,
+        @Nullable public Location get(@PermissionLevel int permissionLevel,
                 boolean isBypass) {
             switch (permissionLevel) {
                 case PERMISSION_FINE:
@@ -2862,7 +2769,7 @@
         private static class GatedCallback implements Runnable {
 
             @GuardedBy("this")
-            private @Nullable Runnable mCallback;
+            @Nullable private Runnable mCallback;
 
             @GuardedBy("this")
             private boolean mGate;
diff --git a/services/core/java/com/android/server/location/provider/PassiveLocationProviderManager.java b/services/core/java/com/android/server/location/provider/PassiveLocationProviderManager.java
index b35af4f..0cb4f9e 100644
--- a/services/core/java/com/android/server/location/provider/PassiveLocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/PassiveLocationProviderManager.java
@@ -54,7 +54,7 @@
      * Reports a new location to passive location provider clients.
      */
     public void updateLocation(LocationResult locationResult) {
-        synchronized (mLock) {
+        synchronized (mMultiplexerLock) {
             PassiveLocationProvider passive = (PassiveLocationProvider) mProvider.getProvider();
             Preconditions.checkState(passive != null);
 
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index c5f73625..8ab3a94 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -1587,7 +1587,7 @@
                 if (!savedCredential.isNone()) {
                     throw new IllegalStateException("Saved credential given, but user has no SP");
                 }
-                initializeSyntheticPasswordLocked(savedCredential, userId);
+                initializeSyntheticPasswordLocked(userId);
             } else if (savedCredential.isNone() && isProfileWithUnifiedLock(userId)) {
                 // get credential from keystore when profile has unified lock
                 try {
@@ -2513,35 +2513,21 @@
     }
 
     /**
-     * Creates the synthetic password (SP) for the given user and protects it with the user's LSKF.
+     * Creates the synthetic password (SP) for the given user and protects it with an empty LSKF.
      * This is called just once in the lifetime of the user: the first time a nonempty LSKF is set,
      * or when an escrow token is activated on a device with an empty LSKF.
-     *
-     * Maintains the SP invariants described in {@link SyntheticPasswordManager}.
      */
     @GuardedBy("mSpManager")
     @VisibleForTesting
-    SyntheticPassword initializeSyntheticPasswordLocked(LockscreenCredential credential,
-            int userId) {
+    SyntheticPassword initializeSyntheticPasswordLocked(int userId) {
         Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId);
         Preconditions.checkState(getCurrentLskfBasedProtectorId(userId) ==
                 SyntheticPasswordManager.NULL_PROTECTOR_ID,
                 "Cannot reinitialize SP");
 
         final SyntheticPassword sp = mSpManager.newSyntheticPassword(userId);
-        long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(), credential,
-                sp, userId);
-        if (!credential.isNone()) {
-            mSpManager.newSidForUser(getGateKeeperService(), sp, userId);
-            mSpManager.verifyChallenge(getGateKeeperService(), sp, 0L, userId);
-            setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
-            setKeystorePassword(sp.deriveKeyStorePassword(), userId);
-        } else {
-            clearUserKeyProtection(userId, null);
-            setKeystorePassword(null, userId);
-            gateKeeperClearSecureUserId(userId);
-        }
-        fixateNewestUserKeyAuth(userId);
+        final long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(),
+                LockscreenCredential.createNone(), sp, userId);
         setCurrentLskfBasedProtectorId(protectorId, userId);
         onSyntheticPasswordKnown(userId, sp);
         return sp;
@@ -2818,8 +2804,7 @@
             if (!isUserSecure(userId)) {
                 long protectorId = getCurrentLskfBasedProtectorId(userId);
                 if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) {
-                    sp = initializeSyntheticPasswordLocked(LockscreenCredential.createNone(),
-                            userId);
+                    sp = initializeSyntheticPasswordLocked(userId);
                 } else {
                     sp = mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId,
                             LockscreenCredential.createNone(), userId, null).syntheticPassword;
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index e03b423..aa2a25b 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1445,6 +1445,11 @@
                     }
 
                     if (flags != data.getFlags()) {
+                        int changedFlags = data.getFlags() ^ flags;
+                        if ((changedFlags & FLAG_SUPPRESS_NOTIFICATION) != 0) {
+                            // Suppress notification flag changed, clear any effects
+                            clearEffectsLocked(key);
+                        }
                         data.setFlags(flags);
                         // Shouldn't alert again just because of a flag change.
                         r.getNotification().flags |= FLAG_ONLY_ALERT_ONCE;
@@ -1596,6 +1601,20 @@
         updateLightsLocked();
     }
 
+    @GuardedBy("mNotificationLock")
+    private void clearEffectsLocked(String key) {
+        if (key.equals(mSoundNotificationKey)) {
+            clearSoundLocked();
+        }
+        if (key.equals(mVibrateNotificationKey)) {
+            clearVibrateLocked();
+        }
+        boolean removed = mLights.remove(key);
+        if (removed) {
+            updateLightsLocked();
+        }
+    }
+
     protected final BroadcastReceiver mLocaleChangeReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -6665,6 +6684,20 @@
             }
         }
 
+        // Ensure only allowed packages have a substitute app name
+        if (notification.extras.containsKey(Notification.EXTRA_SUBSTITUTE_APP_NAME)) {
+            int hasSubstituteAppNamePermission = mPackageManager.checkPermission(
+                    permission.SUBSTITUTE_NOTIFICATION_APP_NAME, pkg, userId);
+            if (hasSubstituteAppNamePermission != PERMISSION_GRANTED) {
+                notification.extras.remove(Notification.EXTRA_SUBSTITUTE_APP_NAME);
+                if (DBG) {
+                    Slog.w(TAG, "warning: pkg " + pkg + " attempting to substitute app name"
+                            + " without holding perm "
+                            + Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME);
+                }
+            }
+        }
+
         // Remote views? Are they too big?
         checkRemoteViews(pkg, tag, id, notification);
     }
@@ -7814,7 +7847,8 @@
                 && (record.getSuppressedVisualEffects() & SUPPRESSED_EFFECT_STATUS_BAR) != 0;
         if (!record.isUpdate
                 && record.getImportance() > IMPORTANCE_MIN
-                && !suppressedByDnd) {
+                && !suppressedByDnd
+                && isNotificationForCurrentUser(record)) {
             sendAccessibilityEvent(record);
             sentAccessibilityEvent = true;
         }
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 477b8da..729c521 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -86,6 +86,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -1327,16 +1328,17 @@
                 return null;
             }
             NotificationChannelGroup group = r.groups.get(groupId).clone();
-            group.setChannels(new ArrayList<>());
+            ArrayList channels = new ArrayList();
             int N = r.channels.size();
             for (int i = 0; i < N; i++) {
                 final NotificationChannel nc = r.channels.valueAt(i);
                 if (includeDeleted || !nc.isDeleted()) {
                     if (groupId.equals(nc.getGroup())) {
-                        group.addChannel(nc);
+                        channels.add(nc);
                     }
                 }
             }
+            group.setChannels(channels);
             return group;
         }
     }
@@ -1349,7 +1351,10 @@
             if (r == null) {
                 return null;
             }
-            return r.groups.get(groupId);
+            if (r.groups.get(groupId) != null) {
+                 return r.groups.get(groupId).clone();
+            }
+            return null;
         }
     }
 
@@ -1357,44 +1362,48 @@
     public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
             int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty) {
         Objects.requireNonNull(pkg);
-        Map<String, NotificationChannelGroup> groups = new ArrayMap<>();
+        List<NotificationChannelGroup> groups = new ArrayList<>();
         synchronized (mPackagePreferences) {
             PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
             if (r == null) {
                 return ParceledListSlice.emptyList();
             }
-            NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
+            Map<String, ArrayList<NotificationChannel>> groupedChannels = new HashMap();
             int N = r.channels.size();
             for (int i = 0; i < N; i++) {
                 final NotificationChannel nc = r.channels.valueAt(i);
                 if (includeDeleted || !nc.isDeleted()) {
                     if (nc.getGroup() != null) {
                         if (r.groups.get(nc.getGroup()) != null) {
-                            NotificationChannelGroup ncg = groups.get(nc.getGroup());
-                            if (ncg == null) {
-                                ncg = r.groups.get(nc.getGroup()).clone();
-                                ncg.setChannels(new ArrayList<>());
-                                groups.put(nc.getGroup(), ncg);
-
-                            }
-                            ncg.addChannel(nc);
+                            ArrayList<NotificationChannel> channels = groupedChannels.getOrDefault(
+                                    nc.getGroup(), new ArrayList<>());
+                            channels.add(nc);
+                            groupedChannels.put(nc.getGroup(), channels);
                         }
                     } else {
-                        nonGrouped.addChannel(nc);
+                        ArrayList<NotificationChannel> channels = groupedChannels.getOrDefault(
+                            null, new ArrayList<>());
+                        channels.add(nc);
+                        groupedChannels.put(null, channels);
                     }
                 }
             }
-            if (includeNonGrouped && nonGrouped.getChannels().size() > 0) {
-                groups.put(null, nonGrouped);
-            }
-            if (includeEmpty) {
-                for (NotificationChannelGroup group : r.groups.values()) {
-                    if (!groups.containsKey(group.getId())) {
-                        groups.put(group.getId(), group);
-                    }
+            for (NotificationChannelGroup group : r.groups.values()) {
+                ArrayList<NotificationChannel> channels =
+                        groupedChannels.getOrDefault(group.getId(), new ArrayList<>());
+                if (includeEmpty || !channels.isEmpty()) {
+                    NotificationChannelGroup clone = group.clone();
+                    clone.setChannels(channels);
+                    groups.add(clone);
                 }
             }
-            return new ParceledListSlice<>(new ArrayList<>(groups.values()));
+
+            if (includeNonGrouped && groupedChannels.containsKey(null)) {
+                NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
+                nonGrouped.setChannels(groupedChannels.get(null));
+                groups.add(nonGrouped);
+            }
+            return new ParceledListSlice<>(groups);
         }
     }
 
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 7159673..51b36dd 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -39,6 +39,7 @@
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
 import android.app.IActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -1416,16 +1417,18 @@
 
     private static void broadcastActionOverlayChanged(@NonNull final Set<String> targetPackages,
             final int userId) {
+        final PackageManagerInternal pmInternal =
+                LocalServices.getService(PackageManagerInternal.class);
+        final ActivityManagerInternal amInternal =
+                LocalServices.getService(ActivityManagerInternal.class);
         CollectionUtils.forEach(targetPackages, target -> {
             final Intent intent = new Intent(ACTION_OVERLAY_CHANGED,
                     Uri.fromParts("package", target, null));
             intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-            try {
-                ActivityManager.getService().broadcastIntent(null, intent, null, null, 0, null,
-                        null, null, android.app.AppOpsManager.OP_NONE, null, false, false, userId);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "broadcastActionOverlayChanged remote exception", e);
-            }
+            final int[] allowList = pmInternal.getVisibilityAllowList(target, userId);
+            amInternal.broadcastIntent(intent, null /* resultTo */, null /* requiredPermissions */,
+                    false /* serialized */, userId, allowList, null /* filterExtrasForReceiver */,
+                    null /* bOptions */);
         });
     }
 
diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java
index 8501c5e..5b3eff9 100644
--- a/services/core/java/com/android/server/pm/AppsFilterBase.java
+++ b/services/core/java/com/android/server/pm/AppsFilterBase.java
@@ -28,6 +28,7 @@
 import android.os.Binder;
 import android.os.Handler;
 import android.os.Process;
+import android.os.SystemProperties;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.text.TextUtils;
@@ -340,7 +341,9 @@
                 return !isForceQueryable(targetPkgSetting.getAppId())
                       && !isImplicitlyQueryable(callingAppId, targetPkgSetting.getAppId());
             }
-            if (mCacheReady) { // use cache
+            // use cache
+            if (mCacheReady && SystemProperties.getBoolean("debug.pm.use_app_filter_cache",
+                    true)) {
                 if (!shouldFilterApplicationUsingCache(callingUid,
                         targetPkgSetting.getAppId(),
                         userId)) {
diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java
index 2aec187..4e9c472 100644
--- a/services/core/java/com/android/server/pm/BroadcastHelper.java
+++ b/services/core/java/com/android/server/pm/BroadcastHelper.java
@@ -28,7 +28,6 @@
 import android.annotation.AppIdInt;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.BroadcastOptions;
@@ -41,19 +40,20 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.PowerExemptionManager;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.text.TextUtils;
+import android.util.IntArray;
 import android.util.Log;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
 
 import com.android.internal.util.ArrayUtils;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
 
 /**
  * Helper class to send broadcasts for various situations.
@@ -80,6 +80,7 @@
             final int flags, final String targetPkg, final IIntentReceiver finishedReceiver,
             final int[] userIds, int[] instantUserIds,
             @Nullable SparseArray<int[]> broadcastAllowList,
+            @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
             @Nullable Bundle bOptions) {
         try {
             final IActivityManager am = ActivityManager.getService();
@@ -93,11 +94,13 @@
 
             if (ArrayUtils.isEmpty(instantUserIds)) {
                 doSendBroadcast(action, pkg, extras, flags, targetPkg, finishedReceiver,
-                        resolvedUserIds, false /* isInstantApp */, broadcastAllowList, bOptions);
+                        resolvedUserIds, false /* isInstantApp */, broadcastAllowList,
+                        filterExtrasForReceiver, bOptions);
             } else {
                 // send restricted broadcasts for instant apps
                 doSendBroadcast(action, pkg, extras, flags, targetPkg, finishedReceiver,
-                        instantUserIds, true /* isInstantApp */, null, bOptions);
+                        instantUserIds, true /* isInstantApp */, null,
+                        null /* filterExtrasForReceiver */, bOptions);
             }
         } catch (RemoteException ex) {
         }
@@ -113,6 +116,7 @@
     public void doSendBroadcast(String action, String pkg, Bundle extras,
             int flags, String targetPkg, IIntentReceiver finishedReceiver,
             int[] userIds, boolean isInstantApp, @Nullable SparseArray<int[]> broadcastAllowList,
+            @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver,
             @Nullable Bundle bOptions) {
         for (int userId : userIds) {
             final Intent intent = new Intent(action,
@@ -148,45 +152,30 @@
                     intent, finishedReceiver, requiredPermissions,
                     finishedReceiver != null, userId,
                     broadcastAllowList == null ? null : broadcastAllowList.get(userId),
-                    bOptions);
+                    filterExtrasForReceiver, bOptions);
         }
     }
 
-    public void sendResourcesChangedBroadcast(@NonNull Computer snapshot, boolean mediaStatus,
-            boolean replacing, @NonNull String[] pkgNames, @NonNull int[] uids) {
+    public void sendResourcesChangedBroadcast(@NonNull Supplier<Computer> snapshotComputer,
+            boolean mediaStatus, boolean replacing, @NonNull String[] pkgNames,
+            @NonNull int[] uids) {
         if (ArrayUtils.isEmpty(pkgNames) || ArrayUtils.isEmpty(uids)) {
             return;
         }
-
-        try {
-            final IActivityManager am = ActivityManager.getService();
-            if (am == null) {
-                return;
-            }
-
-            final int[] resolvedUserIds = am.getRunningUserIds();
-            for (int userId : resolvedUserIds) {
-                final var lists = getBroadcastParams(snapshot, pkgNames, uids, userId);
-                for (int i = 0; i < lists.size(); i++) {
-                    // Send broadcasts here
-                    final Bundle extras = new Bundle(3);
-                    final BroadcastParams list = lists.get(i);
-                    extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST,
-                            list.getPackageNames());
-                    extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, list.getUids());
-                    extras.putBoolean(Intent.EXTRA_REPLACING, replacing);
-                    final SparseArray<int[]> allowList = list.getAllowList().size() == 0
-                            ? null : list.getAllowList();
-                    final String action =
-                            mediaStatus ? Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE
-                                    : Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE;
-                    sendPackageBroadcast(action, null /* pkg */, extras, 0 /* flags */,
-                            null /* targetPkg */, null /* finishedReceiver */, new int[]{userId},
-                            null /* instantUserIds */, allowList, null /* bOptions */);
-                }
-            }
-        } catch (RemoteException ex) {
+        Bundle extras = new Bundle();
+        extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, pkgNames);
+        extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uids);
+        if (replacing) {
+            extras.putBoolean(Intent.EXTRA_REPLACING, replacing);
         }
+        String action = mediaStatus ? Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE
+                : Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE;
+        sendPackageBroadcast(action, null /* pkg */, extras, 0 /* flags */,
+                null /* targetPkg */, null /* finishedReceiver */, null /* userIds */,
+                null /* instantUserIds */, null /* broadcastAllowList */,
+                (callingUid, intentExtras) -> filterExtrasChangedPackageList(
+                        snapshotComputer.get(), callingUid, intentExtras),
+                null /* bOptions */);
     }
 
     /**
@@ -266,7 +255,8 @@
         final int flags = !componentNames.contains(packageName)
                 ? Intent.FLAG_RECEIVER_REGISTERED_ONLY : 0;
         sendPackageBroadcast(Intent.ACTION_PACKAGE_CHANGED, packageName, extras, flags, null, null,
-                userIds, instantUserIds, broadcastAllowList, null);
+                userIds, instantUserIds, broadcastAllowList, null /* filterExtrasForReceiver */,
+                null /* bOptions */);
     }
 
     public static void sendDeviceCustomizationReadyBroadcast() {
@@ -334,53 +324,72 @@
 
         sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
                 packageName, extras, 0, null, null, userIds, instantUserIds,
-                broadcastAllowlist, null);
+                broadcastAllowlist, null /* filterExtrasForReceiver */, null);
     }
 
     public void sendFirstLaunchBroadcast(String pkgName, String installerPkg,
             int[] userIds, int[] instantUserIds) {
         sendPackageBroadcast(Intent.ACTION_PACKAGE_FIRST_LAUNCH, pkgName, null, 0,
-                installerPkg, null, userIds, instantUserIds, null /* broadcastAllowList */, null);
+                installerPkg, null, userIds, instantUserIds, null /* broadcastAllowList */,
+                null /* filterExtrasForReceiver */, null);
     }
 
     /**
-     * Get broadcast params list based on the given package and uid list. The broadcast params are
-     * used to send broadcast separately if the given packages have different visibility allow list.
+     * Filter package names for the intent extras {@link Intent#EXTRA_CHANGED_PACKAGE_LIST} and
+     * {@link Intent#EXTRA_CHANGED_UID_LIST} by using the rules of the package visibility.
      *
-     * @param pkgList The names of packages which have changes.
-     * @param uidList The uids of packages which have changes.
-     * @param userId The user where packages reside.
-     * @return The list of {@link BroadcastParams} object.
+     * @param callingUid The uid that is going to access the intent extras.
+     * @param extras The intent extras to filter
+     * @return An extras that have been filtered, or {@code null} if the given uid is unable to
+     * access all the packages in the extras.
      */
-    public List<BroadcastParams> getBroadcastParams(@NonNull Computer snapshot,
-            @NonNull String[] pkgList, @NonNull int[] uidList, @UserIdInt int userId) {
-        final List<BroadcastParams> lists = new ArrayList<>(pkgList.length);
-        // Get allow lists for the pkg in the pkgList. Merge into the existed pkgs and uids if
-        // allow lists are the same.
-        for (int i = 0; i < pkgList.length; i++) {
-            final String pkgName = pkgList[i];
-            final int uid = uidList[i];
-            if (TextUtils.isEmpty(pkgName) || Process.INVALID_UID == uid) {
+    @Nullable
+    public static Bundle filterExtrasChangedPackageList(@NonNull Computer snapshot, int callingUid,
+            @NonNull Bundle extras) {
+        if (UserHandle.isCore(callingUid)) {
+            // see all
+            return extras;
+        }
+        final String[] pkgs = extras.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST);
+        if (ArrayUtils.isEmpty(pkgs)) {
+            return extras;
+        }
+        final int userId = extras.getInt(
+                Intent.EXTRA_USER_HANDLE, UserHandle.getUserId(callingUid));
+        final int[] uids = extras.getIntArray(Intent.EXTRA_CHANGED_UID_LIST);
+        final Pair<String[], int[]> filteredPkgs =
+                filterPackages(snapshot, pkgs, uids, callingUid, userId);
+        if (ArrayUtils.isEmpty(filteredPkgs.first)) {
+            // caller is unable to access this intent
+            return null;
+        }
+        final Bundle filteredExtras = new Bundle(extras);
+        filteredExtras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, filteredPkgs.first);
+        filteredExtras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, filteredPkgs.second);
+        return filteredExtras;
+    }
+
+    @NonNull
+    private static Pair<String[], int[]> filterPackages(@NonNull Computer snapshot,
+            @NonNull String[] pkgs, @Nullable int[] uids, int callingUid, int userId) {
+        final int pkgSize = pkgs.length;
+        final int uidSize = !ArrayUtils.isEmpty(uids) ? uids.length : 0;
+
+        final ArrayList<String> pkgList = new ArrayList<>(pkgSize);
+        final IntArray uidList = uidSize > 0 ? new IntArray(uidSize) : null;
+        for (int i = 0; i < pkgSize; i++) {
+            final String packageName = pkgs[i];
+            if (snapshot.shouldFilterApplication(
+                    snapshot.getPackageStateInternal(packageName), callingUid, userId)) {
                 continue;
             }
-            int[] allowList = snapshot.getVisibilityAllowList(pkgName, userId);
-            if (allowList == null) {
-                allowList = new int[0];
-            }
-            boolean merged = false;
-            for (int j = 0; j < lists.size(); j++) {
-                final BroadcastParams list = lists.get(j);
-                if (Arrays.equals(list.getAllowList().get(userId), allowList)) {
-                    list.addPackage(pkgName, uid);
-                    merged = true;
-                    break;
-                }
-            }
-            if (!merged) {
-                lists.add(new BroadcastParams(pkgName, uid, allowList, userId));
+            pkgList.add(packageName);
+            if (uidList != null && i < uidSize) {
+                uidList.add(uids[i]);
             }
         }
-
-        return lists;
+        return new Pair<>(
+                pkgList.size() > 0 ? pkgList.toArray(new String[pkgList.size()]) : null,
+                uidList != null && uidList.size() > 0 ? uidList.toArray() : null);
     }
 }
diff --git a/services/core/java/com/android/server/pm/BroadcastParams.java b/services/core/java/com/android/server/pm/BroadcastParams.java
deleted file mode 100644
index 279aab0..0000000
--- a/services/core/java/com/android/server/pm/BroadcastParams.java
+++ /dev/null
@@ -1,62 +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 android.annotation.IntRange;
-import android.annotation.NonNull;
-import android.annotation.UserIdInt;
-import android.util.IntArray;
-import android.util.SparseArray;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * A helper class that contains information about package names and uids that share the same allow
- * list for sending broadcasts. Used by various package helpers.
- */
-final class BroadcastParams {
-    private final @NonNull List<String> mPackageNames;
-    private final @NonNull IntArray mUids;
-    private final @NonNull SparseArray<int[]> mAllowList;
-
-    BroadcastParams(@NonNull String packageName, @IntRange(from = 0) int uid,
-            @NonNull int[] allowList, @UserIdInt int userId) {
-        mPackageNames = new ArrayList<>(Arrays.asList(packageName));
-        mUids = IntArray.wrap(new int[]{uid});
-        mAllowList = new SparseArray<>(1);
-        mAllowList.put(userId, allowList);
-    }
-
-    public void addPackage(@NonNull String packageName, @IntRange(from = 0) int uid) {
-        mPackageNames.add(packageName);
-        mUids.add(uid);
-    }
-
-    public @NonNull String[] getPackageNames() {
-        return mPackageNames.toArray(new String[0]);
-    }
-
-    public @NonNull int[] getUids() {
-        return mUids.toArray();
-    }
-
-    public @NonNull SparseArray<int[]> getAllowList() {
-        return mAllowList;
-    }
-}
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java
index a878bfd..b3c7cc0 100644
--- a/services/core/java/com/android/server/pm/Computer.java
+++ b/services/core/java/com/android/server/pm/Computer.java
@@ -500,6 +500,13 @@
     boolean isComponentEffectivelyEnabled(@NonNull ComponentInfo componentInfo,
             @UserIdInt int userId);
 
+    /**
+     * @return true if the runtime app user enabled state and the install-time app manifest enabled
+     * state are both effectively enabled for the given app. Or if the app cannot be found,
+     * returns false.
+     */
+    boolean isApplicationEffectivelyEnabled(@NonNull String packageName, @UserIdInt int userId);
+
     @Nullable
     KeySet getKeySetByAlias(@NonNull String packageName, @NonNull String alias);
 
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 8ec3d2b..7dc648c 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -5412,6 +5412,26 @@
         }
     }
 
+    @Override
+    public boolean isApplicationEffectivelyEnabled(@NonNull String packageName,
+            @UserIdInt int userId) {
+        try {
+            int appEnabledSetting = mSettings.getApplicationEnabledSetting(packageName, userId);
+            if (appEnabledSetting == COMPONENT_ENABLED_STATE_DEFAULT) {
+                final AndroidPackage pkg = getPackage(packageName);
+                if (pkg == null) {
+                    // Should not happen because getApplicationEnabledSetting would have thrown
+                    return false;
+                }
+                return pkg.isEnabled();
+            } else {
+                return appEnabledSetting == COMPONENT_ENABLED_STATE_ENABLED;
+            }
+        } catch (PackageManager.NameNotFoundException ignored) {
+            return false;
+        }
+    }
+
     @Nullable
     @Override
     public KeySet getKeySetByAlias(@NonNull String packageName, @NonNull String alias) {
diff --git a/services/core/java/com/android/server/pm/DistractingPackageHelper.java b/services/core/java/com/android/server/pm/DistractingPackageHelper.java
index 53e9754..dbf0d29 100644
--- a/services/core/java/com/android/server/pm/DistractingPackageHelper.java
+++ b/services/core/java/com/android/server/pm/DistractingPackageHelper.java
@@ -27,7 +27,6 @@
 import android.util.ArraySet;
 import android.util.IntArray;
 import android.util.Slog;
-import android.util.SparseArray;
 
 import com.android.internal.util.ArrayUtils;
 import com.android.server.pm.pkg.PackageStateInternal;
@@ -127,7 +126,7 @@
         if (!changedPackagesList.isEmpty()) {
             final String[] changedPackages = changedPackagesList.toArray(
                     new String[changedPackagesList.size()]);
-            sendDistractingPackagesChanged(snapshot, changedPackages, changedUids.toArray(), userId,
+            sendDistractingPackagesChanged(changedPackages, changedUids.toArray(), userId,
                     restrictionFlags);
             mPm.scheduleWritePackageRestrictions(userId);
         }
@@ -169,7 +168,7 @@
         if (!changedPackages.isEmpty()) {
             final String[] packageArray = changedPackages.toArray(
                     new String[changedPackages.size()]);
-            sendDistractingPackagesChanged(snapshot, packageArray, changedUids.toArray(), userId,
+            sendDistractingPackagesChanged(packageArray, changedUids.toArray(), userId,
                     RESTRICTION_NONE);
             mPm.scheduleWritePackageRestrictions(userId);
         }
@@ -182,24 +181,21 @@
      * @param uidList The uids of packages which have suspension changes.
      * @param userId The user where packages reside.
      */
-    void sendDistractingPackagesChanged(@NonNull Computer snapshot, @NonNull String[] pkgList,
-            int[] uidList, int userId, int distractionFlags) {
-        final List<BroadcastParams> lists = mBroadcastHelper.getBroadcastParams(
-                snapshot, pkgList, uidList, userId);
+    void sendDistractingPackagesChanged(@NonNull String[] pkgList, int[] uidList, int userId,
+            int distractionFlags) {
+        final Bundle extras = new Bundle();
+        extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, pkgList);
+        extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uidList);
+        extras.putInt(Intent.EXTRA_DISTRACTION_RESTRICTIONS, distractionFlags);
+
         final Handler handler = mInjector.getHandler();
-        for (int i = 0; i < lists.size(); i++) {
-            final Bundle extras = new Bundle(3);
-            final BroadcastParams list = lists.get(i);
-            extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, list.getPackageNames());
-            extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, list.getUids());
-            extras.putInt(Intent.EXTRA_DISTRACTION_RESTRICTIONS, distractionFlags);
-            final SparseArray<int[]> allowList = list.getAllowList().size() == 0
-                    ? null : list.getAllowList();
-            handler.post(() -> mBroadcastHelper.sendPackageBroadcast(
-                    Intent.ACTION_DISTRACTING_PACKAGES_CHANGED, null /* pkg */,
-                    extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY, null /* targetPkg */,
-                    null /* finishedReceiver */, new int[]{userId}, null /* instantUserIds */,
-                    allowList, null /* bOptions */));
-        }
+        handler.post(() -> mBroadcastHelper.sendPackageBroadcast(
+                Intent.ACTION_DISTRACTING_PACKAGES_CHANGED, null /* pkg */,
+                extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY, null /* targetPkg */,
+                null /* finishedReceiver */, new int[]{userId}, null /* instantUserIds */,
+                null /* broadcastAllowList */,
+                (callingUid, intentExtras) -> BroadcastHelper.filterExtrasChangedPackageList(
+                        mPm.snapshotComputer(), callingUid, intentExtras),
+                null /* bOptions */));
     }
 }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 728d2d1..9db9837 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -2626,7 +2626,7 @@
                     }
                     final String[] pkgNames = new String[]{res.mRemovedInfo.mRemovedPackage};
                     final int[] uids = new int[]{res.mRemovedInfo.mUid};
-                    mBroadcastHelper.sendResourcesChangedBroadcast(mPm.snapshotComputer(),
+                    mBroadcastHelper.sendResourcesChangedBroadcast(mPm::snapshotComputer,
                             false /* mediaStatus */, true /* replacing */, pkgNames, uids);
                 }
                 res.mRemovedInfo.sendPackageRemovedBroadcasts(killApp, false /*removedBySystem*/);
@@ -2804,7 +2804,7 @@
                     }
                     final String[] pkgNames = new String[]{packageName};
                     final int[] uids = new int[]{res.mPkg.getUid()};
-                    mBroadcastHelper.sendResourcesChangedBroadcast(mPm.snapshotComputer(),
+                    mBroadcastHelper.sendResourcesChangedBroadcast(mPm::snapshotComputer,
                             true /* mediaStatus */, true /* replacing */, pkgNames, uids);
                 }
             } else if (!ArrayUtils.isEmpty(res.mLibraryConsumers)) { // if static shared lib
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 99c95d5..5ad39f2 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -2947,7 +2947,7 @@
             @Nullable Bundle bOptions) {
         mHandler.post(() -> mBroadcastHelper.sendPackageBroadcast(action, pkg, extras, flags,
                 targetPkg, finishedReceiver, userIds, instantUserIds, broadcastAllowList,
-                bOptions));
+                null /* filterExtrasForReceiver */, bOptions));
     }
 
     @Override
diff --git a/services/core/java/com/android/server/pm/StorageEventHelper.java b/services/core/java/com/android/server/pm/StorageEventHelper.java
index f621d8b..41c6c0f 100644
--- a/services/core/java/com/android/server/pm/StorageEventHelper.java
+++ b/services/core/java/com/android/server/pm/StorageEventHelper.java
@@ -298,7 +298,7 @@
             packageNames[i] = pkg.getPackageName();
             packageUids[i] = pkg.getUid();
         }
-        mBroadcastHelper.sendResourcesChangedBroadcast(mPm.snapshotComputer(), mediaStatus,
+        mBroadcastHelper.sendResourcesChangedBroadcast(mPm::snapshotComputer, mediaStatus,
                 replacing, packageNames, packageUids);
     }
 
diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
index 01aecd9..6847b70 100644
--- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java
+++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
@@ -38,7 +38,6 @@
 import android.util.ArraySet;
 import android.util.IntArray;
 import android.util.Slog;
-import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
@@ -184,11 +183,9 @@
             }
         });
 
-        final Computer newSnapshot = mPm.snapshotComputer();
-
         if (!changedPackagesList.isEmpty()) {
             final String[] changedPackages = changedPackagesList.toArray(new String[0]);
-            sendPackagesSuspendedForUser(newSnapshot,
+            sendPackagesSuspendedForUser(
                     suspended ? Intent.ACTION_PACKAGES_SUSPENDED
                             : Intent.ACTION_PACKAGES_UNSUSPENDED,
                     changedPackages, changedUids.toArray(), userId);
@@ -197,7 +194,7 @@
         }
         // Send the suspension changed broadcast to ensure suspension state is not stale.
         if (!modifiedPackages.isEmpty()) {
-            sendPackagesSuspendedForUser(newSnapshot, Intent.ACTION_PACKAGES_SUSPENSION_CHANGED,
+            sendPackagesSuspendedForUser(Intent.ACTION_PACKAGES_SUSPENSION_CHANGED,
                     modifiedPackages.toArray(new String[0]), modifiedUids.toArray(), userId);
         }
         return unmodifiablePackages.toArray(new String[0]);
@@ -326,14 +323,12 @@
             }
         });
 
-        final Computer newSnapshot = mPm.snapshotComputer();
-
         mPm.scheduleWritePackageRestrictions(userId);
         if (!unsuspendedPackages.isEmpty()) {
             final String[] packageArray = unsuspendedPackages.toArray(
                     new String[unsuspendedPackages.size()]);
             sendMyPackageSuspendedOrUnsuspended(packageArray, false, userId);
-            sendPackagesSuspendedForUser(newSnapshot, Intent.ACTION_PACKAGES_UNSUSPENDED,
+            sendPackagesSuspendedForUser(Intent.ACTION_PACKAGES_UNSUSPENDED,
                     packageArray, unsuspendedUids.toArray(), userId);
         }
     }
@@ -585,23 +580,19 @@
      * @param userId The user where packages reside.
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    void sendPackagesSuspendedForUser(@NonNull Computer snapshot, @NonNull String intent,
-            @NonNull String[] pkgList, @NonNull int[] uidList, int userId) {
-        final List<BroadcastParams> lists = mBroadcastHelper.getBroadcastParams(
-                snapshot, pkgList, uidList, userId);
+    void sendPackagesSuspendedForUser(@NonNull String intent, @NonNull String[] pkgList,
+            @NonNull int[] uidList, int userId) {
         final Handler handler = mInjector.getHandler();
-        for (int i = 0; i < lists.size(); i++) {
-            final Bundle extras = new Bundle(3);
-            final BroadcastParams list = lists.get(i);
-            extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, list.getPackageNames());
-            extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, list.getUids());
-            final SparseArray<int[]> allowList = list.getAllowList().size() == 0
-                    ? null : list.getAllowList();
-            handler.post(() -> mBroadcastHelper.sendPackageBroadcast(intent, null /* pkg */,
-                    extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY, null /* targetPkg */,
-                    null /* finishedReceiver */, new int[]{userId}, null /* instantUserIds */,
-                    allowList, null /* bOptions */));
-        }
+        final Bundle extras = new Bundle(3);
+        extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, pkgList);
+        extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uidList);
+        handler.post(() -> mBroadcastHelper.sendPackageBroadcast(intent, null /* pkg */,
+                extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY, null /* targetPkg */,
+                null /* finishedReceiver */, new int[]{userId}, null /* instantUserIds */,
+                null /* broadcastAllowList */,
+                (callingUid, intentExtras) -> BroadcastHelper.filterExtrasChangedPackageList(
+                        mPm.snapshotComputer(), callingUid, intentExtras),
+                null /* bOptions */));
     }
 
     private String getKnownPackageName(@NonNull Computer snapshot,
@@ -652,7 +643,7 @@
                 }
                 mBroadcastHelper.doSendBroadcast(action, null, intentExtras,
                         Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, packageName, null,
-                        targetUserIds, false, null, null);
+                        targetUserIds, false, null, null, null);
             }
         });
     }
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index 8dc9428..297439f 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -21,6 +21,7 @@
 import android.annotation.UserIdInt;
 import android.content.Context;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.os.UserManager;
@@ -305,4 +306,19 @@
      * for users that already existed on-disk from an older version of Android.
      */
     public abstract boolean shouldIgnorePrepareStorageErrors(int userId);
+
+    /**
+     * Returns the {@link UserProperties} of the given user, or {@code null} if it is not found.
+     * NB: The actual object is returned. So do NOT modify it!
+     */
+    public abstract @Nullable UserProperties getUserProperties(@UserIdInt int userId);
+
+    /** TODO(b/239982558): add javadoc / mention invalid_id is used to unassing */
+    public abstract void assignUserToDisplay(@UserIdInt int userId, int displayId);
+
+    /**
+     * Returns {@code true} if the user is visible (as defined by
+     * {@link UserManager#isUserVisible()} in the given display.
+     */
+    public abstract boolean isUserVisible(@UserIdInt int userId, int displayId);
 }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 5b4468f..e423ea9 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -18,6 +18,7 @@
 
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.os.UserManager.DISALLOW_USER_SWITCH;
 
 import android.Manifest;
 import android.accounts.Account;
@@ -52,6 +53,7 @@
 import android.content.pm.ShortcutServiceInternal;
 import android.content.pm.UserInfo;
 import android.content.pm.UserInfo.UserInfoFlag;
+import android.content.pm.UserProperties;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
@@ -107,6 +109,7 @@
 import android.util.TypedXmlPullParser;
 import android.util.TypedXmlSerializer;
 import android.util.Xml;
+import android.view.Display;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -180,7 +183,7 @@
     private static final String TAG_NAME = "name";
     private static final String TAG_ACCOUNT = "account";
     private static final String ATTR_FLAGS = "flags";
-    private static final String ATTR_TYPE = "type";
+    private static final String ATTR_TYPE = "type"; // userType
     private static final String ATTR_ICON_PATH = "icon";
     private static final String ATTR_ID = "id";
     private static final String ATTR_CREATION_TIME = "created";
@@ -215,6 +218,7 @@
     private static final String TAG_ENTRY = "entry";
     private static final String TAG_VALUE = "value";
     private static final String TAG_SEED_ACCOUNT_OPTIONS = "seedAccountOptions";
+    private static final String TAG_USER_PROPERTIES = "userProperties";
     private static final String TAG_LAST_REQUEST_QUIET_MODE_ENABLED_CALL =
             "lastRequestQuietModeEnabledCall";
     private static final String TAG_IGNORE_PREPARE_STORAGE_ERRORS =
@@ -259,7 +263,7 @@
     @VisibleForTesting
     static final int MAX_RECENTLY_REMOVED_IDS_SIZE = 100;
 
-    private static final int USER_VERSION = 9;
+    private static final int USER_VERSION = 10;
 
     private static final long EPOCH_PLUS_30_YEARS = 30L * 365 * 24 * 60 * 60 * 1000L; // ms
 
@@ -325,6 +329,9 @@
         // Whether to perist the seed account information to be available after a boot
         boolean persistSeedData;
 
+        /** Properties of the user whose default values originate from its user type. */
+        UserProperties userProperties;
+
         /** Elapsed realtime since boot when the user started. */
         long startRealtime;
 
@@ -620,6 +627,16 @@
     @GuardedBy("mUserStates")
     private final WatchedUserStates mUserStates = new WatchedUserStates();
 
+    /**
+     * Used on devices that support background users (key) running on secondary displays (value).
+     *
+     * <p>Is {@code null} by default and instantiated on demand when the users are started on
+     * secondary displays.
+     */
+    @Nullable
+    @GuardedBy("mUsersLock")
+    private SparseIntArray mUsersOnSecondaryDisplays;
+
     private static UserManagerService sInstance;
 
     public static UserManagerService getInstance() {
@@ -1487,6 +1504,36 @@
         return userTypeDetails != null && userTypeDetails.isSystem();
     }
 
+    /**
+     * Returns a *copy* of the given user's UserProperties, stripping out any information for which
+     * the caller lacks permission.
+     */
+    @Override
+    public @NonNull UserProperties getUserPropertiesCopy(@UserIdInt int userId) {
+        checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, "getUserProperties");
+        final UserProperties origProperties = getUserPropertiesInternal(userId);
+        if (origProperties != null) {
+            int callingUid = Binder.getCallingUid();
+            boolean exposeAllFields = callingUid == Process.SYSTEM_UID;
+            boolean hasManage = hasPermissionGranted(Manifest.permission.MANAGE_USERS, callingUid);
+            boolean hasQuery = hasPermissionGranted(Manifest.permission.QUERY_USERS, callingUid);
+            return new UserProperties(origProperties, exposeAllFields, hasManage, hasQuery);
+        }
+        // A non-existent or partial user will reach here.
+        throw new IllegalArgumentException("Cannot access properties for user " + userId);
+    }
+
+    /** Returns the user's actual, canonical UserProperties object. Do not edit it externally. */
+    private @Nullable UserProperties getUserPropertiesInternal(@UserIdInt int userId) {
+        synchronized (mUsersLock) {
+            final UserData userData = getUserDataLU(userId);
+            if (userData != null) {
+                return userData.userProperties;
+            }
+        }
+        return null;
+    }
+
     @Override
     public boolean hasBadge(@UserIdInt int userId) {
         checkManageOrInteractPermissionIfCallerInOtherProfileGroup(userId, "hasBadge");
@@ -1576,6 +1623,10 @@
 
     public boolean isProfile(@UserIdInt int userId) {
         checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, "isProfile");
+        return isProfileUnchecked(userId);
+    }
+
+    private boolean isProfileUnchecked(@UserIdInt int userId) {
         synchronized (mUsersLock) {
             UserInfo userInfo = getUserInfoLU(userId);
             return userInfo != null && userInfo.isProfile();
@@ -1663,15 +1714,33 @@
                     + ") is visible");
         }
 
-        // First check current foreground user (on main display)
+        return isUserVisibleUnchecked(userId);
+    }
+
+    private boolean isUserVisibleUnchecked(@UserIdInt int userId) {
+        // First check current foreground user and their profiles (on main display)
+        if (isCurrentUserOrRunningProfileOfCurrentUser(userId)) {
+            return true;
+        }
+
+        // TODO(b/239824814): STOPSHIP - add CTS tests (requires change on testing infra)
+        synchronized (mUsersLock) {
+            if (mUsersOnSecondaryDisplays != null) {
+                // TODO(b/239824814): make sure it handles profile as well
+                return (mUsersOnSecondaryDisplays.indexOfKey(userId) >= 0);
+            }
+        }
+
+        return false;
+    }
+
+    private boolean isCurrentUserOrRunningProfileOfCurrentUser(@UserIdInt int userId) {
         int currentUserId = Binder.withCleanCallingIdentity(() -> ActivityManager.getCurrentUser());
 
         if (currentUserId == userId) {
             return true;
         }
 
-        // Then profiles of current user
-        // TODO(b/239824814): consider using AMInternal.isCurrentProfile() instead
         if (isProfile(userId)) {
             int parentId = Binder.withCleanCallingIdentity(() -> getProfileParentId(userId));
             if (parentId == currentUserId) {
@@ -1679,10 +1748,24 @@
             }
         }
 
-        // TODO(b/239824814): support background users on secondary display (and their profiles)
         return false;
     }
 
+    // TODO(b/239982558): currently used just by shell command, might need to move to
+    // UserManagerInternal if needed by other components (like WindowManagerService)
+    // TODO(b/239982558): add unit test
+    // TODO(b/239982558): try to merge with isUserVisibleUnchecked()
+    private boolean isUserVisibleOnDisplay(@UserIdInt int userId, int displayId) {
+        if (displayId == Display.DEFAULT_DISPLAY) {
+            return isCurrentUserOrRunningProfileOfCurrentUser(userId);
+        }
+        synchronized (mUsersLock) {
+            // TODO(b/239824814): make sure it handles profile as well
+            return mUsersOnSecondaryDisplays != null && mUsersOnSecondaryDisplays.get(userId,
+                    Display.INVALID_DISPLAY) == displayId;
+        }
+    }
+
     @Override
     public @NonNull String getUserName() {
         final int callingUid = Binder.getCallingUid();
@@ -1803,6 +1886,19 @@
     }
 
     @Override
+    public boolean isUserSwitcherEnabled(@UserIdInt int mUserId) {
+        boolean multiUserSettingOn = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.USER_SWITCHER_ENABLED,
+                Resources.getSystem().getBoolean(com.android.internal
+                        .R.bool.config_showUserSwitcherByDefault) ? 1 : 0) != 0;
+
+        return UserManager.supportsMultipleUsers()
+                && !hasUserRestriction(DISALLOW_USER_SWITCH, mUserId)
+                && !UserManager.isDeviceInDemoMode(mContext)
+                && multiUserSettingOn;
+    }
+
+    @Override
     public boolean isRestricted(@UserIdInt int userId) {
         if (userId != UserHandle.getCallingUserId()) {
             checkCreateUsersPermission("query isRestricted for user " + userId);
@@ -2195,7 +2291,6 @@
                     originatingUserId, local);
             localChanged = updateLocalRestrictionsForTargetUsersLR(originatingUserId, local,
                     updatedLocalTargetUserIds);
-
             if (isDeviceOwner) {
                 // Remember the global restriction owner userId to be able to make a distinction
                 // in getUserRestrictionSource on who set local policies.
@@ -3268,6 +3363,7 @@
     @VisibleForTesting
     void upgradeIfNecessaryLP(Bundle oldGlobalUserRestrictions, int userVersion,
             int userTypeVersion) {
+        Slog.i(LOG_TAG, "Upgrading users from userVersion " + userVersion + " to " + USER_VERSION);
         Set<Integer> userIdsToWrite = new ArraySet<>();
         final int originalVersion = mUserVersion;
         final int originalUserTypeVersion = mUserTypeVersion;
@@ -3403,6 +3499,27 @@
             userVersion = 9;
         }
 
+        if (userVersion < 10) {
+            // Add UserProperties.
+            synchronized (mUsersLock) {
+                for (int i = 0; i < mUsers.size(); i++) {
+                    final UserData userData = mUsers.valueAt(i);
+                    final UserTypeDetails userTypeDetails = mUserTypes.get(userData.info.userType);
+                    if (userTypeDetails == null) {
+                        throw new IllegalStateException(
+                                "Cannot upgrade user because " + userData.info.userType
+                                        + " isn't defined on this device!");
+                    }
+                    userData.userProperties = new UserProperties(
+                            userTypeDetails.getDefaultUserPropertiesReference());
+                    userIdsToWrite.add(userData.info.id);
+                }
+            }
+            userVersion = 10;
+        }
+
+        // Reminder: If you add another upgrade, make sure to increment USER_VERSION too.
+
         // Done with userVersion changes, moving on to deal with userTypeVersion upgrades
         // Upgrade from previous user type to a new user type
         final int newUserTypeVersion = UserTypeFactory.getUserTypeVersion();
@@ -3417,6 +3534,11 @@
             Slog.w(LOG_TAG, "User version " + mUserVersion + " didn't upgrade as expected to "
                     + USER_VERSION);
         } else {
+            if (userVersion > USER_VERSION) {
+                Slog.wtf(LOG_TAG, "Upgraded user version " + mUserVersion + " is higher the SDK's "
+                        + "one of " + USER_VERSION + ". Someone forgot to update USER_VERSION?");
+            }
+
             mUserVersion = userVersion;
             mUserTypeVersion = newUserTypeVersion;
 
@@ -3536,6 +3658,8 @@
         flags |= mUserTypes.get(systemUserType).getDefaultUserInfoFlags();
         UserInfo system = new UserInfo(UserHandle.USER_SYSTEM, null, null, flags, systemUserType);
         UserData userData = putUserInfo(system);
+        userData.userProperties = new UserProperties(
+                mUserTypes.get(userData.info.userType).getDefaultUserPropertiesReference());
         mNextSerialNumber = MIN_USER_ID;
         mUserVersion = USER_VERSION;
         mUserTypeVersion = UserTypeFactory.getUserTypeVersion();
@@ -3710,6 +3834,12 @@
             serializer.endTag(null, TAG_SEED_ACCOUNT_OPTIONS);
         }
 
+        if (userData.userProperties != null) {
+            serializer.startTag(null, TAG_USER_PROPERTIES);
+            userData.userProperties.writeToXml(serializer);
+            serializer.endTag(null, TAG_USER_PROPERTIES);
+        }
+
         if (userData.getLastRequestQuietModeEnabledMillis() != 0L) {
             serializer.startTag(/* namespace */ null, TAG_LAST_REQUEST_QUIET_MODE_ENABLED_CALL);
             serializer.text(String.valueOf(userData.getLastRequestQuietModeEnabledMillis()));
@@ -3829,6 +3959,7 @@
         String seedAccountName = null;
         String seedAccountType = null;
         PersistableBundle seedAccountOptions = null;
+        UserProperties userProperties = null;
         Bundle baseRestrictions = null;
         Bundle legacyLocalRestrictions = null;
         RestrictionsSet localRestrictions = null;
@@ -3907,6 +4038,17 @@
                 } else if (TAG_SEED_ACCOUNT_OPTIONS.equals(tag)) {
                     seedAccountOptions = PersistableBundle.restoreFromXml(parser);
                     persistSeedData = true;
+                } else if (TAG_USER_PROPERTIES.equals(tag)) {
+                    // We already got the userType above (if it exists), so we can use it.
+                    // And it must exist, since ATTR_TYPE historically predates PROPERTIES.
+                    final UserTypeDetails userTypeDetails = mUserTypes.get(userType);
+                    if (userTypeDetails == null) {
+                        Slog.e(LOG_TAG, "User has properties but no user type!");
+                        return null;
+                    }
+                    final UserProperties defaultProps
+                            = userTypeDetails.getDefaultUserPropertiesReference();
+                    userProperties = new UserProperties(parser, defaultProps);
                 } else if (TAG_LAST_REQUEST_QUIET_MODE_ENABLED_CALL.equals(tag)) {
                     type = parser.next();
                     if (type == XmlPullParser.TEXT) {
@@ -3943,6 +4085,7 @@
         userData.seedAccountType = seedAccountType;
         userData.persistSeedData = persistSeedData;
         userData.seedAccountOptions = seedAccountOptions;
+        userData.userProperties = userProperties;
         userData.setLastRequestQuietModeEnabledMillis(lastRequestQuietModeEnabledTimestamp);
         if (ignorePrepareStorageErrors) {
             userData.setIgnorePrepareStorageErrors();
@@ -4278,6 +4421,8 @@
                     }
                     userData = new UserData();
                     userData.info = userInfo;
+                    userData.userProperties = new UserProperties(
+                            userTypeDetails.getDefaultUserPropertiesReference());
                     mUsers.put(userId, userData);
                 }
                 writeUserLP(userData);
@@ -4672,41 +4817,59 @@
                 null, // use default PullAtomMetadata values
                 BackgroundThread.getExecutor(),
                 this::onPullAtom);
+        statsManager.setPullAtomCallback(
+                FrameworkStatsLog.MULTI_USER_INFO,
+                null, // use default PullAtomMetadata values
+                BackgroundThread.getExecutor(),
+                this::onPullAtom);
     }
 
     /** Writes a UserInfo pulled atom for each user on the device. */
     private int onPullAtom(int atomTag, List<StatsEvent> data) {
-        if (atomTag != FrameworkStatsLog.USER_INFO) {
-            Slogf.e(LOG_TAG, "Unexpected atom tag: %d", atomTag);
-            return android.app.StatsManager.PULL_SKIP;
-        }
-        final List<UserInfo> users = getUsersInternal(true, true, true);
-        final int size = users.size();
-        if (size > 1) {
-            for (int idx = 0; idx < size; idx++) {
-                final UserInfo user = users.get(idx);
-                final int userTypeStandard = UserManager.getUserTypeForStatsd(user.userType);
-                final String userTypeCustom = (userTypeStandard == FrameworkStatsLog
-                        .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN)
-                        ?
-                        user.userType : null;
+        if (atomTag == FrameworkStatsLog.USER_INFO) {
+            final List<UserInfo> users = getUsersInternal(true, true, true);
+            final int size = users.size();
+            if (size > 1) {
+                for (int idx = 0; idx < size; idx++) {
+                    final UserInfo user = users.get(idx);
+                    final int userTypeStandard = UserManager.getUserTypeForStatsd(user.userType);
+                    final String userTypeCustom = (userTypeStandard == FrameworkStatsLog
+                            .USER_LIFECYCLE_JOURNEY_REPORTED__USER_TYPE__TYPE_UNKNOWN)
+                            ?
+                            user.userType : null;
 
-                boolean isUserRunningUnlocked;
-                synchronized (mUserStates) {
-                    isUserRunningUnlocked =
-                            mUserStates.get(user.id, -1) == UserState.STATE_RUNNING_UNLOCKED;
+                    boolean isUserRunningUnlocked;
+                    synchronized (mUserStates) {
+                        isUserRunningUnlocked =
+                                mUserStates.get(user.id, -1) == UserState.STATE_RUNNING_UNLOCKED;
+                    }
+
+                    data.add(FrameworkStatsLog.buildStatsEvent(FrameworkStatsLog.USER_INFO,
+                            user.id,
+                            userTypeStandard,
+                            userTypeCustom,
+                            user.flags,
+                            user.creationTime,
+                            user.lastLoggedInTime,
+                            isUserRunningUnlocked
+                    ));
+                }
+            }
+        } else if (atomTag == FrameworkStatsLog.MULTI_USER_INFO) {
+            if (UserManager.getMaxSupportedUsers() > 1) {
+                int deviceOwnerUserId = UserHandle.USER_NULL;
+
+                synchronized (mRestrictionsLock) {
+                    deviceOwnerUserId = mDeviceOwnerUserId;
                 }
 
-                data.add(FrameworkStatsLog.buildStatsEvent(FrameworkStatsLog.USER_INFO,
-                        user.id,
-                        userTypeStandard,
-                        userTypeCustom,
-                        user.flags,
-                        user.creationTime,
-                        user.lastLoggedInTime,
-                        isUserRunningUnlocked
-                ));
+                data.add(FrameworkStatsLog.buildStatsEvent(FrameworkStatsLog.MULTI_USER_INFO,
+                        UserManager.getMaxSupportedUsers(),
+                        isUserSwitcherEnabled(deviceOwnerUserId)));
             }
+        } else {
+            Slogf.e(LOG_TAG, "Unexpected atom tag: %d", atomTag);
+            return android.app.StatsManager.PULL_SKIP;
         }
         return android.app.StatsManager.PULL_SUCCESS;
     }
@@ -5604,6 +5767,7 @@
         }
         // If we got here, we probably recycled user ids, so invalidate any caches.
         UserManager.invalidateStaticUserProperties();
+        UserManager.invalidateUserPropertiesCache();
         if (nextId < 0) {
             throw new IllegalStateException("No user id available!");
         }
@@ -5776,6 +5940,11 @@
             pw.println("          --reboot (which does a full reboot) or");
             pw.println("          --no-restart (which requires a manual restart)");
             pw.println();
+            pw.println("  is-user-visible [--display DISPLAY_ID] <USER_ID>");
+            pw.println("    Checks if the given user is visible in the given display.");
+            pw.println("    If the display option is not set, it uses the user's context to check");
+            pw.println("    (so it emulates what apps would get from UserManager.isUserVisible())");
+            pw.println();
         }
 
         @Override
@@ -5792,6 +5961,8 @@
                         return runReportPackageAllowlistProblems();
                     case "set-system-user-mode-emulation":
                         return runSetSystemUserModeEmulation();
+                    case "is-user-visible":
+                        return runIsUserVisible();
                     default:
                         return handleDefaultCommands(cmd);
                 }
@@ -5841,9 +6012,6 @@
                 for (int i = 0; i < size; i++) {
                     final UserInfo user = users.get(i);
                     final boolean running = am.isUserRunning(user.id, 0);
-                    final boolean current = user.id == currentUser;
-                    final boolean hasParent = user.profileGroupId != user.id
-                            && user.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID;
                     if (verbose) {
                         final DevicePolicyManagerInternal dpm = getDevicePolicyManagerInternal();
                         String deviceOwner = "";
@@ -5862,7 +6030,11 @@
                                 Binder.restoreCallingIdentity(ident);
                             }
                         }
-                        pw.printf("%d: id=%d, name=%s, type=%s, flags=%s%s%s%s%s%s%s%s%s\n",
+                        final boolean current = user.id == currentUser;
+                        final boolean hasParent = user.profileGroupId != user.id
+                                && user.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID;
+                        final boolean visible = isUserVisibleUnchecked(user.id);
+                        pw.printf("%d: id=%d, name=%s, type=%s, flags=%s%s%s%s%s%s%s%s%s%s\n",
                                 i,
                                 user.id,
                                 user.name,
@@ -5874,7 +6046,9 @@
                                 user.preCreated ? " (pre-created)" : "",
                                 user.convertedFromPreCreated ? " (converted)" : "",
                                 deviceOwner, profileOwner,
-                                current ? " (current)" : "");
+                                current ? " (current)" : "",
+                                visible ? " (visible)" : ""
+                        );
                     } else {
                         // NOTE: the standard "list users" command is used by integration tests and
                         // hence should not be changed. If you need to add more info, use the
@@ -6031,6 +6205,56 @@
             return 0;
         }
 
+        private int runIsUserVisible() {
+            PrintWriter pw = getOutPrintWriter();
+            int displayId = Display.INVALID_DISPLAY;
+            String opt;
+            while ((opt = getNextOption()) != null) {
+                boolean invalidOption = false;
+                switch (opt) {
+                    case "--display":
+                        displayId = Integer.parseInt(getNextArgRequired());
+                        invalidOption = displayId == Display.INVALID_DISPLAY;
+                        break;
+                    default:
+                        invalidOption = true;
+                }
+                if (invalidOption) {
+                    pw.println("Invalid option: " + opt);
+                    return -1;
+                }
+            }
+            int userId = UserHandle.parseUserArg(getNextArgRequired());
+            switch (userId) {
+                case UserHandle.USER_ALL:
+                case UserHandle.USER_CURRENT_OR_SELF:
+                case UserHandle.USER_NULL:
+                    pw.printf("invalid value (%d) for --user option\n", userId);
+                    return -1;
+                case UserHandle.USER_CURRENT:
+                    userId = ActivityManager.getCurrentUser();
+                    break;
+            }
+
+            boolean isVisible;
+            if (displayId != Display.INVALID_DISPLAY) {
+                isVisible = isUserVisibleOnDisplay(userId, displayId);
+            } else {
+                isVisible = getUserManagerForUser(userId).isUserVisible();
+            }
+            pw.println(isVisible);
+            return 0;
+        }
+
+        /**
+         * Gets the {@link UserManager} associated with the context of the given user.
+         */
+        private UserManager getUserManagerForUser(int userId) {
+            UserHandle user = UserHandle.of(userId);
+            Context context = mContext.createContextAsUser(user, /* flags= */ 0);
+            return context.getSystemService(UserManager.class);
+        }
+
         /**
          * Confirms if the build is debuggable
          *
@@ -6130,12 +6354,17 @@
                 pw.print("  Cached user IDs (including pre-created): ");
                 pw.println(Arrays.toString(mUserIdsIncludingPreCreated));
             }
-
         } // synchronized (mPackagesLock)
 
         // Multiple Users on Multiple Display info
         pw.println("  Supports users on secondary displays: "
                 + UserManager.isUsersOnSecondaryDisplaysEnabled());
+        synchronized (mUsersLock) {
+            if (mUsersOnSecondaryDisplays != null) {
+                pw.print("  Users on secondary displays: ");
+                pw.println(mUsersOnSecondaryDisplays);
+            }
+        }
 
         // Dump some capabilities
         pw.println();
@@ -6300,6 +6529,10 @@
             }
         }
 
+        if (userData.userProperties != null) {
+            userData.userProperties.println(pw, "    ");
+        }
+
         pw.println("    Ignore errors preparing storage: "
                 + userData.getIgnorePrepareStorageErrors());
     }
@@ -6686,7 +6919,98 @@
                 return userData != null && userData.getIgnorePrepareStorageErrors();
             }
         }
-    }
+
+        @Override
+        public @Nullable UserProperties getUserProperties(@UserIdInt int userId) {
+            final UserProperties props = getUserPropertiesInternal(userId);
+            if (props == null) {
+                Slog.w(LOG_TAG, "A null UserProperties was returned for user " + userId);
+            }
+            return props;
+        }
+
+        @Override
+        public void assignUserToDisplay(int userId, int displayId) {
+            if (DBG) {
+                Slogf.d(LOG_TAG, "assignUserToDisplay(%d, %d): mUsersOnSecondaryDisplays=%s",
+                        userId, displayId, mUsersOnSecondaryDisplays);
+            }
+            // TODO(b/240613396) throw exception if feature not supported
+
+            if (displayId == Display.INVALID_DISPLAY) {
+                synchronized (mUsersLock) {
+                    if (mUsersOnSecondaryDisplays == null) {
+                        if (false) {
+                            // TODO(b/240613396): remove this if once we check for support above
+                            Slogf.wtf(LOG_TAG, "assignUserToDisplay(%d, %d): no "
+                                    + "mUsersOnSecondaryDisplays", userId, displayId);
+                        }
+                        return;
+                    }
+                    if (DBG) {
+                        Slogf.d(LOG_TAG, "Removing %d from mUsersOnSecondaryDisplays", userId);
+                    }
+                    mUsersOnSecondaryDisplays.delete(userId);
+                    if (mUsersOnSecondaryDisplays.size() == 0) {
+                        if (DBG) {
+                            Slogf.d(LOG_TAG, "Removing mUsersOnSecondaryDisplays");
+                        }
+                        mUsersOnSecondaryDisplays = null;
+                    }
+                }
+                return;
+            }
+
+            synchronized (mUsersLock) {
+                if (mUsersOnSecondaryDisplays == null) {
+                    if (DBG) {
+                        Slogf.d(LOG_TAG, "Creating mUsersOnSecondaryDisplays");
+                    }
+                    mUsersOnSecondaryDisplays = new SparseIntArray();
+                }
+                if (DBG) {
+                    Slogf.d(LOG_TAG, "Adding %d->%d to mUsersOnSecondaryDisplays",
+                            userId, displayId);
+                }
+
+                if (isProfileUnchecked(userId)) {
+                    // Profile can only start in the same display as parent
+                    int parentUserId = getProfileParentId(userId);
+                    int parentDisplayId = mUsersOnSecondaryDisplays.get(parentUserId);
+                    if (displayId != parentDisplayId) {
+                        throw new IllegalStateException("Cannot assign profile " + userId + " to "
+                                + "display " + displayId + " as its parent (user " + parentUserId
+                                + ") is assigned to display " + parentDisplayId);
+                    }
+                } else {
+                    // Check if display is available
+                    for (int i = 0; i < mUsersOnSecondaryDisplays.size(); i++) {
+                        // Make sure display is not used by other users...
+                        // TODO(b/240736142); currently, if a user was started in a display, it
+                        // would need to be stopped first, so "switching" a user on secondary
+                        // diplay requires 2 non-atomic operations (stop and start). Once this logic
+                        // is refactored, it should be atomic.
+                        if (mUsersOnSecondaryDisplays.valueAt(i) == displayId) {
+                            throw new IllegalStateException("Cannot assign " + userId + " to "
+                                    + "display " + displayId + " as it's  already assigned to "
+                                    + "user " + mUsersOnSecondaryDisplays.keyAt(i));
+                        }
+                        // TODO(b/239982558) also check that user is not already assigned to other
+                        // display (including 0). That would be harder to tested under CTS though
+                        // (for example, would need to add a new AM method to start user in bg on
+                        // main display), so it's better to test on unit tests
+                    }
+                }
+
+                mUsersOnSecondaryDisplays.put(userId, displayId);
+            }
+        }
+
+        @Override
+        public boolean isUserVisible(int userId, int displayId) {
+            return isUserVisibleOnDisplay(userId, displayId);
+        }
+    } // class LocalService
 
     /**
      * Check if user has restrictions
diff --git a/services/core/java/com/android/server/pm/UserTypeDetails.java b/services/core/java/com/android/server/pm/UserTypeDetails.java
index 2f3ca66..ebb9f98 100644
--- a/services/core/java/com/android/server/pm/UserTypeDetails.java
+++ b/services/core/java/com/android/server/pm/UserTypeDetails.java
@@ -23,6 +23,7 @@
 import android.annotation.StringRes;
 import android.content.pm.UserInfo;
 import android.content.pm.UserInfo.UserInfoFlag;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.UserManager;
@@ -171,6 +172,12 @@
     private final @CrossProfileIntentFilter.AccessControlLevel int
             mCrossProfileIntentFilterAccessControl;
 
+    /**
+     * The default {@link UserProperties} for the user type.
+     * <p> The uninitialized value of each property is implied by {@link UserProperties.Builder}.
+     */
+    private final @NonNull UserProperties mDefaultUserProperties;
+
     private UserTypeDetails(@NonNull String name, boolean enabled, int maxAllowed,
             @UserInfoFlag int baseType, @UserInfoFlag int defaultUserInfoPropertyFlags, int label,
             int maxAllowedPerParent,
@@ -183,7 +190,8 @@
             @Nullable List<DefaultCrossProfileIntentFilter> defaultCrossProfileIntentFilters,
             boolean isMediaSharedWithParent,
             boolean isCredentialSharableWithParent,
-            @CrossProfileIntentFilter.AccessControlLevel int accessControlLevel) {
+            @CrossProfileIntentFilter.AccessControlLevel int accessControlLevel,
+            @NonNull UserProperties defaultUserProperties) {
         this.mName = name;
         this.mEnabled = enabled;
         this.mMaxAllowed = maxAllowed;
@@ -205,6 +213,7 @@
         this.mIsMediaSharedWithParent = isMediaSharedWithParent;
         this.mIsCredentialSharableWithParent = isCredentialSharableWithParent;
         this.mCrossProfileIntentFilterAccessControl = accessControlLevel;
+        this.mDefaultUserProperties = defaultUserProperties;
     }
 
     /**
@@ -310,18 +319,6 @@
         return mDarkThemeBadgeColors[Math.min(badgeIndex, mDarkThemeBadgeColors.length - 1)];
     }
 
-    public boolean isProfile() {
-        return (mBaseType & UserInfo.FLAG_PROFILE) != 0;
-    }
-
-    public boolean isFull() {
-        return (mBaseType & UserInfo.FLAG_FULL) != 0;
-    }
-
-    public boolean isSystem() {
-        return (mBaseType & UserInfo.FLAG_SYSTEM) != 0;
-    }
-
     /**
      * Returns true if the user has shared media with parent user or false otherwise.
      */
@@ -347,6 +344,26 @@
         return mCrossProfileIntentFilterAccessControl;
     }
 
+    /**
+     * Returns the reference to the default {@link UserProperties} for this type of user.
+     * This is not a copy. Do NOT modify this object.
+     */
+    public @NonNull UserProperties getDefaultUserPropertiesReference() {
+        return mDefaultUserProperties;
+    }
+
+    public boolean isProfile() {
+        return (mBaseType & UserInfo.FLAG_PROFILE) != 0;
+    }
+
+    public boolean isFull() {
+        return (mBaseType & UserInfo.FLAG_FULL) != 0;
+    }
+
+    public boolean isSystem() {
+        return (mBaseType & UserInfo.FLAG_SYSTEM) != 0;
+    }
+
     /** Returns a {@link Bundle} representing the default user restrictions. */
     @NonNull Bundle getDefaultRestrictions() {
         return BundleUtils.clone(mDefaultRestrictions);
@@ -384,6 +401,7 @@
         pw.print(prefix); pw.print("mDefaultUserInfoFlags: ");
         pw.println(UserInfo.flagsToString(mDefaultUserInfoPropertyFlags));
         pw.print(prefix); pw.print("mLabel: "); pw.println(mLabel);
+        mDefaultUserProperties.println(pw, prefix);
 
         final String restrictionsPrefix = prefix + "    ";
         if (isSystem()) {
@@ -442,6 +460,9 @@
         private boolean mIsCredentialSharableWithParent = false;
         private @CrossProfileIntentFilter.AccessControlLevel int
                 mCrossProfileIntentFilterAccessControl = CrossProfileIntentFilter.ACCESS_LEVEL_ALL;
+        // Default UserProperties cannot be null but for efficiency we don't initialize it now.
+        // If it isn't set explicitly, {@link UserProperties.Builder#build()} will be used.
+        private @Nullable UserProperties mDefaultUserProperties = null;
 
         public Builder setName(String name) {
             mName = name;
@@ -560,6 +581,23 @@
             return this;
         }
 
+        /**
+         * Sets (replacing if necessary) the default UserProperties object for this user type.
+         * Takes a builder, rather than a built object, to efficiently ensure that a fresh copy of
+         * properties is stored (since it later might be modified by UserProperties#updateFromXml).
+         */
+        public Builder setDefaultUserProperties(UserProperties.Builder userPropertiesBuilder) {
+            mDefaultUserProperties = userPropertiesBuilder.build();
+            return this;
+        }
+
+        public @NonNull UserProperties getDefaultUserProperties() {
+            if (mDefaultUserProperties == null) {
+                mDefaultUserProperties = new UserProperties.Builder().build();
+            }
+            return mDefaultUserProperties;
+        }
+
         @UserInfoFlag int getBaseType() {
             return mBaseType;
         }
@@ -604,7 +642,8 @@
                     mDefaultCrossProfileIntentFilters,
                     mIsMediaSharedWithParent,
                     mIsCredentialSharableWithParent,
-                    mCrossProfileIntentFilterAccessControl);
+                    mCrossProfileIntentFilterAccessControl,
+                    getDefaultUserProperties());
         }
 
         private boolean hasBadge() {
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 857a975..b98d20e 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -37,6 +37,7 @@
 import static com.android.server.pm.UserTypeDetails.UNLIMITED_NUMBER_OF_USERS;
 
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.os.Build;
@@ -124,7 +125,10 @@
                 .setIsMediaSharedWithParent(true)
                 .setCrossProfileIntentFilterAccessControl(
                         CrossProfileIntentFilter.ACCESS_LEVEL_SYSTEM)
-                .setIsCredentialSharableWithParent(true);
+                .setIsCredentialSharableWithParent(true)
+                .setDefaultUserProperties(new UserProperties.Builder()
+                        .setStartWithParent(true)
+                        .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT));
     }
 
     /**
@@ -156,7 +160,10 @@
                 .setDefaultRestrictions(getDefaultManagedProfileRestrictions())
                 .setDefaultSecureSettings(getDefaultManagedProfileSecureSettings())
                 .setDefaultCrossProfileIntentFilters(getDefaultManagedCrossProfileIntentFilter())
-                .setIsCredentialSharableWithParent(true);
+                .setIsCredentialSharableWithParent(true)
+                .setDefaultUserProperties(new UserProperties.Builder()
+                        .setStartWithParent(true)
+                        .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE));
     }
 
     /**
@@ -396,6 +403,9 @@
                         setResAttributeArray(parser, builder::setBadgeColors);
                     } else if (isProfile && "badge-colors-dark".equals(childName)) {
                         setResAttributeArray(parser, builder::setDarkThemeBadgeColors);
+                    } else if ("user-properties".equals(childName)) {
+                        builder.getDefaultUserProperties()
+                                .updateFromXml(XmlUtils.makeTyped(parser));
                     } else {
                         Slog.w(LOG_TAG, "Unrecognized tag " + childName + " in "
                                 + parser.getPositionDescription());
diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java
index 0a39e64..47a3705 100644
--- a/services/core/java/com/android/server/pm/VerifyingSession.java
+++ b/services/core/java/com/android/server/pm/VerifyingSession.java
@@ -25,6 +25,7 @@
 import static android.content.pm.SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V4;
 import static android.os.PowerWhitelistManager.REASON_PACKAGE_VERIFIER;
 import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
+import static android.os.Process.SYSTEM_UID;
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 
 import static com.android.server.pm.PackageManagerService.CHECK_PENDING_INTEGRITY_VERIFICATION;
@@ -77,6 +78,7 @@
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 final class VerifyingSession {
@@ -353,7 +355,7 @@
         }
         final int verifierUserId = verifierUser.getIdentifier();
 
-        String[] requiredVerifierPackages = mPm.mRequiredVerifierPackages;
+        List<String> requiredVerifierPackages = Arrays.asList(mPm.mRequiredVerifierPackages);
         boolean requiredVerifierPackagesOverridden = false;
 
         // Allow verifier override for ADB installations which could already be unverified using
@@ -377,8 +379,7 @@
                     // are not adding a new way to disable verifications.
                     if (!isAdbVerificationEnabled(pkgLite, verifierUserId,
                             requestedDisableVerification)) {
-                        requiredVerifierPackages = adbVerifierOverridePackages.toArray(
-                                new String[adbVerifierOverridePackages.size()]);
+                        requiredVerifierPackages = adbVerifierOverridePackages;
                         requiredVerifierPackagesOverridden = true;
                     }
                 }
@@ -397,6 +398,16 @@
          */
         final Computer snapshot = mPm.snapshotComputer();
 
+        final int numRequiredVerifierPackages = requiredVerifierPackages.size();
+        for (int i = numRequiredVerifierPackages - 1; i >= 0; i--) {
+            if (!snapshot.isApplicationEffectivelyEnabled(requiredVerifierPackages.get(i),
+                    SYSTEM_UID)) {
+                Slog.w(TAG,
+                        "Required verifier: " + requiredVerifierPackages.get(i) + " is disabled");
+                requiredVerifierPackages.remove(i);
+            }
+        }
+
         for (String requiredVerifierPackage : requiredVerifierPackages) {
             final int requiredUid = snapshot.getPackageUid(requiredVerifierPackage,
                     MATCH_DEBUG_TRIAGED_MISSING, verifierUserId);
@@ -514,7 +525,7 @@
             }
         }
 
-        if (requiredVerifierPackages.length == 0) {
+        if (requiredVerifierPackages.size() == 0) {
             Slog.e(TAG, "No required verifiers");
             return;
         }
@@ -532,7 +543,7 @@
 
             final Intent requiredIntent;
             final String receiverPermission;
-            if (!requiredVerifierPackagesOverridden || requiredVerifierPackages.length == 1) {
+            if (!requiredVerifierPackagesOverridden || requiredVerifierPackages.size() == 1) {
                 // Prod code OR test code+single verifier.
                 requiredIntent = new Intent(verification);
                 if (!requiredVerifierPackagesOverridden) {
@@ -657,7 +668,7 @@
      * @return true if verification should be performed
      */
     private boolean isVerificationEnabled(PackageInfoLite pkgInfoLite, int userId,
-            String[] requiredVerifierPackages) {
+            List<String> requiredVerifierPackages) {
         if (!DEFAULT_VERIFY_ENABLE) {
             return false;
         }
diff --git a/services/core/java/com/android/server/policy/PermissionPolicyService.java b/services/core/java/com/android/server/policy/PermissionPolicyService.java
index 9c95769..7602d33 100644
--- a/services/core/java/com/android/server/policy/PermissionPolicyService.java
+++ b/services/core/java/com/android/server/policy/PermissionPolicyService.java
@@ -354,7 +354,7 @@
      * Get op that controls the access related to the permission.
      *
      * <p>Usually the permission-op relationship is 1:1 but some permissions (e.g. fine location)
-     * {@link AppOpsManager#sOpToSwitch share an op} to control the access.
+     * {@link AppOpsManager#opToSwitch(int)}  share an op} to control the access.
      *
      * @param permission The permission
      * @return The op that controls the access of the permission
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 5816e98..7dcfd8b 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -5121,18 +5121,10 @@
 
     /** {@inheritDoc} */
     @Override
-    public void userActivity() {
-        // ***************************************
-        // NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE
-        // ***************************************
-        // THIS IS CALLED FROM DEEP IN THE POWER MANAGER
-        // WITH ITS LOCKS HELD.
-        //
-        // This code must be VERY careful about the locks
-        // it acquires.
-        // In fact, the current code acquires way too many,
-        // and probably has lurking deadlocks.
-
+    public void userActivity(int displayGroupId, int event) {
+        if (displayGroupId == DEFAULT_DISPLAY && event == PowerManager.USER_ACTIVITY_EVENT_TOUCH) {
+            mDefaultDisplayPolicy.onUserActivityEventTouch();
+        }
         synchronized (mScreenLockTimeout) {
             if (mLockScreenTimerActive) {
                 // reset the timer
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index e8a3dcd..4f00992 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -1006,7 +1006,7 @@
      * Called when userActivity is signalled in the power manager.
      * This is safe to call from any thread, with any window manager locks held or not.
      */
-    public void userActivity();
+    void userActivity(int displayGroupId, int event);
 
     /**
      * Called when we have finished booting and can now display the home
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 685b744..dad9584 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -571,7 +571,8 @@
     /**
      * Called when there has been user activity.
      */
-    public void onUserActivity(int displayGroupId, int event, int uid) {
+    public void onUserActivity(int displayGroupId, @PowerManager.UserActivityEvent int event,
+            int uid) {
         if (DEBUG) {
             Slog.d(TAG, "onUserActivity: event=" + event + ", uid=" + uid);
         }
@@ -712,7 +713,7 @@
         }
         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
         tm.notifyUserActivity();
-        mPolicy.userActivity();
+        mPolicy.userActivity(displayGroupId, event);
         mFaceDownDetector.userActivity(event);
         mScreenUndimDetector.userActivity(displayGroupId);
     }
diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java
index fec61ac..9fe53fb 100644
--- a/services/core/java/com/android/server/power/PowerGroup.java
+++ b/services/core/java/com/android/server/power/PowerGroup.java
@@ -74,6 +74,8 @@
     private long mLastPowerOnTime;
     private long mLastUserActivityTime;
     private long mLastUserActivityTimeNoChangeLights;
+    @PowerManager.UserActivityEvent
+    private int mLastUserActivityEvent;
     /** Timestamp (milliseconds since boot) of the last time the power group was awoken.*/
     private long mLastWakeTime;
     /** Timestamp (milliseconds since boot) of the last time the power group was put to sleep. */
@@ -244,7 +246,7 @@
         return true;
     }
 
-    boolean dozeLocked(long eventTime, int uid, int reason) {
+    boolean dozeLocked(long eventTime, int uid, @PowerManager.GoToSleepReason int reason) {
         if (eventTime < getLastWakeTimeLocked() || !isInteractive(mWakefulness)) {
             return false;
         }
@@ -253,9 +255,14 @@
         try {
             reason = Math.min(PowerManager.GO_TO_SLEEP_REASON_MAX,
                     Math.max(reason, PowerManager.GO_TO_SLEEP_REASON_MIN));
+            long millisSinceLastUserActivity = eventTime - Math.max(
+                    mLastUserActivityTimeNoChangeLights, mLastUserActivityTime);
             Slog.i(TAG, "Powering off display group due to "
-                    + PowerManager.sleepReasonToString(reason)  + " (groupId= " + getGroupId()
-                    + ", uid= " + uid + ")...");
+                    + PowerManager.sleepReasonToString(reason)
+                    + " (groupId= " + getGroupId() + ", uid= " + uid
+                    + ", millisSinceLastUserActivity=" + millisSinceLastUserActivity
+                    + ", lastUserActivityEvent=" + PowerManager.userActivityEventToString(
+                    mLastUserActivityEvent) + ")...");
 
             setSandmanSummonedLocked(/* isSandmanSummoned= */ true);
             setWakefulnessLocked(WAKEFULNESS_DOZING, eventTime, uid, reason, /* opUid= */ 0,
@@ -266,14 +273,16 @@
         return true;
     }
 
-    boolean sleepLocked(long eventTime, int uid, int reason) {
+    boolean sleepLocked(long eventTime, int uid, @PowerManager.GoToSleepReason int reason) {
         if (eventTime < mLastWakeTime || getWakefulnessLocked() == WAKEFULNESS_ASLEEP) {
             return false;
         }
 
         Trace.traceBegin(Trace.TRACE_TAG_POWER, "sleepPowerGroup");
         try {
-            Slog.i(TAG, "Sleeping power group (groupId=" + getGroupId() + ", uid=" + uid + ")...");
+            Slog.i(TAG,
+                    "Sleeping power group (groupId=" + getGroupId() + ", uid=" + uid + ", reason="
+                            + PowerManager.sleepReasonToString(reason) + ")...");
             setSandmanSummonedLocked(/* isSandmanSummoned= */ true);
             setWakefulnessLocked(WAKEFULNESS_ASLEEP, eventTime, uid, reason, /* opUid= */0,
                     /* opPackageName= */ null, /* details= */ null);
@@ -287,16 +296,20 @@
         return mLastUserActivityTime;
     }
 
-    void setLastUserActivityTimeLocked(long lastUserActivityTime) {
+    void setLastUserActivityTimeLocked(long lastUserActivityTime,
+            @PowerManager.UserActivityEvent int event) {
         mLastUserActivityTime = lastUserActivityTime;
+        mLastUserActivityEvent = event;
     }
 
     public long getLastUserActivityTimeNoChangeLightsLocked() {
         return mLastUserActivityTimeNoChangeLights;
     }
 
-    public void setLastUserActivityTimeNoChangeLightsLocked(long time) {
+    public void setLastUserActivityTimeNoChangeLightsLocked(long time,
+            @PowerManager.UserActivityEvent int event) {
         mLastUserActivityTimeNoChangeLights = time;
+        mLastUserActivityEvent = event;
     }
 
     public int getUserActivitySummaryLocked() {
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 2a1748c..ca3599c 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -1216,6 +1216,7 @@
                 return;
             }
 
+            Slog.i(TAG, "onFlip(): Face " + (isFaceDown ? "down." : "up."));
             mIsFaceDown = isFaceDown;
             if (isFaceDown) {
                 final long currentTime = mClock.uptimeMillis();
@@ -1937,12 +1938,13 @@
 
     // Called from native code.
     @SuppressWarnings("unused")
-    private void userActivityFromNative(long eventTime, int event, int displayId, int flags) {
+    private void userActivityFromNative(long eventTime, @PowerManager.UserActivityEvent int event,
+            int displayId, int flags) {
         userActivityInternal(displayId, eventTime, event, flags, Process.SYSTEM_UID);
     }
 
-    private void userActivityInternal(int displayId, long eventTime, int event, int flags,
-            int uid) {
+    private void userActivityInternal(int displayId, long eventTime,
+            @PowerManager.UserActivityEvent int event, int flags, int uid) {
         synchronized (mLock) {
             if (displayId == Display.INVALID_DISPLAY) {
                 if (userActivityNoUpdateLocked(eventTime, event, flags, uid)) {
@@ -1993,11 +1995,12 @@
 
     @GuardedBy("mLock")
     private boolean userActivityNoUpdateLocked(final PowerGroup powerGroup, long eventTime,
-            int event, int flags, int uid) {
+            @PowerManager.UserActivityEvent int event, int flags, int uid) {
         final int groupId = powerGroup.getGroupId();
         if (DEBUG_SPEW) {
             Slog.d(TAG, "userActivityNoUpdateLocked: groupId=" + groupId
-                    + ", eventTime=" + eventTime + ", event=" + event
+                    + ", eventTime=" + eventTime
+                    + ", event=" + PowerManager.userActivityEventToString(event)
                     + ", flags=0x" + Integer.toHexString(flags) + ", uid=" + uid);
         }
 
@@ -2032,7 +2035,7 @@
             if ((flags & PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS) != 0) {
                 if (eventTime > powerGroup.getLastUserActivityTimeNoChangeLightsLocked()
                         && eventTime > powerGroup.getLastUserActivityTimeLocked()) {
-                    powerGroup.setLastUserActivityTimeNoChangeLightsLocked(eventTime);
+                    powerGroup.setLastUserActivityTimeNoChangeLightsLocked(eventTime, event);
                     mDirty |= DIRTY_USER_ACTIVITY;
                     if (event == PowerManager.USER_ACTIVITY_EVENT_BUTTON) {
                         mDirty |= DIRTY_QUIESCENT;
@@ -2042,7 +2045,7 @@
                 }
             } else {
                 if (eventTime > powerGroup.getLastUserActivityTimeLocked()) {
-                    powerGroup.setLastUserActivityTimeLocked(eventTime);
+                    powerGroup.setLastUserActivityTimeLocked(eventTime, event);
                     mDirty |= DIRTY_USER_ACTIVITY;
                     if (event == PowerManager.USER_ACTIVITY_EVENT_BUTTON) {
                         mDirty |= DIRTY_QUIESCENT;
@@ -2069,7 +2072,8 @@
             @WakeReason int reason, String details, int uid, String opPackageName, int opUid) {
         if (DEBUG_SPEW) {
             Slog.d(TAG, "wakePowerGroupLocked: eventTime=" + eventTime
-                    + ", groupId=" + powerGroup.getGroupId() + ", uid=" + uid);
+                    + ", groupId=" + powerGroup.getGroupId()
+                    + ", reason=" + PowerManager.wakeReasonToString(reason) + ", uid=" + uid);
         }
         if (mForceSuspendActive || !mSystemReady) {
             return;
@@ -2092,11 +2096,11 @@
 
     @GuardedBy("mLock")
     private boolean dozePowerGroupLocked(final PowerGroup powerGroup, long eventTime,
-            int reason, int uid) {
+            @GoToSleepReason int reason, int uid) {
         if (DEBUG_SPEW) {
             Slog.d(TAG, "dozePowerGroup: eventTime=" + eventTime
-                    + ", groupId=" + powerGroup.getGroupId() + ", reason=" + reason
-                    + ", uid=" + uid);
+                    + ", groupId=" + powerGroup.getGroupId()
+                    + ", reason=" + PowerManager.sleepReasonToString(reason) + ", uid=" + uid);
         }
 
         if (!mSystemReady || !mBootCompleted) {
@@ -2107,10 +2111,12 @@
     }
 
     @GuardedBy("mLock")
-    private boolean sleepPowerGroupLocked(final PowerGroup powerGroup, long eventTime, int reason,
-            int uid) {
+    private boolean sleepPowerGroupLocked(final PowerGroup powerGroup, long eventTime,
+            @GoToSleepReason int reason, int uid) {
         if (DEBUG_SPEW) {
-            Slog.d(TAG, "sleepPowerGroup: eventTime=" + eventTime + ", uid=" + uid);
+            Slog.d(TAG, "sleepPowerGroup: eventTime=" + eventTime
+                    + ", groupId=" + powerGroup.getGroupId()
+                    + ", reason=" + PowerManager.sleepReasonToString(reason) + ", uid=" + uid);
         }
         if (!mBootCompleted || !mSystemReady) {
             return false;
@@ -2172,7 +2178,11 @@
             case WAKEFULNESS_DOZING:
                 traceMethodName = "goToSleep";
                 Slog.i(TAG, "Going to sleep due to " + PowerManager.sleepReasonToString(reason)
-                        + " (uid " + uid + ")...");
+                        + " (uid " + uid + ", screenOffTimeout=" + mScreenOffTimeoutSetting
+                        + ", activityTimeoutWM=" + mUserActivityTimeoutOverrideFromWindowManager
+                        + ", maxDimRatio=" + mMaximumScreenDimRatioConfig
+                        + ", maxDimDur=" + mMaximumScreenDimDurationConfig + ")...");
+
                 mLastGlobalSleepTime = eventTime;
                 mLastGlobalSleepReason = reason;
                 mLastGlobalSleepTimeRealtime = mClock.elapsedRealtime();
@@ -4257,7 +4267,7 @@
     void onUserActivity() {
         synchronized (mLock) {
             mPowerGroups.get(Display.DEFAULT_DISPLAY_GROUP).setLastUserActivityTimeLocked(
-                    mClock.uptimeMillis());
+                    mClock.uptimeMillis(), PowerManager.USER_ACTIVITY_EVENT_OTHER);
         }
     }
 
@@ -5645,7 +5655,8 @@
         }
 
         @Override // Binder call
-        public void userActivity(int displayId, long eventTime, int event, int flags) {
+        public void userActivity(int displayId, long eventTime,
+                @PowerManager.UserActivityEvent int event, int flags) {
             final long now = mClock.uptimeMillis();
             if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER)
                     != PackageManager.PERMISSION_GRANTED
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index 96823c8a..f378588 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -72,6 +72,12 @@
 public class ThermalManagerService extends SystemService {
     private static final String TAG = ThermalManagerService.class.getSimpleName();
 
+    private static final boolean DEBUG = false;
+
+    /** Input range limits for getThermalHeadroom API */
+    public static final int MIN_FORECAST_SEC = 0;
+    public static final int MAX_FORECAST_SEC = 60;
+
     /** Lock to protect listen list. */
     private final Object mLock = new Object();
 
@@ -478,6 +484,13 @@
                 return Float.NaN;
             }
 
+            if (forecastSeconds < MIN_FORECAST_SEC || forecastSeconds > MAX_FORECAST_SEC) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Invalid forecastSeconds: " + forecastSeconds);
+                }
+                return Float.NaN;
+            }
+
             return mTemperatureWatcher.getForecast(forecastSeconds);
         }
 
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 0c9ada8..202beb3 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -5911,7 +5911,8 @@
     }
 
     @GuardedBy("this")
-    public void noteUserActivityLocked(int uid, int event, long elapsedRealtimeMs, long uptimeMs) {
+    public void noteUserActivityLocked(int uid, @PowerManager.UserActivityEvent int event,
+            long elapsedRealtimeMs, long uptimeMs) {
         if (mOnBatteryInternal) {
             uid = mapUid(uid);
             getUidStatsLocked(uid, elapsedRealtimeMs, uptimeMs).noteUserActivityLocked(event);
@@ -9416,14 +9417,14 @@
         }
 
         @Override
-        public void noteUserActivityLocked(int type) {
+        public void noteUserActivityLocked(@PowerManager.UserActivityEvent int event) {
             if (mUserActivityCounters == null) {
                 initUserActivityLocked();
             }
-            if (type >= 0 && type < NUM_USER_ACTIVITY_TYPES) {
-                mUserActivityCounters[type].stepAtomic();
+            if (event >= 0 && event < NUM_USER_ACTIVITY_TYPES) {
+                mUserActivityCounters[event].stepAtomic();
             } else {
-                Slog.w(TAG, "Unknown user activity type " + type + " was specified.",
+                Slog.w(TAG, "Unknown user activity event " + event + " was specified.",
                         new Throwable());
             }
         }
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index d48af21..f8cbd8b3d 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -70,6 +70,7 @@
 import static com.android.internal.util.FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__CLICKED_REVERT_TREATMENT;
 import static com.android.server.am.MemoryStatUtil.MemoryStat;
 import static com.android.server.am.MemoryStatUtil.readMemoryStatFromFilesystem;
+import static com.android.server.am.ProcessList.INVALID_ADJ;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_METRICS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -277,6 +278,8 @@
         final boolean mProcessSwitch;
         /** The process state of the launching activity prior to the launch */
         final int mProcessState;
+        /** The oom adj score of the launching activity prior to the launch */
+        final int mProcessOomAdj;
         /** Whether the last launched activity has reported drawn. */
         boolean mIsDrawn;
         /** The latest activity to have been launched. */
@@ -312,7 +315,7 @@
         @Nullable
         static TransitionInfo create(@NonNull ActivityRecord r,
                 @NonNull LaunchingState launchingState, @Nullable ActivityOptions options,
-                boolean processRunning, boolean processSwitch, int processState,
+                boolean processRunning, boolean processSwitch, int processState, int processOomAdj,
                 boolean newActivityCreated, int startResult) {
             if (startResult != START_SUCCESS && startResult != START_TASK_TO_FRONT) {
                 return null;
@@ -328,19 +331,20 @@
                 transitionType = TYPE_TRANSITION_COLD_LAUNCH;
             }
             return new TransitionInfo(r, launchingState, options, transitionType, processRunning,
-                    processSwitch, processState);
+                    processSwitch, processState, processOomAdj);
         }
 
         /** Use {@link TransitionInfo#create} instead to ensure the transition type is valid. */
         private TransitionInfo(ActivityRecord r, LaunchingState launchingState,
                 ActivityOptions options, int transitionType, boolean processRunning,
-                boolean processSwitch, int processState) {
+                boolean processSwitch, int processState, int processOomAdj) {
             mLaunchingState = launchingState;
             mTransitionStartTimeNs = launchingState.mCurrentTransitionStartTimeNs;
             mTransitionType = transitionType;
             mProcessRunning = processRunning;
             mProcessSwitch = processSwitch;
             mProcessState = processState;
+            mProcessOomAdj = processOomAdj;
             mTransitionDeviceUptimeMs = launchingState.mCurrentUpTimeMs;
             setLatestLaunchedActivity(r);
             // The launching state can be reused by consecutive launch. Its original association
@@ -644,9 +648,15 @@
         // interesting.
         final boolean processSwitch = !processRunning
                 || !processRecord.hasStartedActivity(launchedActivity);
-        final int processState = processRunning
-                ? processRecord.getCurrentProcState()
-                : PROCESS_STATE_NONEXISTENT;
+        final int processState;
+        final int processOomAdj;
+        if (processRunning) {
+            processState = processRecord.getCurrentProcState();
+            processOomAdj = processRecord.getCurrentAdj();
+        } else {
+            processState = PROCESS_STATE_NONEXISTENT;
+            processOomAdj = INVALID_ADJ;
+        }
 
         final TransitionInfo info = launchingState.mAssociatedTransitionInfo;
         if (DEBUG_METRICS) {
@@ -654,6 +664,7 @@
                     + " launchedActivity=" + launchedActivity + " processRunning=" + processRunning
                     + " processSwitch=" + processSwitch
                     + " processState=" + processState
+                    + " processOomAdj=" + processOomAdj
                     + " newActivityCreated=" + newActivityCreated + " info=" + info);
         }
 
@@ -689,8 +700,8 @@
         }
 
         final TransitionInfo newInfo = TransitionInfo.create(launchedActivity, launchingState,
-                options, processRunning, processSwitch, processState, newActivityCreated,
-                resultCode);
+                options, processRunning, processSwitch, processState, processOomAdj,
+                newActivityCreated, resultCode);
         if (newInfo == null) {
             abort(launchingState, "unrecognized launch");
             return;
@@ -1006,8 +1017,10 @@
             final long uptime = info.mTransitionDeviceUptimeMs;
             final int transitionDelay = info.mCurrentTransitionDelayMs;
             final int processState = info.mProcessState;
+            final int processOomAdj = info.mProcessOomAdj;
             mLoggerHandler.post(() -> logAppTransition(
-                    timestamp, uptime, transitionDelay, infoSnapshot, isHibernating, processState));
+                    timestamp, uptime, transitionDelay, infoSnapshot, isHibernating,
+                    processState, processOomAdj));
         }
         mLoggerHandler.post(() -> logAppDisplayed(infoSnapshot));
         if (info.mPendingFullyDrawn != null) {
@@ -1020,7 +1033,7 @@
     // This gets called on another thread without holding the activity manager lock.
     private void logAppTransition(long transitionStartTimeNs, long transitionDeviceUptimeMs,
             int currentTransitionDelayMs, TransitionInfoSnapshot info, boolean isHibernating,
-            int processState) {
+            int processState, int processOomAdj) {
         final LogMaker builder = new LogMaker(APP_TRANSITION);
         builder.setPackageName(info.packageName);
         builder.setType(info.type);
@@ -1087,7 +1100,8 @@
                 isLoading,
                 info.launchedActivityName.hashCode(),
                 TimeUnit.NANOSECONDS.toMillis(transitionStartTimeNs),
-                processState);
+                processState,
+                processOomAdj);
 
         if (DEBUG_METRICS) {
             Slog.i(TAG, String.format("APP_START_OCCURRED(%s, %s, %s, %s, %s)",
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d11adc1..7e94743 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -659,6 +659,9 @@
 
     boolean mUseTransferredAnimation;
 
+    /** Whether we need to setup the animation to animate only within the letterbox. */
+    private boolean mNeedsLetterboxedAnimation;
+
     /**
      * @see #currentLaunchCanTurnScreenOn()
      */
@@ -1566,7 +1569,7 @@
         if (task == mLastParentBeforePip && task != null) {
             // Notify the TaskFragmentOrganizer that the activity is reparented back from pip.
             mAtmService.mWindowOrganizerController.mTaskFragmentOrganizerController
-                    .onActivityReparentToTask(this);
+                    .onActivityReparentedToTask(this);
             // Activity's reparented back from pip, clear the links once established
             clearLastParentBeforePip();
         }
@@ -1762,8 +1765,12 @@
         mLetterboxUiController.layoutLetterbox(winHint);
     }
 
-    boolean hasWallpaperBackgroudForLetterbox() {
-        return mLetterboxUiController.hasWallpaperBackgroudForLetterbox();
+    boolean hasWallpaperBackgroundForLetterbox() {
+        return mLetterboxUiController.hasWallpaperBackgroundForLetterbox();
+    }
+
+    void updateLetterboxSurface(WindowState winHint, Transaction t) {
+        mLetterboxUiController.updateLetterboxSurface(winHint, t);
     }
 
     void updateLetterboxSurface(WindowState winHint) {
@@ -5325,6 +5332,18 @@
         commitVisibility(visible, performLayout, false /* fromTransition */);
     }
 
+    void setNeedsLetterboxedAnimation(boolean needsLetterboxedAnimation) {
+        mNeedsLetterboxedAnimation = needsLetterboxedAnimation;
+    }
+
+    boolean isNeedsLetterboxedAnimation() {
+        return mNeedsLetterboxedAnimation;
+    }
+
+    boolean isInLetterboxAnimation() {
+        return mNeedsLetterboxedAnimation && isAnimating();
+    }
+
     /**
      * Post process after applying an app transition animation.
      *
@@ -7252,6 +7271,10 @@
                 .setParent(getAnimationLeashParent())
                 .setName(getSurfaceControl() + " - animation-bounds")
                 .setCallsite("ActivityRecord.createAnimationBoundsLayer");
+        if (mNeedsLetterboxedAnimation) {
+            // Needs to be an effect layer to support rounded corners
+            builder.setEffectLayer();
+        }
         final SurfaceControl boundsLayer = builder.build();
         t.show(boundsLayer);
         return boundsLayer;
@@ -7289,6 +7312,11 @@
             mAnimatingActivityRegistry.notifyStarting(this);
         }
 
+        if (mNeedsLetterboxedAnimation) {
+            updateLetterboxSurface(findMainWindow(), t);
+            mNeedsAnimationBoundsLayer = true;
+        }
+
         // If the animation needs to be cropped then an animation bounds layer is created as a
         // child of the root pinned task or animation layer. The leash is then reparented to this
         // new layer.
@@ -7311,6 +7339,17 @@
             t.setLayer(leash, 0);
             t.setLayer(mAnimationBoundsLayer, getLastLayer());
 
+            if (mNeedsLetterboxedAnimation) {
+                final int cornerRadius = mLetterboxUiController
+                        .getRoundedCornersRadius(findMainWindow());
+
+                final Rect letterboxInnerBounds = new Rect();
+                getLetterboxInnerBounds(letterboxInnerBounds);
+
+                t.setCornerRadius(mAnimationBoundsLayer, cornerRadius)
+                        .setCrop(mAnimationBoundsLayer, letterboxInnerBounds);
+            }
+
             // Reparent leash to animation bounds layer.
             t.reparent(leash, mAnimationBoundsLayer);
         }
@@ -7424,6 +7463,12 @@
             mAnimationBoundsLayer = null;
         }
 
+        mNeedsAnimationBoundsLayer = false;
+        if (mNeedsLetterboxedAnimation) {
+            mNeedsLetterboxedAnimation = false;
+            updateLetterboxSurface(findMainWindow(), t);
+        }
+
         if (mAnimatingActivityRegistry != null) {
             mAnimatingActivityRegistry.notifyFinished(this);
         }
@@ -7436,7 +7481,6 @@
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "AR#onAnimationFinished");
         mTransit = TRANSIT_OLD_UNSET;
         mTransitFlags = 0;
-        mNeedsAnimationBoundsLayer = false;
 
         setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM | FINISH_LAYOUT_REDO_WALLPAPER,
                 "ActivityRecord");
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 87a4fe9..de3b2a6 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -311,7 +311,7 @@
 
     /**
      * The duration to keep a process in animating state (top scheduling group) when the
-     * wakefulness is changing from awake to doze or sleep.
+     * wakefulness is dozing (unlocking) or changing from awake to doze or sleep (locking).
      */
     private static final long DOZE_ANIMATING_STATE_RETAIN_TIME_MS = 2000;
 
@@ -1486,6 +1486,7 @@
         a.colorMode = ActivityInfo.COLOR_MODE_DEFAULT;
         a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS;
         a.resizeMode = RESIZE_MODE_UNRESIZEABLE;
+        a.configChanges = ActivityInfo.CONFIG_ORIENTATION;
 
         final ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchActivityType(ACTIVITY_TYPE_DREAM);
@@ -2927,12 +2928,14 @@
             mDemoteTopAppReasons &= ~DEMOTE_TOP_REASON_DURING_UNLOCKING;
             final WindowState notificationShade = mRootWindowContainer.getDefaultDisplay()
                     .getDisplayPolicy().getNotificationShade();
-            proc = notificationShade != null
-                    ? mProcessMap.getProcess(notificationShade.mSession.mPid) : null;
+            proc = notificationShade != null ? notificationShade.getProcess() : null;
         }
-        if (proc == null) {
-            return;
-        }
+        setProcessAnimatingWhileDozing(proc);
+    }
+
+    // The caller MUST NOT hold the global lock because it calls AM method directly.
+    void setProcessAnimatingWhileDozing(WindowProcessController proc) {
+        if (proc == null) return;
         // Set to activity manager directly to make sure the state can be seen by the subsequent
         // update of scheduling group.
         proc.setRunningAnimationUnsafe();
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 5a1afc4..20032d6 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -789,7 +789,7 @@
 
             // schedule launch ticks to collect information about slow apps.
             r.startLaunchTickingLocked();
-
+            r.lastLaunchTime = SystemClock.uptimeMillis();
             r.setProcess(proc);
 
             // Ensure activity is allowed to be resumed after process has set.
@@ -835,8 +835,6 @@
             final IActivityClientController activityClientController =
                     proc.hasEverLaunchedActivity() ? null : mService.mActivityClientController;
             r.launchCount++;
-            r.lastLaunchTime = SystemClock.uptimeMillis();
-            proc.setLastActivityLaunchTime(r.lastLaunchTime);
 
             if (DEBUG_ALL) Slog.v(TAG, "Launching: " + r);
 
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index 963345f..44f388b 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -81,9 +81,11 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.Rect;
 import android.os.Trace;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Pair;
 import android.util.Slog;
 import android.view.Display;
 import android.view.RemoteAnimationAdapter;
@@ -1037,6 +1039,32 @@
             return;
         }
 
+        if (AppTransition.isActivityTransitOld(transit)) {
+            final ArrayList<Pair<ActivityRecord, Rect>> closingLetterboxes = new ArrayList();
+            for (int i = 0; i < closingApps.size(); ++i) {
+                ActivityRecord closingApp = closingApps.valueAt(i);
+                if (closingApp.areBoundsLetterboxed()) {
+                    final Rect insets = closingApp.getLetterboxInsets();
+                    closingLetterboxes.add(new Pair(closingApp, insets));
+                }
+            }
+
+            for (int i = 0; i < openingApps.size(); ++i) {
+                ActivityRecord openingApp = openingApps.valueAt(i);
+                if (openingApp.areBoundsLetterboxed()) {
+                    final Rect openingInsets = openingApp.getLetterboxInsets();
+                    for (Pair<ActivityRecord, Rect> closingLetterbox : closingLetterboxes) {
+                        final Rect closingInsets = closingLetterbox.second;
+                        if (openingInsets.equals(closingInsets)) {
+                            ActivityRecord closingApp = closingLetterbox.first;
+                            openingApp.setNeedsLetterboxedAnimation(true);
+                            closingApp.setNeedsLetterboxedAnimation(true);
+                        }
+                    }
+                }
+            }
+        }
+
         final ArraySet<WindowContainer> openingWcs = getAnimationTargets(
                 openingApps, closingApps, true /* visible */);
         final ArraySet<WindowContainer> closingWcs = getAnimationTargets(
diff --git a/services/core/java/com/android/server/wm/AppWarnings.java b/services/core/java/com/android/server/wm/AppWarnings.java
index 994f079..5a24099 100644
--- a/services/core/java/com/android/server/wm/AppWarnings.java
+++ b/services/core/java/com/android/server/wm/AppWarnings.java
@@ -17,8 +17,12 @@
 package com.android.server.wm;
 
 import android.annotation.UiThread;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.os.Build;
 import android.os.Handler;
@@ -305,21 +309,21 @@
     private void hideDialogsForPackageUiThread(String name) {
         // Hides the "unsupported display" dialog if necessary.
         if (mUnsupportedDisplaySizeDialog != null && (name == null || name.equals(
-                mUnsupportedDisplaySizeDialog.getPackageName()))) {
+                mUnsupportedDisplaySizeDialog.mPackageName))) {
             mUnsupportedDisplaySizeDialog.dismiss();
             mUnsupportedDisplaySizeDialog = null;
         }
 
         // Hides the "unsupported compile SDK" dialog if necessary.
         if (mUnsupportedCompileSdkDialog != null && (name == null || name.equals(
-                mUnsupportedCompileSdkDialog.getPackageName()))) {
+                mUnsupportedCompileSdkDialog.mPackageName))) {
             mUnsupportedCompileSdkDialog.dismiss();
             mUnsupportedCompileSdkDialog = null;
         }
 
         // Hides the "deprecated target sdk version" dialog if necessary.
         if (mDeprecatedTargetSdkVersionDialog != null && (name == null || name.equals(
-                mDeprecatedTargetSdkVersionDialog.getPackageName()))) {
+                mDeprecatedTargetSdkVersionDialog.mPackageName))) {
             mDeprecatedTargetSdkVersionDialog.dismiss();
             mDeprecatedTargetSdkVersionDialog = null;
         }
@@ -431,6 +435,49 @@
         }
     }
 
+    static class BaseDialog {
+        final AppWarnings mManager;
+        final String mPackageName;
+        AlertDialog mDialog;
+        private BroadcastReceiver mCloseReceiver;
+
+        BaseDialog(AppWarnings manager, String packageName) {
+            mManager = manager;
+            mPackageName = packageName;
+        }
+
+        @UiThread
+        void show() {
+            if (mDialog == null) return;
+            if (mCloseReceiver == null) {
+                mCloseReceiver = new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
+                            mManager.mUiHandler.hideDialogsForPackage(mPackageName);
+                        }
+                    }
+                };
+                mManager.mUiContext.registerReceiver(mCloseReceiver,
+                        new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
+                        Context.RECEIVER_EXPORTED);
+            }
+            Slog.w(TAG, "Showing " + getClass().getSimpleName() + " for package " + mPackageName);
+            mDialog.show();
+        }
+
+        @UiThread
+        void dismiss() {
+            if (mDialog == null) return;
+            if (mCloseReceiver != null) {
+                mManager.mUiContext.unregisterReceiver(mCloseReceiver);
+                mCloseReceiver = null;
+            }
+            mDialog.dismiss();
+            mDialog = null;
+        }
+    }
+
     /**
      * Handles messages on the ActivityTaskManagerService thread.
      */
diff --git a/services/core/java/com/android/server/wm/DeprecatedTargetSdkVersionDialog.java b/services/core/java/com/android/server/wm/DeprecatedTargetSdkVersionDialog.java
index 37244bd..1a7a9b2 100644
--- a/services/core/java/com/android/server/wm/DeprecatedTargetSdkVersionDialog.java
+++ b/services/core/java/com/android/server/wm/DeprecatedTargetSdkVersionDialog.java
@@ -16,31 +16,23 @@
 
 package com.android.server.wm;
 
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
-
 import android.app.AlertDialog;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageItemInfo;
 import android.content.pm.PackageManager;
-import android.util.Log;
 import android.view.Window;
 import android.view.WindowManager;
 
 import com.android.internal.R;
 import com.android.server.utils.AppInstallerUtil;
 
-public class DeprecatedTargetSdkVersionDialog {
-    private static final String TAG = TAG_WITH_CLASS_NAME ? "DeprecatedTargetSdkVersionDialog" : TAG_ATM;
+class DeprecatedTargetSdkVersionDialog extends AppWarnings.BaseDialog {
 
-    private final AlertDialog mDialog;
-    private final String mPackageName;
-
-    public DeprecatedTargetSdkVersionDialog(final AppWarnings manager, Context context,
+    DeprecatedTargetSdkVersionDialog(final AppWarnings manager, Context context,
             ApplicationInfo appInfo) {
-        mPackageName = appInfo.packageName;
+        super(manager, appInfo.packageName);
 
         final PackageManager pm = context.getPackageManager();
         final CharSequence label = appInfo.loadSafeLabel(pm,
@@ -75,17 +67,4 @@
         // DO NOT MODIFY. Used by CTS to verify the dialog is displayed.
         window.getAttributes().setTitle("DeprecatedTargetSdkVersionDialog");
     }
-
-    public String getPackageName() {
-        return mPackageName;
-    }
-
-    public void show() {
-        Log.w(TAG, "Showing SDK deprecation warning for package " + mPackageName);
-        mDialog.show();
-    }
-
-    public void dismiss() {
-        mDialog.dismiss();
-    }
 }
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index 0422906..b033dca 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -504,6 +504,7 @@
 
     @Override
     public void onConfigurationChanged(Configuration newParentConfig) {
+        mTransitionController.collectForDisplayAreaChange(this);
         mTmpConfiguration.setTo(getConfiguration());
         super.onConfigurationChanged(newParentConfig);
 
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index fa3fc9f..8a34af3 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -5946,6 +5946,9 @@
         if (changes != 0) {
             Slog.i(TAG, "Override config changes=" + Integer.toHexString(changes) + " "
                     + mTempConfig + " for displayId=" + mDisplayId);
+            if (isReady() && mTransitionController.isShellTransitionsEnabled()) {
+                requestChangeTransitionIfNeeded(changes, null /* displayChange */);
+            }
             onRequestedOverrideConfigurationChanged(mTempConfig);
 
             final boolean isDensityChange = (changes & ActivityInfo.CONFIG_DENSITY) != 0;
@@ -5962,9 +5965,6 @@
             }
             mWmService.mDisplayNotificationController.dispatchDisplayChanged(
                     this, getConfiguration());
-            if (isReady() && mTransitionController.isShellTransitionsEnabled()) {
-                requestChangeTransitionIfNeeded(changes, null /* displayChange */);
-            }
         }
         return changes;
     }
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 5221072..1a34c93 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -258,7 +258,7 @@
     private volatile boolean mWindowManagerDrawComplete;
 
     private WindowState mStatusBar = null;
-    private WindowState mNotificationShade = null;
+    private volatile WindowState mNotificationShade;
     private final int[] mStatusBarHeightForRotation = new int[4];
     private WindowState mNavigationBar = null;
     @NavigationBarPosition
@@ -794,11 +794,11 @@
     }
 
     public void setAwake(boolean awake) {
-        if (awake == mAwake) {
-            return;
-        }
-        mAwake = awake;
-        synchronized (mService.mGlobalLock) {
+        synchronized (mLock) {
+            if (awake == mAwake) {
+                return;
+            }
+            mAwake = awake;
             if (!mDisplayContent.isDefaultDisplay) {
                 return;
             }
@@ -2371,18 +2371,19 @@
             return;
         }
 
-        // The immersive mode confirmation should never affect the system bar visibility, otherwise
+        // Immersive mode confirmation should never affect the system bar visibility, otherwise
         // it will unhide the navigation bar and hide itself.
         if (winCandidate.getAttrs().token == mImmersiveModeConfirmation.getWindowToken()) {
-
-            // The immersive mode confirmation took the focus from mLastFocusedWindow which was
-            // controlling the system ui visibility. So if mLastFocusedWindow can still receive
-            // keys, we let it keep controlling the visibility.
-            final boolean lastFocusCanReceiveKeys =
-                    (mLastFocusedWindow != null && mLastFocusedWindow.canReceiveKeys());
-            winCandidate = isKeyguardShowing() && !isKeyguardOccluded() ? mNotificationShade
-                    : lastFocusCanReceiveKeys ? mLastFocusedWindow
-                            : mTopFullscreenOpaqueWindowState;
+            if (mNotificationShade != null && mNotificationShade.canReceiveKeys()) {
+                // Let notification shade control the system bar visibility.
+                winCandidate = mNotificationShade;
+            } else if (mLastFocusedWindow != null && mLastFocusedWindow.canReceiveKeys()) {
+                // Immersive mode confirmation took the focus from mLastFocusedWindow which was
+                // controlling the system bar visibility. Let it keep controlling the visibility.
+                winCandidate = mLastFocusedWindow;
+            } else {
+                winCandidate = mTopFullscreenOpaqueWindowState;
+            }
             if (winCandidate == null) {
                 return;
             }
@@ -2745,6 +2746,19 @@
         mImmersiveModeConfirmation.onLockTaskModeChangedLw(lockTaskState);
     }
 
+    /** Called when a {@link android.os.PowerManager#USER_ACTIVITY_EVENT_TOUCH} is sent. */
+    public void onUserActivityEventTouch() {
+        // If there is keyguard, it may use INPUT_FEATURE_DISABLE_USER_ACTIVITY (InputDispatcher
+        // won't trigger user activity for touch). So while the device is not interactive, the user
+        // event is only sent explicitly from SystemUI.
+        if (mAwake) return;
+        // If the event is triggered while the display is not awake, the screen may be showing
+        // dozing UI such as AOD or overlay UI of under display fingerprint. Then set the animating
+        // state temporarily to make the process more responsive.
+        final WindowState w = mNotificationShade;
+        mService.mAtmService.setProcessAnimatingWhileDozing(w != null ? w.getProcess() : null);
+    }
+
     boolean onSystemUiSettingsChanged() {
         return mImmersiveModeConfirmation.onSettingChanged(mService.mCurrentUserId);
     }
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 8dd5850..97609a7 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -513,19 +513,6 @@
         return true;
     }
 
-    /**
-     * Utility to get a rotating displaycontent from a Transition.
-     * @return null if the transition doesn't contain a rotating display.
-     */
-    static DisplayContent getDisplayFromTransition(Transition transition) {
-        for (int i = transition.mParticipants.size() - 1; i >= 0; --i) {
-            final WindowContainer wc = transition.mParticipants.valueAt(i);
-            if (!(wc instanceof DisplayContent)) continue;
-            return (DisplayContent) wc;
-        }
-        return null;
-    }
-
     private void startRemoteRotation(int fromRotation, int toRotation) {
         mDisplayContent.mRemoteDisplayChangeController.performRemoteDisplayChange(
                 fromRotation, toRotation, null /* newDisplayAreaInfo */,
@@ -545,11 +532,6 @@
                 throw new IllegalStateException("Trying to rotate outside a transition");
             }
             mDisplayContent.mTransitionController.collect(mDisplayContent);
-            // Go through all tasks and collect them before the rotation
-            // TODO(shell-transitions): move collect() to onConfigurationChange once wallpaper
-            //       handling is synchronized.
-            mDisplayContent.mTransitionController.collectForDisplayAreaChange(mDisplayContent,
-                    null /* use collecting transition */);
         }
         mService.mAtmService.deferWindowLayout();
         try {
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 7a9e6a9..f3bd1a1 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -761,7 +761,7 @@
 
         InsetsPolicyAnimationControlListener(boolean show, Runnable finishCallback, int types) {
             super(show, false /* hasCallbacks */, types, BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE,
-                    false /* disable */, 0 /* floatingImeBottomInsets */);
+                    false /* disable */, 0 /* floatingImeBottomInsets */, null);
             mFinishCallback = finishCallback;
             mControlCallbacks = new InsetsPolicyAnimationControlCallbacks(this);
         }
diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java
index df3109a..27550d9 100644
--- a/services/core/java/com/android/server/wm/Letterbox.java
+++ b/services/core/java/com/android/server/wm/Letterbox.java
@@ -56,6 +56,7 @@
     private final Supplier<Boolean> mHasWallpaperBackgroundSupplier;
     private final Supplier<Integer> mBlurRadiusSupplier;
     private final Supplier<Float> mDarkScrimAlphaSupplier;
+    private final Supplier<SurfaceControl> mParentSurfaceSupplier;
 
     private final Rect mOuter = new Rect();
     private final Rect mInner = new Rect();
@@ -87,7 +88,8 @@
             Supplier<Integer> blurRadiusSupplier,
             Supplier<Float> darkScrimAlphaSupplier,
             IntConsumer doubleTapCallbackX,
-            IntConsumer doubleTapCallbackY) {
+            IntConsumer doubleTapCallbackY,
+            Supplier<SurfaceControl> parentSurface) {
         mSurfaceControlFactory = surfaceControlFactory;
         mTransactionFactory = transactionFactory;
         mAreCornersRounded = areCornersRounded;
@@ -97,6 +99,7 @@
         mDarkScrimAlphaSupplier = darkScrimAlphaSupplier;
         mDoubleTapCallbackX = doubleTapCallbackX;
         mDoubleTapCallbackY = doubleTapCallbackY;
+        mParentSurfaceSupplier = parentSurface;
     }
 
     /**
@@ -121,7 +124,6 @@
         mFullWindowSurface.layout(outer.left, outer.top, outer.right, outer.bottom, surfaceOrigin);
     }
 
-
     /**
      * Gets the insets between the outer and inner rects.
      */
@@ -333,6 +335,7 @@
         private SurfaceControl mSurface;
         private Color mColor;
         private boolean mHasWallpaperBackground;
+        private SurfaceControl mParentSurface;
 
         private final Rect mSurfaceFrameRelative = new Rect();
         private final Rect mLayoutFrameGlobal = new Rect();
@@ -403,10 +406,12 @@
                 }
 
                 mColor = mColorSupplier.get();
+                mParentSurface = mParentSurfaceSupplier.get();
                 t.setColor(mSurface, getRgbColorArray());
                 t.setPosition(mSurface, mSurfaceFrameRelative.left, mSurfaceFrameRelative.top);
                 t.setWindowCrop(mSurface, mSurfaceFrameRelative.width(),
                         mSurfaceFrameRelative.height());
+                t.reparent(mSurface, mParentSurface);
 
                 mHasWallpaperBackground = mHasWallpaperBackgroundSupplier.get();
                 updateAlphaAndBlur(t);
@@ -452,12 +457,13 @@
 
         public boolean needsApplySurfaceChanges() {
             return !mSurfaceFrameRelative.equals(mLayoutFrameRelative)
-                    // If mSurfaceFrameRelative is empty then mHasWallpaperBackground and mColor
-                    // may never be updated in applySurfaceChanges but this doesn't mean that
-                    // update is needed.
+                    // If mSurfaceFrameRelative is empty then mHasWallpaperBackground, mColor,
+                    // and mParentSurface may never be updated in applySurfaceChanges but this
+                    // doesn't mean that update is needed.
                     || !mSurfaceFrameRelative.isEmpty()
                     && (mHasWallpaperBackgroundSupplier.get() != mHasWallpaperBackground
-                    || !mColorSupplier.get().equals(mColor));
+                    || !mColorSupplier.get().equals(mColor)
+                    || mParentSurfaceSupplier.get() != mParentSurface);
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 57c60f4..a469c6b 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -265,7 +265,7 @@
     }
 
     /**
-     * Overrides corners raidus for activities presented in the letterbox mode. If given value < 0,
+     * Overrides corners radius 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.
@@ -275,7 +275,7 @@
     }
 
     /**
-     * Resets corners raidus for activities presented in the letterbox mode to {@link
+     * Resets corners radius for activities presented in the letterbox mode to {@link
      * com.android.internal.R.integer.config_letterboxActivityCornersRadius}.
      */
     void resetLetterboxActivityCornersRadius() {
@@ -291,7 +291,7 @@
     }
 
     /**
-     * Gets corners raidus for activities presented in the letterbox mode.
+     * Gets corners radius for activities presented in the letterbox mode.
      */
     int getLetterboxActivityCornersRadius() {
         return mLetterboxActivityCornersRadius;
@@ -318,7 +318,7 @@
     /**
      * Sets color of letterbox background which is used when {@link
      * #getLetterboxBackgroundType()} is {@link #LETTERBOX_BACKGROUND_SOLID_COLOR} or as
-     * fallback for other backfround types.
+     * fallback for other background types.
      */
     void setLetterboxBackgroundColor(Color color) {
         mLetterboxBackgroundColorOverride = color;
@@ -327,7 +327,7 @@
     /**
      * 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.
+     * fallback for other background types.
      */
     void setLetterboxBackgroundColorResourceId(int colorId) {
         mLetterboxBackgroundColorResourceIdOverride = colorId;
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index c8ed602..317c93e 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -89,7 +89,7 @@
 
     // Taskbar expanded height. Used to determine whether to crop an app window to display rounded
     // corners above the taskbar.
-    private float mExpandedTaskBarHeight;
+    private final float mExpandedTaskBarHeight;
 
     private boolean mShowWallpaperForLetterboxBackground;
 
@@ -120,7 +120,7 @@
         }
     }
 
-    boolean hasWallpaperBackgroudForLetterbox() {
+    boolean hasWallpaperBackgroundForLetterbox() {
         return mShowWallpaperForLetterboxBackground;
     }
 
@@ -137,6 +137,11 @@
     void getLetterboxInnerBounds(Rect outBounds) {
         if (mLetterbox != null) {
             outBounds.set(mLetterbox.getInnerFrame());
+            final WindowState w = mActivityRecord.findMainWindow();
+            if (w == null) {
+                return;
+            }
+            adjustBoundsForTaskbar(w, outBounds);
         } else {
             outBounds.setEmpty();
         }
@@ -160,13 +165,17 @@
     }
 
     void updateLetterboxSurface(WindowState winHint) {
+        updateLetterboxSurface(winHint, mActivityRecord.getSyncTransaction());
+    }
+
+    void updateLetterboxSurface(WindowState winHint, Transaction t) {
         final WindowState w = mActivityRecord.findMainWindow();
         if (w != winHint && winHint != null && w != null) {
             return;
         }
         layoutLetterbox(winHint);
         if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) {
-            mLetterbox.applySurfaceChanges(mActivityRecord.getSyncTransaction());
+            mLetterbox.applySurfaceChanges(t);
         }
     }
 
@@ -191,14 +200,22 @@
                         mActivityRecord.mWmService.mTransactionFactory,
                         this::shouldLetterboxHaveRoundedCorners,
                         this::getLetterboxBackgroundColor,
-                        this::hasWallpaperBackgroudForLetterbox,
+                        this::hasWallpaperBackgroundForLetterbox,
                         this::getLetterboxWallpaperBlurRadius,
                         this::getLetterboxWallpaperDarkScrimAlpha,
                         this::handleHorizontalDoubleTap,
-                        this::handleVerticalDoubleTap);
+                        this::handleVerticalDoubleTap,
+                        this::getLetterboxParentSurface);
                 mLetterbox.attachInput(w);
             }
-            mActivityRecord.getPosition(mTmpPoint);
+
+            if (mActivityRecord.isInLetterboxAnimation()) {
+                // In this case we attach the letterbox to the task instead of the activity.
+                mActivityRecord.getTask().getPosition(mTmpPoint);
+            } else {
+                mActivityRecord.getPosition(mTmpPoint);
+            }
+
             // Get the bounds of the "space-to-fill". The transformed bounds have the highest
             // priority because the activity is launched in a rotated environment. In multi-window
             // mode, the task-level represents this. In fullscreen-mode, the task container does
@@ -215,6 +232,13 @@
         }
     }
 
+    SurfaceControl getLetterboxParentSurface() {
+        if (mActivityRecord.isInLetterboxAnimation()) {
+            return mActivityRecord.getTask().getSurfaceControl();
+        }
+        return mActivityRecord.getSurfaceControl();
+    }
+
     private boolean shouldLetterboxHaveRoundedCorners() {
         // TODO(b/214030873): remove once background is drawn for transparent activities
         // Letterbox shouldn't have rounded corners if the activity is transparent
@@ -436,7 +460,7 @@
                 }
                 break;
             case LETTERBOX_BACKGROUND_WALLPAPER:
-                if (hasWallpaperBackgroudForLetterbox()) {
+                if (hasWallpaperBackgroundForLetterbox()) {
                     // Color is used for translucent scrim that dims wallpaper.
                     return Color.valueOf(Color.BLACK);
                 }
@@ -459,15 +483,14 @@
     private void updateRoundedCorners(WindowState mainWindow) {
         final SurfaceControl windowSurface = mainWindow.getClientViewRootSurface();
         if (windowSurface != null && windowSurface.isValid()) {
-            Transaction transaction = mActivityRecord.getSyncTransaction();
+            final Transaction transaction = mActivityRecord.getSyncTransaction();
 
-            final InsetsState insetsState = mainWindow.getInsetsState();
-            final InsetsSource taskbarInsetsSource =
-                    insetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
-
-            if (!isLetterboxedNotForDisplayCutout(mainWindow)
-                    || !mLetterboxConfiguration.isLetterboxActivityCornersRounded()
-                    || taskbarInsetsSource == null) {
+            if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) {
+                // We don't want corner radius on the window.
+                // In the case the ActivityRecord requires a letterboxed animation we never want
+                // rounded corners on the window because rounded corners are applied at the
+                // animation-bounds surface level and rounded corners on the window would interfere
+                // with that leading to unexpected rounded corner positioning during the animation.
                 transaction
                         .setWindowCrop(windowSurface, null)
                         .setCornerRadius(windowSurface, 0);
@@ -476,48 +499,89 @@
 
             Rect cropBounds = null;
 
-            // Rounded corners should be displayed above the taskbar. When taskbar is hidden,
-            // an insets frame is equal to a navigation bar which shouldn't affect position of
-            // rounded corners since apps are expected to handle navigation bar inset.
-            // This condition checks whether the taskbar is visible.
-            // Do not crop the taskbar inset if the window is in immersive mode - the user can
-            // swipe to show/hide the taskbar as an overlay.
-            if (taskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight
-                    && taskbarInsetsSource.isVisible()) {
+            if (hasVisibleTaskbar(mainWindow)) {
                 cropBounds = new Rect(mActivityRecord.getBounds());
                 // Activity bounds are in screen coordinates while (0,0) for activity's surface
                 // control is at the top left corner of an app window so offsetting bounds
                 // accordingly.
                 cropBounds.offsetTo(0, 0);
-                // Rounded cornerners should be displayed above the taskbar.
-                cropBounds.bottom =
-                        Math.min(cropBounds.bottom, taskbarInsetsSource.getFrame().top);
-                if (mActivityRecord.inSizeCompatMode()
-                        && mActivityRecord.getSizeCompatScale() < 1.0f) {
-                    cropBounds.scale(1.0f / mActivityRecord.getSizeCompatScale());
-                }
+                // Rounded corners should be displayed above the taskbar.
+                adjustBoundsForTaskbarUnchecked(mainWindow, cropBounds);
             }
 
             transaction
                     .setWindowCrop(windowSurface, cropBounds)
-                    .setCornerRadius(windowSurface, getRoundedCorners(insetsState));
+                    .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow));
         }
     }
 
-    // Returns rounded corners radius based on override in
+    private boolean requiresRoundedCorners(WindowState mainWindow) {
+        final InsetsSource taskbarInsetsSource = getTaskbarInsetsSource(mainWindow);
+
+        return isLetterboxedNotForDisplayCutout(mainWindow)
+                && mLetterboxConfiguration.isLetterboxActivityCornersRounded()
+                && taskbarInsetsSource != null;
+    }
+
+    // Returns rounded corners radius the letterboxed activity should have based on override in
     // R.integer.config_letterboxActivityCornersRadius or min device bottom corner radii.
     // Device corners can be different on the right and left sides but we use the same radius
     // for all corners for consistency and pick a minimal bottom one for consistency with a
     // taskbar rounded corners.
-    private int getRoundedCorners(InsetsState insetsState) {
+    int getRoundedCornersRadius(WindowState mainWindow) {
+        if (!requiresRoundedCorners(mainWindow)) {
+            return 0;
+        }
+
         if (mLetterboxConfiguration.getLetterboxActivityCornersRadius() >= 0) {
             return mLetterboxConfiguration.getLetterboxActivityCornersRadius();
         }
+
+        final InsetsState insetsState = mainWindow.getInsetsState();
         return Math.min(
                 getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT),
                 getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT));
     }
 
+    /**
+     * Returns whether the taskbar is visible. Returns false if the window is in immersive mode,
+     * since the user can swipe to show/hide the taskbar as an overlay.
+     */
+    private boolean hasVisibleTaskbar(WindowState mainWindow) {
+        final InsetsSource taskbarInsetsSource = getTaskbarInsetsSource(mainWindow);
+
+        return taskbarInsetsSource != null
+                && taskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight
+                && taskbarInsetsSource.isVisible();
+    }
+
+    private InsetsSource getTaskbarInsetsSource(WindowState mainWindow) {
+        final InsetsState insetsState = mainWindow.getInsetsState();
+        return insetsState.peekSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+    }
+
+    private void adjustBoundsForTaskbar(WindowState mainWindow, Rect bounds) {
+        // Rounded corners should be displayed above the taskbar. When taskbar is hidden,
+        // an insets frame is equal to a navigation bar which shouldn't affect position of
+        // rounded corners since apps are expected to handle navigation bar inset.
+        // This condition checks whether the taskbar is visible.
+        // Do not crop the taskbar inset if the window is in immersive mode - the user can
+        // swipe to show/hide the taskbar as an overlay.
+        if (hasVisibleTaskbar(mainWindow)) {
+            adjustBoundsForTaskbarUnchecked(mainWindow, bounds);
+        }
+    }
+
+    private void adjustBoundsForTaskbarUnchecked(WindowState mainWindow, Rect bounds) {
+        // Rounded corners should be displayed above the taskbar.
+        bounds.bottom =
+                Math.min(bounds.bottom, getTaskbarInsetsSource(mainWindow).getFrame().top);
+        if (mActivityRecord.inSizeCompatMode()
+                && mActivityRecord.getSizeCompatScale() < 1.0f) {
+            bounds.scale(1.0f / mActivityRecord.getSizeCompatScale());
+        }
+    }
+
     private int getInsetsStateCornerRadius(
                 InsetsState insetsState, @RoundedCorner.Position int position) {
         RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position);
@@ -592,7 +656,7 @@
                 + letterboxBackgroundTypeToString(
                         mLetterboxConfiguration.getLetterboxBackgroundType()));
         pw.println(prefix + "  letterboxCornerRadius="
-                + getRoundedCorners(mainWin.getInsetsState()));
+                + getRoundedCornersRadius(mainWin));
         if (mLetterboxConfiguration.getLetterboxBackgroundType()
                 == LETTERBOX_BACKGROUND_WALLPAPER) {
             pw.println(prefix + "  isLetterboxWallpaperBlurSupported="
diff --git a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
index 64749cf..a89894d 100644
--- a/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
+++ b/services/core/java/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncher.java
@@ -101,7 +101,6 @@
 
         if (t != null) {
             mDisplayContent.mAtmService.startLaunchPowerMode(POWER_MODE_REASON_CHANGE_DISPLAY);
-            mTransitionController.collectForDisplayAreaChange(mDisplayContent, t);
             mTransition = t;
         }
     }
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java b/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java
index c7f8a1e..f3670e4 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimationRunner.java
@@ -27,7 +27,10 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.annotation.Nullable;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
 import android.graphics.Insets;
+import android.graphics.Paint;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
 import android.hardware.power.Boost;
@@ -374,43 +377,45 @@
         final int targetSurfaceWidth = bounds.width();
 
         if (maxExtensionInsets.left < 0) {
-            final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
+            final Rect edgeBounds = new Rect(bounds.left, bounds.top, bounds.left + 1,
+                    bounds.bottom);
             final Rect extensionRect = new Rect(0, 0,
                     -maxExtensionInsets.left, targetSurfaceHeight);
-            final int xPos = maxExtensionInsets.left;
-            final int yPos = 0;
+            final int xPos = bounds.left + maxExtensionInsets.left;
+            final int yPos = bounds.top;
             createExtensionSurface(leash, edgeBounds,
                     extensionRect, xPos, yPos, "Left Edge Extension", transaction);
         }
 
         if (maxExtensionInsets.top < 0) {
-            final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
+            final Rect edgeBounds = new Rect(bounds.left, bounds.top, targetSurfaceWidth,
+                    bounds.top + 1);
             final Rect extensionRect = new Rect(0, 0,
                     targetSurfaceWidth, -maxExtensionInsets.top);
-            final int xPos = 0;
-            final int yPos = maxExtensionInsets.top;
+            final int xPos = bounds.left;
+            final int yPos = bounds.top + maxExtensionInsets.top;
             createExtensionSurface(leash, edgeBounds,
                     extensionRect, xPos, yPos, "Top Edge Extension", transaction);
         }
 
         if (maxExtensionInsets.right < 0) {
-            final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
-                    targetSurfaceWidth, targetSurfaceHeight);
+            final Rect edgeBounds = new Rect(bounds.right - 1, bounds.top, bounds.right,
+                    bounds.bottom);
             final Rect extensionRect = new Rect(0, 0,
                     -maxExtensionInsets.right, targetSurfaceHeight);
-            final int xPos = targetSurfaceWidth;
-            final int yPos = 0;
+            final int xPos = bounds.right;
+            final int yPos = bounds.top;
             createExtensionSurface(leash, edgeBounds,
                     extensionRect, xPos, yPos, "Right Edge Extension", transaction);
         }
 
         if (maxExtensionInsets.bottom < 0) {
-            final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
-                    targetSurfaceWidth, targetSurfaceHeight);
+            final Rect edgeBounds = new Rect(bounds.left, bounds.bottom - 1,
+                    bounds.right, bounds.bottom);
             final Rect extensionRect = new Rect(0, 0,
                     targetSurfaceWidth, -maxExtensionInsets.bottom);
-            final int xPos = maxExtensionInsets.left;
-            final int yPos = targetSurfaceHeight;
+            final int xPos = bounds.left;
+            final int yPos = bounds.bottom;
             createExtensionSurface(leash, edgeBounds,
                     extensionRect, xPos, yPos, "Bottom Edge Extension", transaction);
         }
@@ -453,16 +458,20 @@
                 .setHidden(true)
                 .setCallsite("DefaultTransitionHandler#startAnimation")
                 .setOpaque(true)
-                .setBufferSize(edgeBounds.width(), edgeBounds.height())
+                .setBufferSize(extensionRect.width(), extensionRect.height())
                 .build();
 
-        final Surface surface = new Surface(edgeExtensionLayer);
-        surface.attachAndQueueBufferWithColorSpace(edgeBuffer.getHardwareBuffer(),
-                edgeBuffer.getColorSpace());
-        surface.release();
+        BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(),
+                android.graphics.Shader.TileMode.CLAMP,
+                android.graphics.Shader.TileMode.CLAMP);
+        final Paint paint = new Paint();
+        paint.setShader(shader);
 
-        final float scaleX = getScaleXForExtensionSurface(edgeBounds, extensionRect);
-        final float scaleY = getScaleYForExtensionSurface(edgeBounds, extensionRect);
+        final Surface surface = new Surface(edgeExtensionLayer);
+        Canvas c = surface.lockHardwareCanvas();
+        c.drawRect(extensionRect, paint);
+        surface.unlockCanvasAndPost(c);
+        surface.release();
 
         synchronized (mEdgeExtensionLock) {
             if (!mEdgeExtensions.containsKey(leash)) {
@@ -472,7 +481,6 @@
                 return;
             }
 
-            startTransaction.setScale(edgeExtensionLayer, scaleX, scaleY);
             startTransaction.reparent(edgeExtensionLayer, leash);
             startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
             startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
@@ -508,8 +516,6 @@
         throw new RuntimeException("Unexpected edgeBounds and extensionRect heights");
     }
 
-
-
     private static final class RunningAnimation {
         final AnimationSpec mAnimSpec;
         final SurfaceControl mLeash;
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index c1f06e4..44b5b88 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2340,7 +2340,10 @@
             return false;
         }
 
-        return !startBounds.equals(getBounds());
+        // Only take snapshot if the bounds are resized.
+        final Rect endBounds = getConfiguration().windowConfiguration.getBounds();
+        return endBounds.width() != startBounds.width()
+                || endBounds.height() != startBounds.height();
     }
 
     boolean canHaveEmbeddingActivityTransition(@NonNull ActivityRecord child) {
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 6ca5648..88059e1 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -18,7 +18,7 @@
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.window.TaskFragmentOrganizer.putErrorInfoInBundle;
-import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENT_TO_TASK;
+import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED;
@@ -277,7 +277,7 @@
         }
 
         @Nullable
-        TaskFragmentTransaction.Change prepareActivityReparentToTask(
+        TaskFragmentTransaction.Change prepareActivityReparentedToTask(
                 @NonNull ActivityRecord activity) {
             if (activity.finishing) {
                 Slog.d(TAG, "Reparent activity=" + activity.token + " is finishing");
@@ -315,7 +315,7 @@
             }
             ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Activity=%s reparent to taskId=%d",
                     activity.token, task.mTaskId);
-            return new TaskFragmentTransaction.Change(TYPE_ACTIVITY_REPARENT_TO_TASK)
+            return new TaskFragmentTransaction.Change(TYPE_ACTIVITY_REPARENTED_TO_TASK)
                     .setTaskId(task.mTaskId)
                     .setActivityIntent(activity.intent)
                     .setActivityToken(activityToken);
@@ -521,7 +521,7 @@
         mAtmService.mWindowManager.mWindowPlacerLocked.requestTraversal();
     }
 
-    void onActivityReparentToTask(@NonNull ActivityRecord activity) {
+    void onActivityReparentedToTask(@NonNull ActivityRecord activity) {
         final ITaskFragmentOrganizer organizer;
         if (activity.mLastTaskFragmentOrganizerBeforePip != null) {
             // If the activity is previously embedded in an organized TaskFragment.
@@ -547,7 +547,7 @@
             return;
         }
         addPendingEvent(new PendingTaskFragmentEvent.Builder(
-                PendingTaskFragmentEvent.EVENT_ACTIVITY_REPARENT_TO_TASK, organizer)
+                PendingTaskFragmentEvent.EVENT_ACTIVITY_REPARENTED_TO_TASK, organizer)
                 .setActivity(activity)
                 .build());
     }
@@ -601,7 +601,7 @@
         static final int EVENT_INFO_CHANGED = 2;
         static final int EVENT_PARENT_INFO_CHANGED = 3;
         static final int EVENT_ERROR = 4;
-        static final int EVENT_ACTIVITY_REPARENT_TO_TASK = 5;
+        static final int EVENT_ACTIVITY_REPARENTED_TO_TASK = 5;
 
         @IntDef(prefix = "EVENT_", value = {
                 EVENT_APPEARED,
@@ -609,7 +609,7 @@
                 EVENT_INFO_CHANGED,
                 EVENT_PARENT_INFO_CHANGED,
                 EVENT_ERROR,
-                EVENT_ACTIVITY_REPARENT_TO_TASK
+                EVENT_ACTIVITY_REPARENTED_TO_TASK
         })
         @Retention(RetentionPolicy.SOURCE)
         public @interface EventType {}
@@ -900,8 +900,8 @@
             case PendingTaskFragmentEvent.EVENT_ERROR:
                 return state.prepareTaskFragmentError(event.mErrorCallbackToken, taskFragment,
                         event.mOpType, event.mException);
-            case PendingTaskFragmentEvent.EVENT_ACTIVITY_REPARENT_TO_TASK:
-                return state.prepareActivityReparentToTask(event.mActivity);
+            case PendingTaskFragmentEvent.EVENT_ACTIVITY_REPARENTED_TO_TASK:
+                return state.prepareActivityReparentedToTask(event.mActivity);
             default:
                 throw new IllegalArgumentException("Unknown TaskFragmentEvent=" + event.mEventType);
         }
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 584a40e..803890b 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1205,7 +1205,14 @@
             return false;
         }
 
-        final @TransitionInfo.TransitionMode int mode = changes.get(target).getTransitMode(target);
+        final ChangeInfo change = changes.get(target);
+        if (change.mStartParent != null && target.getParent() != change.mStartParent) {
+            // When a window is reparented, the state change won't fit into any of the parents.
+            // Don't promote such change so that we can animate the reparent if needed.
+            return false;
+        }
+
+        final @TransitionInfo.TransitionMode int mode = change.getTransitMode(target);
         for (int i = parent.getChildCount() - 1; i >= 0; --i) {
             final WindowContainer<?> sibling = parent.getChildAt(i);
             if (target == sibling) continue;
@@ -1345,14 +1352,14 @@
                     // Intermediate parents must be those that has window to be managed by Shell.
                     continue;
                 }
-                if (parentChange.mParent != null && !skipIntermediateReports) {
-                    changes.get(wc).mParent = p;
+                if (parentChange.mEndParent != null && !skipIntermediateReports) {
+                    changes.get(wc).mEndParent = p;
                     // The chain above the parent was processed.
                     break;
                 }
                 if (targetList.contains(p)) {
                     if (skipIntermediateReports) {
-                        changes.get(wc).mParent = p;
+                        changes.get(wc).mEndParent = p;
                     } else {
                         intermediates.add(p);
                     }
@@ -1364,10 +1371,10 @@
             }
             if (!foundParentInTargets || intermediates.isEmpty()) continue;
             // Add any always-report parents along the way.
-            changes.get(wc).mParent = intermediates.get(0);
+            changes.get(wc).mEndParent = intermediates.get(0);
             for (int j = 0; j < intermediates.size() - 1; j++) {
                 final WindowContainer<?> intermediate = intermediates.get(j);
-                changes.get(intermediate).mParent = intermediates.get(j + 1);
+                changes.get(intermediate).mEndParent = intermediates.get(j + 1);
                 targets.add(intermediate);
             }
         }
@@ -1480,8 +1487,8 @@
                     target.mRemoteToken != null ? target.mRemoteToken.toWindowContainerToken()
                             : null, getLeashSurface(target, startT));
             // TODO(shell-transitions): Use leash for non-organized windows.
-            if (info.mParent != null) {
-                change.setParent(info.mParent.mRemoteToken.toWindowContainerToken());
+            if (info.mEndParent != null) {
+                change.setParent(info.mEndParent.mRemoteToken.toWindowContainerToken());
             }
             change.setMode(info.getTransitMode(target));
             change.setStartAbsBounds(info.mAbsoluteBounds);
@@ -1634,6 +1641,19 @@
         return mainWin.getAttrs().rotationAnimation;
     }
 
+    /** Applies the new configuration and returns {@code true} if there is a display change. */
+    boolean applyDisplayChangeIfNeeded() {
+        boolean changed = false;
+        for (int i = mParticipants.size() - 1; i >= 0; --i) {
+            final WindowContainer<?> wc = mParticipants.valueAt(i);
+            final DisplayContent dc = wc.asDisplayContent();
+            if (dc == null || !mChanges.get(dc).hasChanged(dc)) continue;
+            dc.sendNewConfiguration();
+            changed = true;
+        }
+        return changed;
+    }
+
     boolean getLegacyIsReady() {
         return isCollecting() && mSyncId >= 0;
     }
@@ -1664,7 +1684,9 @@
         @interface Flag {}
 
         // Usually "post" change state.
-        WindowContainer mParent;
+        WindowContainer mEndParent;
+        // Parent before change state.
+        WindowContainer mStartParent;
 
         // State tracking
         boolean mExistenceChanged = false;
@@ -1685,6 +1707,7 @@
             mAbsoluteBounds.set(origState.getBounds());
             mShowWallpaper = origState.showWallpaper();
             mRotation = origState.getWindowConfiguration().getRotation();
+            mStartParent = origState.getParent();
         }
 
         @VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 4f324f2..846aa3e 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -463,13 +463,12 @@
     }
 
     /**
-     * Collects the window containers which need to be synced with the changing display (e.g.
-     * rotating) to the given transition or the current collecting transition.
+     * Collects the window containers which need to be synced with the changing display area into
+     * the current collecting transition.
      */
-    void collectForDisplayAreaChange(@NonNull DisplayArea<?> wc, @Nullable Transition incoming) {
-        if (incoming == null) incoming = mCollectingTransition;
-        if (incoming == null) return;
-        final Transition transition = incoming;
+    void collectForDisplayAreaChange(@NonNull DisplayArea<?> wc) {
+        final Transition transition = mCollectingTransition;
+        if (transition == null || !transition.mParticipants.contains(wc)) return;
         // Collect all visible tasks.
         wc.forAllLeafTasks(task -> {
             if (task.isVisible()) {
diff --git a/services/core/java/com/android/server/wm/UnsupportedCompileSdkDialog.java b/services/core/java/com/android/server/wm/UnsupportedCompileSdkDialog.java
index 6a878b9..f376e8b 100644
--- a/services/core/java/com/android/server/wm/UnsupportedCompileSdkDialog.java
+++ b/services/core/java/com/android/server/wm/UnsupportedCompileSdkDialog.java
@@ -29,13 +29,11 @@
 import com.android.internal.R;
 import com.android.server.utils.AppInstallerUtil;
 
-public class UnsupportedCompileSdkDialog {
-    private final AlertDialog mDialog;
-    private final String mPackageName;
+class UnsupportedCompileSdkDialog extends AppWarnings.BaseDialog {
 
-    public UnsupportedCompileSdkDialog(final AppWarnings manager, Context context,
+    UnsupportedCompileSdkDialog(final AppWarnings manager, Context context,
             ApplicationInfo appInfo) {
-        mPackageName = appInfo.packageName;
+        super(manager, appInfo.packageName);
 
         final PackageManager pm = context.getPackageManager();
         final CharSequence label = appInfo.loadSafeLabel(pm,
@@ -72,16 +70,4 @@
         alwaysShow.setOnCheckedChangeListener((buttonView, isChecked) -> manager.setPackageFlag(
                 mPackageName, AppWarnings.FLAG_HIDE_COMPILE_SDK, !isChecked));
     }
-
-    public String getPackageName() {
-        return mPackageName;
-    }
-
-    public void show() {
-        mDialog.show();
-    }
-
-    public void dismiss() {
-        mDialog.dismiss();
-    }
 }
diff --git a/services/core/java/com/android/server/wm/UnsupportedDisplaySizeDialog.java b/services/core/java/com/android/server/wm/UnsupportedDisplaySizeDialog.java
index 4a800c4..b11c22d 100644
--- a/services/core/java/com/android/server/wm/UnsupportedDisplaySizeDialog.java
+++ b/services/core/java/com/android/server/wm/UnsupportedDisplaySizeDialog.java
@@ -16,8 +16,6 @@
 
 package com.android.server.wm;
 
-import com.android.internal.R;
-
 import android.app.AlertDialog;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
@@ -27,13 +25,13 @@
 import android.view.WindowManager;
 import android.widget.CheckBox;
 
-public class UnsupportedDisplaySizeDialog {
-    private final AlertDialog mDialog;
-    private final String mPackageName;
+import com.android.internal.R;
 
-    public UnsupportedDisplaySizeDialog(final AppWarnings manager, Context context,
+class UnsupportedDisplaySizeDialog extends AppWarnings.BaseDialog {
+
+    UnsupportedDisplaySizeDialog(final AppWarnings manager, Context context,
             ApplicationInfo appInfo) {
-        mPackageName = appInfo.packageName;
+        super(manager, appInfo.packageName);
 
         final PackageManager pm = context.getPackageManager();
         final CharSequence label = appInfo.loadSafeLabel(pm,
@@ -63,16 +61,4 @@
         alwaysShow.setOnCheckedChangeListener((buttonView, isChecked) -> manager.setPackageFlag(
                 mPackageName, AppWarnings.FLAG_HIDE_DISPLAY_SIZE, !isChecked));
     }
-
-    public String getPackageName() {
-        return mPackageName;
-    }
-
-    public void show() {
-        mDialog.show();
-    }
-
-    public void dismiss() {
-        mDialog.dismiss();
-    }
 }
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 70dd9f3..73658b2 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3019,6 +3019,10 @@
                 final float windowCornerRadius = !inMultiWindowMode()
                         ? getDisplayContent().getWindowCornerRadius()
                         : 0;
+                if (asActivityRecord() != null
+                        && asActivityRecord().isNeedsLetterboxedAnimation()) {
+                    asActivityRecord().getLetterboxInnerBounds(mTmpRect);
+                }
                 AnimationAdapter adapter = new LocalAnimationAdapter(
                         new WindowAnimationSpec(a, mTmpPoint, mTmpRect,
                                 getDisplayContent().mAppTransition.canSkipFirstFrame(),
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 3ee4be0..22411bb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -88,6 +88,7 @@
 import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED;
 import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_RELAUNCH;
+import static android.view.WindowManager.fixScale;
 import static android.view.WindowManagerGlobal.ADD_OKAY;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED;
@@ -1326,15 +1327,10 @@
         }, UserHandle.ALL, suspendPackagesFilter, null, null);
 
         // Get persisted window scale setting
-        mWindowAnimationScaleSetting = Settings.Global.getFloat(resolver,
-                Settings.Global.WINDOW_ANIMATION_SCALE, mWindowAnimationScaleSetting);
-        mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver,
-                Settings.Global.TRANSITION_ANIMATION_SCALE,
-                context.getResources().getFloat(
-                        R.dimen.config_appTransitionAnimationDurationScaleDefault));
+        mWindowAnimationScaleSetting = getWindowAnimationScaleSetting();
+        mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
 
-        setAnimatorDurationScale(Settings.Global.getFloat(resolver,
-                Settings.Global.ANIMATOR_DURATION_SCALE, mAnimatorDurationScaleSetting));
+        setAnimatorDurationScale(getAnimatorDurationScaleSetting());
 
         mForceDesktopModeOnExternalDisplays = Settings.Global.getInt(resolver,
                 DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0;
@@ -1408,6 +1404,22 @@
                 lightRadius);
     }
 
+    private float getTransitionAnimationScaleSetting() {
+        return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
+                                R.dimen.config_appTransitionAnimationDurationScaleDefault)));
+    }
+
+    private float getAnimatorDurationScaleSetting() {
+        return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.ANIMATOR_DURATION_SCALE, mAnimatorDurationScaleSetting));
+    }
+
+    private float getWindowAnimationScaleSetting() {
+        return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.WINDOW_ANIMATION_SCALE, mWindowAnimationScaleSetting));
+    }
+
     /**
      * Called after all entities (such as the {@link ActivityManagerService}) have been set up and
      * associated with the {@link WindowManagerService}.
@@ -3407,11 +3419,6 @@
         }
     }
 
-    static float fixScale(float scale) {
-        if (scale < 0) scale = 0;
-        else if (scale > 20) scale = 20;
-        return Math.abs(scale);
-    }
 
     @Override
     public void setAnimationScale(int which, float scale) {
@@ -5339,24 +5346,16 @@
                     final int mode = msg.arg1;
                     switch (mode) {
                         case WINDOW_ANIMATION_SCALE: {
-                            mWindowAnimationScaleSetting = Settings.Global.getFloat(
-                                    mContext.getContentResolver(),
-                                    Settings.Global.WINDOW_ANIMATION_SCALE,
-                                    mWindowAnimationScaleSetting);
+                            mWindowAnimationScaleSetting = getWindowAnimationScaleSetting();
                             break;
                         }
                         case TRANSITION_ANIMATION_SCALE: {
-                            mTransitionAnimationScaleSetting = Settings.Global.getFloat(
-                                    mContext.getContentResolver(),
-                                    Settings.Global.TRANSITION_ANIMATION_SCALE,
-                                    mTransitionAnimationScaleSetting);
+                            mTransitionAnimationScaleSetting =
+                                    getTransitionAnimationScaleSetting();
                             break;
                         }
                         case ANIMATION_DURATION_SCALE: {
-                            mAnimatorDurationScaleSetting = Settings.Global.getFloat(
-                                    mContext.getContentResolver(),
-                                    Settings.Global.ANIMATOR_DURATION_SCALE,
-                                    mAnimatorDurationScaleSetting);
+                            mAnimatorDurationScaleSetting = getAnimatorDurationScaleSetting();
                             dispatchNewAnimatorScaleLocked(null);
                             break;
                         }
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 3b9cd36..4f03264 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -397,17 +397,8 @@
         mService.deferWindowLayout();
         mService.mTaskSupervisor.setDeferRootVisibilityUpdate(true /* deferUpdate */);
         try {
-            if (transition != null) {
-                // First check if we have a display rotation transition and if so, update it.
-                final DisplayContent dc = DisplayRotation.getDisplayFromTransition(transition);
-                if (dc != null && transition.mChanges.get(dc).hasChanged(dc)) {
-                    // Go through all tasks and collect them before the rotation
-                    // TODO(shell-transitions): move collect() to onConfigurationChange once
-                    //       wallpaper handling is synchronized.
-                    dc.mTransitionController.collectForDisplayAreaChange(dc, transition);
-                    dc.sendNewConfiguration();
-                    effects |= TRANSACT_EFFECTS_LIFECYCLE;
-                }
+            if (transition != null && transition.applyDisplayChangeIfNeeded()) {
+                effects |= TRANSACT_EFFECTS_LIFECYCLE;
             }
             final List<WindowContainerTransaction.HierarchyOp> hops = t.getHierarchyOps();
             final int hopSize = hops.size();
@@ -428,15 +419,6 @@
                     addToSyncSet(syncId, wc);
                 }
                 if (transition != null) transition.collect(wc);
-                final DisplayArea da = wc.asDisplayArea();
-                // Only check DisplayArea here as a similar thing is done for DisplayContent above.
-                if (da != null && wc.asDisplayContent() == null
-                        && entry.getValue().getWindowingMode() != da.getWindowingMode()) {
-                    // Go through all tasks and collect them before changing the windowing mode of a
-                    // display-level container.
-                    // TODO(shell-transitions): handle this more elegantly.
-                    da.mTransitionController.collectForDisplayAreaChange(da, transition);
-                }
 
                 if ((entry.getValue().getChangeMask()
                         & WindowContainerTransaction.Change.CHANGE_FORCE_NO_PIP) != 0) {
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 95b0645..8f63e93 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -27,6 +27,7 @@
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.server.am.ProcessList.INVALID_ADJ;
 import static com.android.server.wm.ActivityRecord.State.DESTROYED;
 import static com.android.server.wm.ActivityRecord.State.DESTROYING;
 import static com.android.server.wm.ActivityRecord.State.PAUSED;
@@ -125,6 +126,8 @@
     private volatile int mCurProcState = PROCESS_STATE_NONEXISTENT;
     // Last reported process state;
     private volatile int mRepProcState = PROCESS_STATE_NONEXISTENT;
+    // Currently computed oom adj score
+    private volatile int mCurAdj = INVALID_ADJ;
     // are we in the process of crashing?
     private volatile boolean mCrashing;
     // does the app have a not responding dialog?
@@ -319,6 +322,14 @@
         return mCurProcState;
     }
 
+    public void setCurrentAdj(int curAdj) {
+        mCurAdj = curAdj;
+    }
+
+    int getCurrentAdj() {
+        return mCurAdj;
+    }
+
     /**
      * Sets the computed process state from the oom adjustment calculation. This is frequently
      * called in activity manager's lock, so don't use window manager lock here.
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index d222a56..d2b9bda 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3802,6 +3802,10 @@
         return wpc != null && wpc.registeredForDisplayAreaConfigChanges();
     }
 
+    WindowProcessController getProcess() {
+        return mWpcForDisplayAreaConfigChanges;
+    }
+
     /**
      * Fills the given window frames and merged configuration for the client.
      *
@@ -6037,7 +6041,7 @@
     }
 
     boolean hasWallpaperForLetterboxBackground() {
-        return mActivityRecord != null && mActivityRecord.hasWallpaperBackgroudForLetterbox();
+        return mActivityRecord != null && mActivityRecord.hasWallpaperBackgroundForLetterbox();
     }
 
     /**
diff --git a/services/core/xsd/display-device-config/autobrightness.xsd b/services/core/xsd/display-device-config/autobrightness.xsd
new file mode 100644
index 0000000..477625a
--- /dev/null
+++ b/services/core/xsd/display-device-config/autobrightness.xsd
@@ -0,0 +1,33 @@
+<!--
+    Copyright (C) 2022 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<xs:schema version="2.0"
+           elementFormDefault="qualified"
+           xmlns:xs="http://www.w3.org/2001/XMLSchema">
+    <xs:complexType name="autoBrightness">
+        <xs:sequence>
+            <!-- Sets the debounce for autoBrightness brightening in millis-->
+            <xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger"
+                        minOccurs="0" maxOccurs="1">
+                <xs:annotation name="final"/>
+            </xs:element>
+            <!-- Sets the debounce for autoBrightness darkening in millis-->
+            <xs:element name="darkeningLightDebounceMillis" type="xs:nonNegativeInteger"
+                        minOccurs="0" maxOccurs="1">
+                <xs:annotation name="final"/>
+            </xs:element>
+        </xs:sequence>
+    </xs:complexType>
+</xs:schema>
\ No newline at end of file
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 073b131c..bea5e2c 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -23,6 +23,7 @@
 <xs:schema version="2.0"
            elementFormDefault="qualified"
            xmlns:xs="http://www.w3.org/2001/XMLSchema">
+    <xs:include schemaLocation="autobrightness.xsd" />
     <xs:element name="displayConfiguration">
         <xs:complexType>
             <xs:sequence>
@@ -102,22 +103,6 @@
     </xs:element>
 
     <!-- Type definitions -->
-
-    <xs:complexType name="autoBrightness">
-        <xs:sequence>
-            <!-- Sets the debounce for autoBrightness brightening in millis-->
-            <xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger"
-                        minOccurs="0" maxOccurs="1">
-                <xs:annotation name="final"/>
-            </xs:element>
-            <!-- Sets the debounce for autoBrightness darkening in millis-->
-            <xs:element name="darkeningLightDebounceMillis" type="xs:nonNegativeInteger"
-                        minOccurs="0" maxOccurs="1">
-                <xs:annotation name="final"/>
-            </xs:element>
-        </xs:sequence>
-    </xs:complexType>
-
     <xs:complexType name="displayQuirks">
         <xs:sequence>
             <xs:element name="quirk" type="xs:string" minOccurs="0" maxOccurs="unbounded" />
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index c1c7bce..0018a52 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -8363,6 +8363,7 @@
                 + PackageManager.FEATURE_DEVICE_ADMIN + " feature.");
     }
 
+    // TODO(b/240562946): Remove owner name from API parameters.
     @Override
     public boolean setDeviceOwner(ComponentName admin, String ownerName, int userId,
             boolean setProfileOwnerOnCurrentUserIfNecessary) {
@@ -8397,7 +8398,7 @@
                         .write();
             }
 
-            mOwners.setDeviceOwner(admin, ownerName, userId);
+            mOwners.setDeviceOwner(admin, userId);
             mOwners.writeDeviceOwner();
             setDeviceOwnershipSystemPropertyLocked();
 
@@ -8649,6 +8650,7 @@
         }
     }
 
+    // TODO(b/240562946): Remove api as owner name is not used.
     /**
      * Returns the "name" of the device owner.  It'll work for non-DO users too, but requires
      * MANAGE_USERS.
@@ -8819,6 +8821,7 @@
         });
     }
 
+    // TODO(b/240562946): Remove owner name from API parameters.
     @Override
     public boolean setProfileOwner(ComponentName who, String ownerName, int userHandle) {
         if (!mHasFeature) {
@@ -8866,7 +8869,7 @@
             // Shutting down backup manager service permanently.
             toggleBackupServiceActive(userHandle, /* makeActive= */ false);
 
-            mOwners.setProfileOwner(who, ownerName, userHandle);
+            mOwners.setProfileOwner(who, userHandle);
             mOwners.writeProfileOwner(userHandle);
             Slogf.i(LOG_TAG, "Profile owner set: " + who + " on user " + userHandle);
 
@@ -9337,6 +9340,7 @@
         return who.getPackageName().equals(configPackage);
     }
 
+    // TODO(b/240562946): Remove api as owner name is not used.
     @Override
     public String getProfileOwnerName(int userHandle) {
         if (!mHasFeature) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
index 08bd3e4..3b46d52 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
@@ -193,12 +193,6 @@
         }
     }
 
-    String getDeviceOwnerName() {
-        synchronized (mData) {
-            return mData.mDeviceOwner != null ? mData.mDeviceOwner.name : null;
-        }
-    }
-
     ComponentName getDeviceOwnerComponent() {
         synchronized (mData) {
             return mData.mDeviceOwner != null ? mData.mDeviceOwner.admin : null;
@@ -217,7 +211,7 @@
         }
     }
 
-    void setDeviceOwner(ComponentName admin, String ownerName, int userId) {
+    void setDeviceOwner(ComponentName admin, int userId) {
         if (userId < 0) {
             Slog.e(TAG, "Invalid user id for device owner user: " + userId);
             return;
@@ -226,7 +220,7 @@
             // A device owner is allowed to access device identifiers. Even though this flag
             // is not currently checked for device owner, it is set to true here so that it is
             // semantically compatible with the meaning of this flag.
-            mData.mDeviceOwner = new OwnerInfo(ownerName, admin, /* remoteBugreportUri =*/ null,
+            mData.mDeviceOwner = new OwnerInfo(admin, /* remoteBugreportUri =*/ null,
                     /* remoteBugreportHash =*/ null, /* isOrganizationOwnedDevice =*/ true);
             mData.mDeviceOwnerUserId = userId;
 
@@ -248,10 +242,10 @@
         }
     }
 
-    void setProfileOwner(ComponentName admin, String ownerName, int userId) {
+    void setProfileOwner(ComponentName admin, int userId) {
         synchronized (mData) {
             // For a newly set PO, there's no need for migration.
-            mData.mProfileOwners.put(userId, new OwnerInfo(ownerName, admin,
+            mData.mProfileOwners.put(userId, new OwnerInfo(admin,
                     /* remoteBugreportUri =*/ null, /* remoteBugreportHash =*/ null,
                     /* isOrganizationOwnedDevice =*/ false));
             mUserManagerInternal.setUserManaged(userId, true);
@@ -270,7 +264,7 @@
     void transferProfileOwner(ComponentName target, int userId) {
         synchronized (mData) {
             final OwnerInfo ownerInfo = mData.mProfileOwners.get(userId);
-            final OwnerInfo newOwnerInfo = new OwnerInfo(target.getPackageName(), target,
+            final OwnerInfo newOwnerInfo = new OwnerInfo(target,
                     ownerInfo.remoteBugreportUri, ownerInfo.remoteBugreportHash,
                     ownerInfo.isOrganizationOwnedDevice);
             mData.mProfileOwners.put(userId, newOwnerInfo);
@@ -282,9 +276,7 @@
         synchronized (mData) {
             Integer previousDeviceOwnerType = mData.mDeviceOwnerTypes.remove(
                     mData.mDeviceOwner.packageName);
-            // We don't set a name because it's not used anyway.
-            // See DevicePolicyManagerService#getDeviceOwnerName
-            mData.mDeviceOwner = new OwnerInfo(null, target,
+            mData.mDeviceOwner = new OwnerInfo(target,
                     mData.mDeviceOwner.remoteBugreportUri,
                     mData.mDeviceOwner.remoteBugreportHash,
                     mData.mDeviceOwner.isOrganizationOwnedDevice);
@@ -305,13 +297,6 @@
         }
     }
 
-    String getProfileOwnerName(int userId) {
-        synchronized (mData) {
-            OwnerInfo profileOwner = mData.mProfileOwners.get(userId);
-            return profileOwner != null ? profileOwner.name : null;
-        }
-    }
-
     String getProfileOwnerPackage(int userId) {
         synchronized (mData) {
             OwnerInfo profileOwner = mData.mProfileOwners.get(userId);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
index 6948420..2ab5464 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
@@ -493,16 +493,14 @@
     }
 
     static class OwnerInfo {
-        public final String name;
         public final String packageName;
         public final ComponentName admin;
         public String remoteBugreportUri;
         public String remoteBugreportHash;
         public boolean isOrganizationOwnedDevice;
 
-        OwnerInfo(String name, ComponentName admin, String remoteBugreportUri,
+        OwnerInfo(ComponentName admin, String remoteBugreportUri,
                 String remoteBugreportHash, boolean isOrganizationOwnedDevice) {
-            this.name = name;
             this.admin = admin;
             this.packageName = admin.getPackageName();
             this.remoteBugreportUri = remoteBugreportUri;
@@ -512,9 +510,6 @@
 
         public void writeToXml(TypedXmlSerializer out, String tag) throws IOException {
             out.startTag(null, tag);
-            if (name != null) {
-                out.attribute(null, ATTR_NAME, name);
-            }
             if (admin != null) {
                 out.attribute(null, ATTR_COMPONENT_NAME, admin.flattenToString());
             }
@@ -532,7 +527,6 @@
         }
 
         public static OwnerInfo readFromXml(TypedXmlPullParser parser) {
-            final String name = parser.getAttributeValue(null, ATTR_NAME);
             final String componentName = parser.getAttributeValue(null, ATTR_COMPONENT_NAME);
             final String remoteBugreportUri =
                     parser.getAttributeValue(null, ATTR_REMOTE_BUGREPORT_URI);
@@ -556,13 +550,11 @@
                 return null;
             }
 
-            return new OwnerInfo(
-                    name, admin, remoteBugreportUri, remoteBugreportHash, isOrgOwnedDevice);
+            return new OwnerInfo(admin, remoteBugreportUri, remoteBugreportHash, isOrgOwnedDevice);
         }
 
         public void dump(IndentingPrintWriter pw) {
             pw.println("admin=" + admin);
-            pw.println("name=" + name);
             pw.println("package=" + packageName);
             pw.println("isOrganizationOwnedDevice=" + isOrganizationOwnedDevice);
         }
diff --git a/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java
index 297538a..159285a 100644
--- a/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java
+++ b/services/robotests/backup/src/com/android/server/backup/UserBackupManagerServiceTest.java
@@ -61,8 +61,8 @@
 import com.android.server.backup.testing.TransportData;
 import com.android.server.backup.testing.TransportTestUtils.TransportMock;
 import com.android.server.backup.transport.TransportNotRegisteredException;
-import com.android.server.testing.shadows.ShadowBackupEligibilityRules;
 import com.android.server.testing.shadows.ShadowApplicationPackageManager;
+import com.android.server.testing.shadows.ShadowBackupEligibilityRules;
 import com.android.server.testing.shadows.ShadowBinder;
 import com.android.server.testing.shadows.ShadowKeyValueBackupJob;
 import com.android.server.testing.shadows.ShadowKeyValueBackupTask;
@@ -361,6 +361,26 @@
     }
 
     /**
+     * Test verifying that {@link UserBackupManagerService#selectBackupTransport(String)} does not
+     * switch the current transport to the inputted transport, when the inputted transport is not
+     * registered.
+     */
+    @Test
+    public void testSelectBackupTransport_nonRegisteredTransport() throws Exception {
+        setUpForSelectTransport();
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.isTransportRegistered(eq(mNewTransport.transportName)))
+                .thenReturn(false);
+        UserBackupManagerService backupManagerService = createUserBackupManagerServiceAndRunTasks();
+
+        String oldTransport = backupManagerService.selectBackupTransport(
+                mNewTransport.transportName);
+
+        assertThat(getSettingsTransport()).isNotEqualTo(mNewTransport.transportName);
+        assertThat(oldTransport).isEqualTo(null);
+    }
+
+    /**
      * Test verifying that {@link UserBackupManagerService#selectBackupTransport(String)} throws a
      * {@link SecurityException} if the caller does not have backup permission.
      */
diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
index b53a2c6..9022db8 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
@@ -17,8 +17,10 @@
 package com.android.server.app;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.server.app.GameManagerService.WRITE_SETTINGS;
 
 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;
@@ -35,11 +37,15 @@
 import static org.mockito.Mockito.when;
 
 import android.Manifest;
+import android.annotation.Nullable;
 import android.app.GameManager;
 import android.app.GameModeInfo;
 import android.app.GameState;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -97,6 +103,7 @@
     private PowerManagerInternal mMockPowerManager;
     @Mock
     private UserManager mMockUserManager;
+    private BroadcastReceiver mShutDownActionReceiver;
 
     // Stolen from ConnectivityServiceTest.MockContext
     class MockContext extends ContextWrapper {
@@ -165,6 +172,12 @@
             }
             throw new UnsupportedOperationException("Couldn't find system service: " + name);
         }
+
+        @Override
+        public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter) {
+            mShutDownActionReceiver =  receiver;
+            return null;
+        }
     }
 
     @Before
@@ -200,15 +213,16 @@
     @After
     public void tearDown() throws Exception {
         LocalServices.removeServiceForTest(PowerManagerInternal.class);
-        GameManagerService gameManagerService = new GameManagerService(mMockContext);
         if (mMockingSession != null) {
             mMockingSession.finishMocking();
         }
+        deleteFolder(InstrumentationRegistry.getTargetContext().getFilesDir());
     }
 
     private void startUser(GameManagerService gameManagerService, int userId) {
         UserInfo userInfo = new UserInfo(userId, "name", 0);
-        gameManagerService.onUserStarting(new SystemService.TargetUser(userInfo));
+        gameManagerService.onUserStarting(new SystemService.TargetUser(userInfo),
+                InstrumentationRegistry.getContext().getFilesDir());
         mTestLooper.dispatchAll();
     }
 
@@ -584,7 +598,7 @@
             gameManagerService.updateConfigsForUser(USER_ID_1, true, mPackageName);
         }
         GameManagerService.GamePackageConfiguration config =
-                gameManagerService.getConfig(mPackageName);
+                gameManagerService.getConfig(mPackageName, USER_ID_1);
         assertEquals(scaling, config.getGameModeConfiguration(gameMode).getScaling(), 0.01f);
     }
 
@@ -594,7 +608,7 @@
 
         // Validate GamePackageConfiguration returns the correct value.
         GameManagerService.GamePackageConfiguration config =
-                gameManagerService.getConfig(mPackageName);
+                gameManagerService.getConfig(mPackageName, USER_ID_1);
         assertEquals(config.getGameModeConfiguration(gameMode).getUseAngle(), angleEnabled);
 
         // Validate GameManagerService.isAngleEnabled() returns the correct value.
@@ -607,7 +621,7 @@
 
         // Validate GamePackageConfiguration returns the correct value.
         GameManagerService.GamePackageConfiguration config =
-                gameManagerService.getConfig(mPackageName);
+                gameManagerService.getConfig(mPackageName, USER_ID_1);
         assertEquals(
                 loadingBoost, config.getGameModeConfiguration(gameMode).getLoadingBoostDuration());
 
@@ -623,7 +637,7 @@
             gameManagerService.updateConfigsForUser(USER_ID_1, true, mPackageName);
         }
         GameManagerService.GamePackageConfiguration config =
-                gameManagerService.getConfig(mPackageName);
+                gameManagerService.getConfig(mPackageName, USER_ID_1);
         assertEquals(fps, config.getGameModeConfiguration(gameMode).getFps());
     }
 
@@ -1049,7 +1063,7 @@
                 mTestLooper.getLooper());
         startUser(gameManagerService, USER_ID_1);
         GameManagerService.GamePackageConfiguration config =
-                gameManagerService.getConfig(mPackageName);
+                gameManagerService.getConfig(mPackageName, USER_ID_1);
         assertEquals(90,
                 config.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE).getFps());
         assertEquals(30, config.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY).getFps());
@@ -1064,7 +1078,7 @@
                 mTestLooper.getLooper());
         startUser(gameManagerService, USER_ID_1);
         GameManagerService.GamePackageConfiguration config =
-                gameManagerService.getConfig(mPackageName);
+                gameManagerService.getConfig(mPackageName, USER_ID_1);
         assertEquals(0,
                 config.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE).getFps());
         assertEquals(0, config.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY).getFps());
@@ -1092,7 +1106,7 @@
         startUser(gameManagerService, USER_ID_1);
         gameManagerService.updateConfigsForUser(USER_ID_1, true, mPackageName);
         GameManagerService.GamePackageConfiguration config =
-                gameManagerService.getConfig(mPackageName);
+                gameManagerService.getConfig(mPackageName, USER_ID_1);
         assertNull(config.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE));
     }
 
@@ -1339,10 +1353,15 @@
         mTestLooper.dispatchAll();
 
         /* Expected fileOutput (order may vary)
+         # user 1001:
          com.android.app2 <UID>   0   2   angle=0,scaling=0.5,fps=90  3   angle=0,scaling=0.5,fps=60
          com.android.app1 <UID>   1   2   angle=0,scaling=0.5,fps=90  3   angle=0,scaling=0.7,fps=30
          com.android.app0 <UID>   0   2   angle=0,scaling=0.6,fps=120 3   angle=0,scaling=0.7,fps=30
 
+         # user 1002:
+         com.android.app2 <UID>   0   2   angle=0,scaling=0.5,fps=90  3   angle=0,scaling=0.7,fps=30
+         com.android.app1 <UID>   1   2   angle=0,scaling=0.5,fps=90  3   angle=0,scaling=0.7,fps=30
+         com.android.app0 <UID>   0   2   angle=0,scaling=0.5,fps=90  3   angle=0,scaling=0.7,fps=30
          The current game mode would only be set to non-zero if the current user have that game
          installed.
         */
@@ -1386,7 +1405,7 @@
         assertEquals(splitLine[3], "2");
         assertEquals(splitLine[4], "angle=0,scaling=0.5,fps=90");
         assertEquals(splitLine[5], "3");
-        assertEquals(splitLine[6], "angle=0,scaling=0.5,fps=60");
+        assertEquals(splitLine[6], "angle=0,scaling=0.7,fps=30");
         splitLine = fileOutput.get(1).split("\\s+");
         assertEquals(splitLine[0], "com.android.app1");
         assertEquals(splitLine[2], "3");
@@ -1398,7 +1417,7 @@
         assertEquals(splitLine[0], "com.android.app0");
         assertEquals(splitLine[2], "0");
         assertEquals(splitLine[3], "2");
-        assertEquals(splitLine[4], "angle=0,scaling=0.6,fps=120");
+        assertEquals(splitLine[4], "angle=0,scaling=0.5,fps=90");
         assertEquals(splitLine[5], "3");
         assertEquals(splitLine[6], "angle=0,scaling=0.7,fps=30");
 
@@ -1493,12 +1512,52 @@
 
     @Test
     public void testGetResolutionScalingFactor_noUserId() {
-        mockModifyGameModeDenied();
+        mockModifyGameModeGranted();
         mockDeviceConfigAll();
         GameManagerService gameManagerService =
                 new GameManagerService(mMockContext, mTestLooper.getLooper());
         startUser(gameManagerService, USER_ID_2);
-        assertEquals(-1f, gameManagerService.getResolutionScalingFactor(mPackageName,
-                GameManager.GAME_MODE_BATTERY, USER_ID_1), 0.001f);
+        assertThrows(IllegalArgumentException.class, () -> {
+            gameManagerService.getResolutionScalingFactor(mPackageName,
+                    GameManager.GAME_MODE_BATTERY, USER_ID_1);
+        });
+    }
+
+    @Test
+    public void testWritingSettingFile_onShutdown() throws InterruptedException {
+        mockModifyGameModeGranted();
+        mockDeviceConfigAll();
+        GameManagerService gameManagerService = new GameManagerService(mMockContext);
+        gameManagerService.onBootCompleted();
+        startUser(gameManagerService, USER_ID_1);
+        Thread.sleep(500);
+        gameManagerService.setGameModeConfigOverride("com.android.app1", USER_ID_1,
+                GameManager.GAME_MODE_BATTERY, "60", "0.5");
+        gameManagerService.setGameMode("com.android.app1", USER_ID_1,
+                GameManager.GAME_MODE_PERFORMANCE);
+        GameManagerSettings settings = new GameManagerSettings(
+                InstrumentationRegistry.getContext().getFilesDir());
+        Thread.sleep(500);
+        // no data written as delayed messages are queued
+        assertFalse(settings.readPersistentDataLocked());
+        assertTrue(gameManagerService.mHandler.hasEqualMessages(WRITE_SETTINGS, USER_ID_1));
+        Intent shutdown = new Intent();
+        shutdown.setAction(Intent.ACTION_SHUTDOWN);
+        mShutDownActionReceiver.onReceive(mMockContext, shutdown);
+        Thread.sleep(500);
+        // data is written on processing new message with no delay on shutdown,
+        // and all queued messages should be removed
+        assertTrue(settings.readPersistentDataLocked());
+        assertFalse(gameManagerService.mHandler.hasEqualMessages(WRITE_SETTINGS, USER_ID_1));
+    }
+
+    private static void deleteFolder(File folder) {
+        File[] files = folder.listFiles();
+        if (files != null) {
+            for (File file : files) {
+                deleteFolder(file);
+            }
+        }
+        folder.delete();
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
index 427c3b3..4a631a12 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
@@ -34,7 +34,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManagerInternal;
-import android.content.pm.ServiceInfo;
 import android.os.BatteryManagerInternal;
 import android.os.RemoteException;
 import android.util.ArraySet;
@@ -190,7 +189,7 @@
             JobInfo jobInfo) {
         JobStatus js = JobStatus.createFromJobInfo(
                 jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag);
-        js.serviceInfo = mock(ServiceInfo.class);
+        js.serviceProcessName = "testProcess";
         // Make sure tests aren't passing just because the default bucket is likely ACTIVE.
         js.setStandbyBucket(FREQUENT_INDEX);
         return js;
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 1b39add..9d039bd 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -209,7 +209,8 @@
     public void testOnConstantsUpdated_DeadlineProximity() {
         JobStatus js = createJobStatus("testDeadlineProximityConfig", createJob(0));
         setDeviceConfigLong(KEY_DEADLINE_PROXIMITY_LIMIT, Long.MAX_VALUE);
-        mFlexibilityController.mFlexibilityAlarmQueue.scheduleDropNumConstraintsAlarm(js);
+        mFlexibilityController.mFlexibilityAlarmQueue
+                .scheduleDropNumConstraintsAlarm(js, FROZEN_TIME);
         assertEquals(0, js.getNumRequiredFlexibleConstraints());
     }
 
@@ -336,26 +337,31 @@
     @Test
     public void testCurPercent() {
         long deadline = 1000;
+        long nowElapsed;
         JobInfo.Builder jb = createJob(0).setOverrideDeadline(deadline);
         JobStatus js = createJobStatus("time", jb);
 
         assertEquals(FROZEN_TIME, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
         assertEquals(deadline + FROZEN_TIME,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(js, FROZEN_TIME));
+        nowElapsed = 600 + FROZEN_TIME;
         JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(600 + FROZEN_TIME), ZoneOffset.UTC);
-        assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js));
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
+        assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
+        nowElapsed = 1400;
         JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(1400), ZoneOffset.UTC);
-        assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js));
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
+        assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
+        nowElapsed = 950 + FROZEN_TIME;
         JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(950 + FROZEN_TIME), ZoneOffset.UTC);
-        assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js));
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
+        assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
+        nowElapsed = FROZEN_TIME;
         JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC);
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
         long delay = 100;
         deadline = 1100;
         jb = createJob(0).setOverrideDeadline(deadline).setMinimumLatency(delay);
@@ -366,18 +372,21 @@
         assertEquals(deadline + FROZEN_TIME,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(js, FROZEN_TIME + delay));
 
+        nowElapsed = 600 + FROZEN_TIME + delay;
         JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(600 + FROZEN_TIME + delay), ZoneOffset.UTC);
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
-        assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js));
+        assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
+        nowElapsed = 1400;
         JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(1400), ZoneOffset.UTC);
-        assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js));
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
+        assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
 
+        nowElapsed = 950 + FROZEN_TIME + delay;
         JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(950 + FROZEN_TIME + delay), ZoneOffset.UTC);
-        assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js));
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
+        assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
     }
 
     @Test
@@ -491,19 +500,19 @@
             assertEquals(3, trackedJobs.get(2).size());
             assertEquals(0, trackedJobs.get(3).size());
 
-            flexTracker.adjustJobsRequiredConstraints(jobs[0], -1);
+            flexTracker.adjustJobsRequiredConstraints(jobs[0], -1, FROZEN_TIME);
             assertEquals(0, trackedJobs.get(0).size());
             assertEquals(1, trackedJobs.get(1).size());
             assertEquals(2, trackedJobs.get(2).size());
             assertEquals(0, trackedJobs.get(3).size());
 
-            flexTracker.adjustJobsRequiredConstraints(jobs[0], -1);
+            flexTracker.adjustJobsRequiredConstraints(jobs[0], -1, FROZEN_TIME);
             assertEquals(1, trackedJobs.get(0).size());
             assertEquals(0, trackedJobs.get(1).size());
             assertEquals(2, trackedJobs.get(2).size());
             assertEquals(0, trackedJobs.get(3).size());
 
-            flexTracker.adjustJobsRequiredConstraints(jobs[0], -1);
+            flexTracker.adjustJobsRequiredConstraints(jobs[0], -1, FROZEN_TIME);
             assertEquals(0, trackedJobs.get(0).size());
             assertEquals(0, trackedJobs.get(1).size());
             assertEquals(2, trackedJobs.get(2).size());
@@ -515,24 +524,25 @@
             assertEquals(1, trackedJobs.get(2).size());
             assertEquals(0, trackedJobs.get(3).size());
 
-            flexTracker.resetJobNumDroppedConstraints(jobs[0]);
+            flexTracker.resetJobNumDroppedConstraints(jobs[0], FROZEN_TIME);
             assertEquals(0, trackedJobs.get(0).size());
             assertEquals(0, trackedJobs.get(1).size());
             assertEquals(2, trackedJobs.get(2).size());
             assertEquals(0, trackedJobs.get(3).size());
 
-            flexTracker.adjustJobsRequiredConstraints(jobs[0], -2);
+            flexTracker.adjustJobsRequiredConstraints(jobs[0], -2, FROZEN_TIME);
 
             assertEquals(1, trackedJobs.get(0).size());
             assertEquals(0, trackedJobs.get(1).size());
             assertEquals(1, trackedJobs.get(2).size());
             assertEquals(0, trackedJobs.get(3).size());
 
+            final long nowElapsed = ((DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 2)
+                    + HOUR_IN_MILLIS);
             JobSchedulerService.sElapsedRealtimeClock =
-                    Clock.fixed(Instant.ofEpochMilli((DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 2)
-                            + HOUR_IN_MILLIS), ZoneOffset.UTC);
+                    Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
-            flexTracker.resetJobNumDroppedConstraints(jobs[0]);
+            flexTracker.resetJobNumDroppedConstraints(jobs[0], nowElapsed);
             assertEquals(0, trackedJobs.get(0).size());
             assertEquals(1, trackedJobs.get(1).size());
             assertEquals(1, trackedJobs.get(2).size());
@@ -615,13 +625,13 @@
 
     @Test
     public void testSetConstraintSatisfied_Constraints() {
-        mFlexibilityController.setConstraintSatisfied(CONSTRAINT_IDLE, false);
+        mFlexibilityController.setConstraintSatisfied(CONSTRAINT_IDLE, false, FROZEN_TIME);
         assertFalse(mFlexibilityController.isConstraintSatisfied(CONSTRAINT_IDLE));
 
-        mFlexibilityController.setConstraintSatisfied(CONSTRAINT_IDLE, true);
+        mFlexibilityController.setConstraintSatisfied(CONSTRAINT_IDLE, true, FROZEN_TIME);
         assertTrue(mFlexibilityController.isConstraintSatisfied(CONSTRAINT_IDLE));
 
-        mFlexibilityController.setConstraintSatisfied(CONSTRAINT_IDLE, false);
+        mFlexibilityController.setConstraintSatisfied(CONSTRAINT_IDLE, false, FROZEN_TIME);
         assertFalse(mFlexibilityController.isConstraintSatisfied(CONSTRAINT_IDLE));
     }
 
@@ -651,20 +661,21 @@
                         createJobStatus(String.valueOf(i), jb), null);
             }
         }
-        mFlexibilityController.setConstraintSatisfied(CONSTRAINT_CHARGING, false);
-        mFlexibilityController.setConstraintSatisfied(CONSTRAINT_IDLE, false);
-        mFlexibilityController.setConstraintSatisfied(CONSTRAINT_BATTERY_NOT_LOW, false);
+        mFlexibilityController.setConstraintSatisfied(CONSTRAINT_CHARGING, false, FROZEN_TIME);
+        mFlexibilityController.setConstraintSatisfied(CONSTRAINT_IDLE, false, FROZEN_TIME);
+        mFlexibilityController.setConstraintSatisfied(
+                CONSTRAINT_BATTERY_NOT_LOW, false, FROZEN_TIME);
 
         assertEquals(0, mFlexibilityController.mSatisfiedFlexibleConstraints);
 
         for (int i = 0; i < constraintCombinations.length; i++) {
             constraints = constraintCombinations[i];
             mFlexibilityController.setConstraintSatisfied(CONSTRAINT_CHARGING,
-                    (constraints & CONSTRAINT_CHARGING) != 0);
+                    (constraints & CONSTRAINT_CHARGING) != 0, FROZEN_TIME);
             mFlexibilityController.setConstraintSatisfied(CONSTRAINT_IDLE,
-                    (constraints & CONSTRAINT_IDLE) != 0);
+                    (constraints & CONSTRAINT_IDLE) != 0, FROZEN_TIME);
             mFlexibilityController.setConstraintSatisfied(CONSTRAINT_BATTERY_NOT_LOW,
-                    (constraints & CONSTRAINT_BATTERY_NOT_LOW) != 0);
+                    (constraints & CONSTRAINT_BATTERY_NOT_LOW) != 0, FROZEN_TIME);
 
             assertEquals(constraints, mFlexibilityController.mSatisfiedFlexibleConstraints);
             synchronized (mFlexibilityController.mLock) {
@@ -679,6 +690,7 @@
         JobInfo.Builder jb = createJob(22).setOverrideDeadline(100L);
         JobStatus js = createJobStatus("testResetJobNumDroppedConstraints", jb);
         js.adjustNumRequiredFlexibleConstraints(3);
+        long nowElapsed;
 
         mFlexibilityController.mFlexibilityTracker.add(js);
 
@@ -687,45 +699,51 @@
         assertEquals(1, mFlexibilityController
                 .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size());
 
-        JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(155L), ZoneOffset.UTC);
 
-        mFlexibilityController.mFlexibilityTracker.adjustJobsRequiredConstraints(js, -1);
+        nowElapsed = 155L;
+        JobSchedulerService.sElapsedRealtimeClock =
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
+
+        mFlexibilityController.mFlexibilityTracker
+                .adjustJobsRequiredConstraints(js, -1, nowElapsed);
 
         assertEquals(2, js.getNumRequiredFlexibleConstraints());
         assertEquals(1, js.getNumDroppedFlexibleConstraints());
         assertEquals(1, mFlexibilityController
                 .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size());
 
-        mFlexibilityController.mFlexibilityTracker.resetJobNumDroppedConstraints(js);
+        mFlexibilityController.mFlexibilityTracker.resetJobNumDroppedConstraints(js, nowElapsed);
 
         assertEquals(2, js.getNumRequiredFlexibleConstraints());
         assertEquals(1, js.getNumDroppedFlexibleConstraints());
         assertEquals(1, mFlexibilityController
                 .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size());
 
+        nowElapsed = 140L;
         JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(140L), ZoneOffset.UTC);
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
-        mFlexibilityController.mFlexibilityTracker.resetJobNumDroppedConstraints(js);
+        mFlexibilityController.mFlexibilityTracker.resetJobNumDroppedConstraints(js, nowElapsed);
 
         assertEquals(3, js.getNumRequiredFlexibleConstraints());
         assertEquals(0, js.getNumDroppedFlexibleConstraints());
         assertEquals(1, mFlexibilityController
                 .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size());
 
+        nowElapsed = 175L;
         JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(175), ZoneOffset.UTC);
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
-        mFlexibilityController.mFlexibilityTracker.resetJobNumDroppedConstraints(js);
+        mFlexibilityController.mFlexibilityTracker.resetJobNumDroppedConstraints(js, nowElapsed);
 
         assertEquals(0, js.getNumRequiredFlexibleConstraints());
         assertEquals(3, js.getNumDroppedFlexibleConstraints());
 
+        nowElapsed = 165L;
         JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(165L), ZoneOffset.UTC);
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
-        mFlexibilityController.mFlexibilityTracker.resetJobNumDroppedConstraints(js);
+        mFlexibilityController.mFlexibilityTracker.resetJobNumDroppedConstraints(js, nowElapsed);
 
         assertEquals(1, js.getNumRequiredFlexibleConstraints());
         assertEquals(2, js.getNumDroppedFlexibleConstraints());
@@ -745,12 +763,14 @@
 
         mFlexibilityController.maybeStartTrackingJobLocked(js, null);
 
+        final long nowElapsed = 150L;
         JobSchedulerService.sElapsedRealtimeClock =
-                Clock.fixed(Instant.ofEpochMilli(150L), ZoneOffset.UTC);
+                Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
 
         mFlexibilityController.mPrefetchChangedListener.onPrefetchCacheUpdated(
                 jobs, js.getUserId(), js.getSourcePackageName(), Long.MAX_VALUE,
-                1150L + mFlexibilityController.mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS);
+                1150L + mFlexibilityController.mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS,
+                nowElapsed);
 
         assertEquals(150L,
                 (long) mFlexibilityController.mPrefetchLifeCycleStart
@@ -758,7 +778,7 @@
         assertEquals(150L, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
         assertEquals(1150L,
                 mFlexibilityController.getLifeCycleEndElapsedLocked(js, 150L));
-        assertEquals(0, mFlexibilityController.getCurPercentOfLifecycleLocked(js));
+        assertEquals(0, mFlexibilityController.getCurPercentOfLifecycleLocked(js, FROZEN_TIME));
         assertEquals(650L, mFlexibilityController
                 .getNextConstraintDropTimeElapsedLocked(js));
         assertEquals(3, js.getNumRequiredFlexibleConstraints());
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
index df523fe..149ae0b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
@@ -51,7 +51,6 @@
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.ComponentName;
 import android.content.pm.PackageManagerInternal;
-import android.content.pm.ServiceInfo;
 import android.net.Uri;
 import android.os.SystemClock;
 import android.provider.MediaStore;
@@ -887,7 +886,7 @@
 
     private static JobStatus createJobStatus(JobInfo job) {
         JobStatus jobStatus = JobStatus.createFromJobInfo(job, 0, null, -1, "JobStatusTest");
-        jobStatus.serviceInfo = mock(ServiceInfo.class);
+        jobStatus.serviceProcessName = "testProcess";
         return jobStatus;
     }
 }
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 7242b1b..bb477b1 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
@@ -47,7 +47,6 @@
 import android.appwidget.AppWidgetManager;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.ServiceInfo;
 import android.os.Looper;
 import android.os.Process;
 import android.os.SystemClock;
@@ -185,7 +184,7 @@
             JobInfo jobInfo) {
         JobStatus js = JobStatus.createFromJobInfo(
                 jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag);
-        js.serviceInfo = mock(ServiceInfo.class);
+        js.serviceProcessName = "testProcess";
         js.setStandbyBucket(FREQUENT_INDEX);
         // Make sure Doze and background-not-restricted don't affect tests.
         js.setDeviceNotDozingConstraintSatisfied(/* nowElapsed */ sElapsedRealtimeClock.millis(),
@@ -491,9 +490,9 @@
         final PrefetchController.PrefetchChangedListener prefetchChangedListener =
                 new PrefetchController.PrefetchChangedListener() {
                     @Override
-                    public void onPrefetchCacheUpdated(
-                            ArraySet<JobStatus> jobs, int userId, String pkgName,
-                            long prevEstimatedLaunchTime, long newEstimatedLaunchTime) {
+                    public void onPrefetchCacheUpdated(ArraySet<JobStatus> jobs,
+                            int userId, String pkgName, long prevEstimatedLaunchTime,
+                            long newEstimatedLaunchTime, long nowElapsed) {
                         onPrefetchCacheChangedCalled[0] = true;
                     }
                 };
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 7048fb2..9407968 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
@@ -70,7 +70,6 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
-import android.content.pm.ServiceInfo;
 import android.os.BatteryManagerInternal;
 import android.os.Handler;
 import android.os.Looper;
@@ -385,7 +384,7 @@
             JobInfo jobInfo) {
         JobStatus js = JobStatus.createFromJobInfo(
                 jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag);
-        js.serviceInfo = mock(ServiceInfo.class);
+        js.serviceProcessName = "testProcess";
         // Make sure tests aren't passing just because the default bucket is likely ACTIVE.
         js.setStandbyBucket(FREQUENT_INDEX);
         // Make sure Doze and background-not-restricted don't affect tests.
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/listeners/ListenerMultiplexerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/listeners/ListenerMultiplexerTest.java
index d7fef60..5b92706 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/listeners/ListenerMultiplexerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/listeners/ListenerMultiplexerTest.java
@@ -58,8 +58,10 @@
         void onRegistrationAdded(Consumer<TestListenerRegistration> consumer,
                 TestListenerRegistration registration);
 
-        void onRegistrationReplaced(Consumer<TestListenerRegistration> consumer,
-                TestListenerRegistration oldRegistration, TestListenerRegistration newRegistration);
+        void onRegistrationReplaced(Consumer<TestListenerRegistration> oldConsumer,
+                TestListenerRegistration oldRegistration,
+                Consumer<TestListenerRegistration> newConsumer,
+                TestListenerRegistration newRegistration);
 
         void onRegistrationRemoved(Consumer<TestListenerRegistration> consumer,
                 TestListenerRegistration registration);
@@ -93,10 +95,10 @@
         assertThat(mMultiplexer.mMergedRequest).isEqualTo(0);
 
         mMultiplexer.addListener(1, consumer);
-        mInOrder.verify(mCallbacks).onRegistrationRemoved(eq(consumer),
-                any(TestListenerRegistration.class));
         mInOrder.verify(mCallbacks).onRegistrationReplaced(eq(consumer),
-                any(TestListenerRegistration.class), any(TestListenerRegistration.class));
+                any(TestListenerRegistration.class),
+                eq(consumer),
+                any(TestListenerRegistration.class));
         assertThat(mMultiplexer.mRegistered).isTrue();
         assertThat(mMultiplexer.mMergedRequest).isEqualTo(1);
 
@@ -115,10 +117,10 @@
                 any(TestListenerRegistration.class));
         mInOrder.verify(mCallbacks).onActive();
         mMultiplexer.replaceListener(1, oldConsumer, consumer);
-        mInOrder.verify(mCallbacks).onRegistrationRemoved(eq(oldConsumer),
+        mInOrder.verify(mCallbacks).onRegistrationReplaced(eq(oldConsumer),
+                any(TestListenerRegistration.class),
+                eq(consumer),
                 any(TestListenerRegistration.class));
-        mInOrder.verify(mCallbacks).onRegistrationReplaced(eq(consumer),
-                any(TestListenerRegistration.class), any(TestListenerRegistration.class));
         assertThat(mMultiplexer.mRegistered).isTrue();
         assertThat(mMultiplexer.mMergedRequest).isEqualTo(1);
 
@@ -352,13 +354,19 @@
     }
 
     private static class TestListenerRegistration extends
-            RequestListenerRegistration<Integer, Consumer<TestListenerRegistration>> {
+            ListenerRegistration<Consumer<TestListenerRegistration>> {
 
+        private final Integer mInteger;
         boolean mActive = true;
 
         protected TestListenerRegistration(Integer integer,
                 Consumer<TestListenerRegistration> consumer) {
-            super(DIRECT_EXECUTOR, integer, consumer);
+            super(DIRECT_EXECUTOR, consumer);
+            mInteger = integer;
+        }
+
+        public Integer getInteger() {
+            return mInteger;
         }
     }
 
@@ -375,11 +383,6 @@
             mCallbacks = callbacks;
         }
 
-        @Override
-        public String getTag() {
-            return "TestMultiplexer";
-        }
-
         public void addListener(Integer request, Consumer<TestListenerRegistration> consumer) {
             putRegistration(consumer, new TestListenerRegistration(request, consumer));
         }
@@ -399,9 +402,9 @@
             removeRegistration(consumer, registration);
         }
 
-        public void setActive(Integer request, boolean active) {
+        public void setActive(Integer integer, boolean active) {
             updateRegistrations(testRegistration -> {
-                if (testRegistration.getRequest().equals(request)) {
+                if (testRegistration.getInteger().equals(integer)) {
                     testRegistration.mActive = active;
                     return true;
                 }
@@ -458,10 +461,11 @@
         }
 
         @Override
-        protected void onRegistrationReplaced(Consumer<TestListenerRegistration> consumer,
+        protected void onRegistrationReplaced(Consumer<TestListenerRegistration> oldKey,
                 TestListenerRegistration oldRegistration,
+                Consumer<TestListenerRegistration> newKey,
                 TestListenerRegistration newRegistration) {
-            mCallbacks.onRegistrationReplaced(consumer, oldRegistration, newRegistration);
+            mCallbacks.onRegistrationReplaced(oldKey, oldRegistration, newKey, newRegistration);
         }
 
         @Override
@@ -475,8 +479,8 @@
                 Collection<TestListenerRegistration> testRegistrations) {
             int max = Integer.MIN_VALUE;
             for (TestListenerRegistration registration : testRegistrations) {
-                if (registration.getRequest() > max) {
-                    max = registration.getRequest();
+                if (registration.getInteger() > max) {
+                    max = registration.getInteger();
                 }
             }
             mMergeCount++;
@@ -493,7 +497,7 @@
         @Override
         protected void onRegistrationAdded(Consumer<TestListenerRegistration> consumer,
                 TestListenerRegistration registration) {
-            addListener(registration.getRequest(), consumer);
+            addListener(registration.getInteger(), consumer);
         }
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
index 71cc65b..0ac1443 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
@@ -32,7 +32,6 @@
 import static com.android.server.location.LocationPermissions.PERMISSION_FINE;
 import static com.android.server.location.LocationUtils.createLocation;
 import static com.android.server.location.LocationUtils.createLocationResult;
-import static com.android.server.location.listeners.RemoteListenerRegistration.IN_PROCESS_EXECUTOR;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -534,7 +533,7 @@
                 listener);
 
         CountDownLatch blocker = new CountDownLatch(1);
-        IN_PROCESS_EXECUTOR.execute(() -> {
+        FgThread.getExecutor().execute(() -> {
             try {
                 blocker.await();
             } catch (InterruptedException e) {
@@ -661,7 +660,7 @@
                 listener);
 
         CountDownLatch blocker = new CountDownLatch(1);
-        IN_PROCESS_EXECUTOR.execute(() -> {
+        FgThread.getExecutor().execute(() -> {
             try {
                 blocker.await();
             } catch (InterruptedException e) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BroadcastHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/BroadcastHelperTest.kt
deleted file mode 100644
index d25649e..0000000
--- a/services/tests/mockingservicestests/src/com/android/server/pm/BroadcastHelperTest.kt
+++ /dev/null
@@ -1,117 +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 com.android.server.testutils.whenever
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-
-@RunWith(JUnit4::class)
-class BroadcastHelperTest {
-
-    companion object {
-        const val TEST_PACKAGE_1 = "com.android.test.package1"
-        const val TEST_PACKAGE_2 = "com.android.test.package2"
-        const val TEST_UID_1 = 10100
-        const val TEST_UID_2 = 10101
-        const val TEST_USER_ID = 0
-    }
-
-    lateinit var broadcastHelper: BroadcastHelper
-    lateinit var packagesToChange: Array<String>
-    lateinit var uidsToChange: IntArray
-
-    @Mock
-    lateinit var snapshot: Computer
-
-    @Rule
-    @JvmField
-    val rule = MockSystemRule()
-
-    @Before
-    open fun setup() {
-        MockitoAnnotations.initMocks(this)
-        rule.system().stageNominalSystemState()
-        broadcastHelper = BroadcastHelper(rule.mocks().injector)
-        packagesToChange = arrayOf(TEST_PACKAGE_1, TEST_PACKAGE_2)
-        uidsToChange = intArrayOf(TEST_UID_1, TEST_UID_2)
-    }
-
-    @Test
-    fun getBroadcastParams_withSameVisibilityAllowList_shouldGroup() {
-        val allowList = intArrayOf(10001, 10002, 10003)
-        mockVisibilityAllowList(TEST_PACKAGE_1, allowList)
-        mockVisibilityAllowList(TEST_PACKAGE_2, allowList)
-
-        val broadcastParams: List<BroadcastParams> = broadcastHelper.getBroadcastParams(
-                snapshot, packagesToChange, uidsToChange, TEST_USER_ID)
-
-        assertThat(broadcastParams).hasSize(1)
-        assertThat(broadcastParams[0].packageNames).asList().containsExactlyElementsIn(
-                packagesToChange.toCollection(ArrayList()))
-        assertThat(broadcastParams[0].uids).asList().containsExactlyElementsIn(
-                uidsToChange.toCollection(ArrayList()))
-    }
-
-    @Test
-    fun getBroadcastParams_withDifferentVisibilityAllowList_shouldNotGroup() {
-        val allowList1 = intArrayOf(10001, 10002, 10003)
-        val allowList2 = intArrayOf(10001, 10002, 10007)
-        mockVisibilityAllowList(TEST_PACKAGE_1, allowList1)
-        mockVisibilityAllowList(TEST_PACKAGE_2, allowList2)
-
-        val broadcastParams: List<BroadcastParams> = broadcastHelper.getBroadcastParams(
-                snapshot, packagesToChange, uidsToChange, TEST_USER_ID)
-
-        assertThat(broadcastParams).hasSize(2)
-        broadcastParams.forEachIndexed { i, params ->
-            val changedPackages = params.packageNames
-            val changedUids = params.uids
-            assertThat(changedPackages[0]).isEqualTo(packagesToChange[i])
-            assertThat(changedUids[0]).isEqualTo(uidsToChange[i])
-        }
-    }
-
-    @Test
-    fun getBroadcastParams_withNullVisibilityAllowList_shouldNotGroup() {
-        val allowList = intArrayOf(10001, 10002, 10003)
-        mockVisibilityAllowList(TEST_PACKAGE_1, allowList)
-        mockVisibilityAllowList(TEST_PACKAGE_2, null)
-
-        val broadcastParams: List<BroadcastParams> = broadcastHelper.getBroadcastParams(
-                snapshot, packagesToChange, uidsToChange, TEST_USER_ID)
-
-        assertThat(broadcastParams).hasSize(2)
-        broadcastParams.forEachIndexed { i, params ->
-            val changedPackages = params.packageNames
-            val changedUids = params.uids
-            assertThat(changedPackages[0]).isEqualTo(packagesToChange[i])
-            assertThat(changedUids[0]).isEqualTo(uidsToChange[i])
-        }
-    }
-
-    private fun mockVisibilityAllowList(pkgName: String, list: IntArray?) {
-        whenever(snapshot.getVisibilityAllowList(pkgName, TEST_USER_ID))
-                .thenReturn(list ?: IntArray(0))
-    }
-}
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt
index d8770e5..9f1cec3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt
@@ -29,7 +29,6 @@
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.never
-import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 
 @RunWith(JUnit4::class)
@@ -53,7 +52,7 @@
         verify(pms).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
         verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED),
                 nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-                nullable(), nullable(), nullable())
+                nullable(), nullable(), nullable(), nullable())
 
         val modifiedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
         val distractionFlags = bundleCaptor.value.getInt(Intent.EXTRA_DISTRACTION_RESTRICTIONS)
@@ -78,7 +77,8 @@
         verify(pms, never()).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
         verify(broadcastHelper, never()).sendPackageBroadcast(
                 eq(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED), nullable(), bundleCaptor.capture(),
-                anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable())
+                anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable(),
+                nullable())
         assertThat(unactionedPackages).isEmpty()
     }
 
@@ -156,7 +156,7 @@
         verify(pms).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
         verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED),
                 nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-                nullable(), nullable(), nullable())
+                nullable(), nullable(), nullable(), nullable())
         val modifiedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
         val distractionFlags = bundleCaptor.value.getInt(Intent.EXTRA_DISTRACTION_RESTRICTIONS)
         assertThat(modifiedPackages).asList().containsExactly(TEST_PACKAGE_1, TEST_PACKAGE_2)
@@ -172,7 +172,7 @@
         verify(pms, never()).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
         verify(broadcastHelper, never()).sendPackageBroadcast(eq(
                 Intent.ACTION_DISTRACTING_PACKAGES_CHANGED), nullable(), nullable(), anyInt(),
-                nullable(), nullable(), any(), nullable(), nullable(), nullable())
+                nullable(), nullable(), any(), nullable(), nullable(), nullable(), nullable())
     }
 
     @Test
@@ -183,7 +183,7 @@
         verify(pms, never()).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
         verify(broadcastHelper, never()).sendPackageBroadcast(eq(
                 Intent.ACTION_DISTRACTING_PACKAGES_CHANGED), nullable(), nullable(), anyInt(),
-                nullable(), nullable(), any(), nullable(), nullable(), nullable())
+                nullable(), nullable(), any(), nullable(), nullable(), nullable(), nullable())
 
         distractingPackageHelper.removeDistractingPackageRestrictions(pms.snapshotComputer(),
                 arrayOfNulls(0), TEST_USER_ID)
@@ -191,18 +191,17 @@
         verify(pms, never()).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
         verify(broadcastHelper, never()).sendPackageBroadcast(eq(
                 Intent.ACTION_DISTRACTING_PACKAGES_CHANGED), nullable(), nullable(), anyInt(),
-                nullable(), nullable(), any(), nullable(), nullable(), nullable())
+                nullable(), nullable(), any(), nullable(), nullable(), nullable(), nullable())
     }
 
     @Test
-    fun sendDistractingPackagesChanged_withSameVisibilityAllowList() {
-        distractingPackageHelper.sendDistractingPackagesChanged(pms.snapshotComputer(),
-                packagesToChange, uidsToChange, TEST_USER_ID,
-                PackageManager.RESTRICTION_HIDE_NOTIFICATIONS)
+    fun sendDistractingPackagesChanged() {
+        distractingPackageHelper.sendDistractingPackagesChanged(packagesToChange, uidsToChange,
+                TEST_USER_ID, PackageManager.RESTRICTION_HIDE_NOTIFICATIONS)
         testHandler.flush()
         verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED),
                 nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-                nullable(), nullable(), nullable())
+                nullable(), nullable(), nullable(), nullable())
 
         var changedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
         var changedUids = bundleCaptor.value.getIntArray(Intent.EXTRA_CHANGED_UID_LIST)
@@ -210,49 +209,4 @@
         assertThat(changedUids).asList().containsExactly(
                 packageSetting1.appId, packageSetting2.appId)
     }
-
-    @Test
-    fun sendDistractingPackagesChanged_withDifferentVisibilityAllowList() {
-        mockDividedSeparatedBroadcastList(
-                intArrayOf(10001, 10002, 10003), intArrayOf(10001, 10002, 10007))
-
-        distractingPackageHelper.sendDistractingPackagesChanged(pms.snapshotComputer(),
-                packagesToChange, uidsToChange, TEST_USER_ID,
-                PackageManager.RESTRICTION_HIDE_NOTIFICATIONS)
-        testHandler.flush()
-        verify(broadcastHelper, times(2)).sendPackageBroadcast(
-                eq(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED), nullable(), bundleCaptor.capture(),
-                anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable())
-
-        bundleCaptor.allValues.forEachIndexed { i, it ->
-            var changedPackages = it.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-            var changedUids = it.getIntArray(Intent.EXTRA_CHANGED_UID_LIST)
-            assertThat(changedPackages?.size).isEqualTo(1)
-            assertThat(changedUids?.size).isEqualTo(1)
-            assertThat(changedPackages?.get(0)).isEqualTo(packagesToChange[i])
-            assertThat(changedUids?.get(0)).isEqualTo(uidsToChange[i])
-        }
-    }
-
-    @Test
-    fun sendDistractingPackagesChanged_withNullVisibilityAllowList() {
-        mockDividedSeparatedBroadcastList(intArrayOf(10001, 10002, 10003), null)
-
-        distractingPackageHelper.sendDistractingPackagesChanged(pms.snapshotComputer(),
-                packagesToChange, uidsToChange, TEST_USER_ID,
-                PackageManager.RESTRICTION_HIDE_NOTIFICATIONS)
-        testHandler.flush()
-        verify(broadcastHelper, times(2)).sendPackageBroadcast(
-                eq(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED), nullable(), bundleCaptor.capture(),
-                anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable())
-
-        bundleCaptor.allValues.forEachIndexed { i, it ->
-            var changedPackages = it.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-            var changedUids = it.getIntArray(Intent.EXTRA_CHANGED_UID_LIST)
-            assertThat(changedPackages?.size).isEqualTo(1)
-            assertThat(changedUids?.size).isEqualTo(1)
-            assertThat(changedPackages?.get(0)).isEqualTo(packagesToChange[i])
-            assertThat(changedUids?.get(0)).isEqualTo(uidsToChange[i])
-        }
-    }
-}
\ No newline at end of file
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt b/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt
index 5f9ef58..4ac5e86 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageHelperTestBase.kt
@@ -28,7 +28,6 @@
 import org.junit.Before
 import org.junit.Rule
 import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito
@@ -102,7 +101,6 @@
         whenever(rule.mocks().userManagerService.hasUserRestriction(
                 eq(UserManager.DISALLOW_UNINSTALL_APPS), eq(TEST_USER_ID))).thenReturn(true)
         mockKnownPackages(pms)
-        mockUnifiedSeparatedBroadcastList()
     }
 
     private fun mockKnownPackages(pms: PackageManagerService) {
@@ -139,26 +137,4 @@
         rule.system().validateFinalState()
         return pms
     }
-
-    protected fun mockUnifiedSeparatedBroadcastList() {
-        whenever(broadcastHelper.getBroadcastParams(any(Computer::class.java),
-                any() as Array<String>, any(IntArray::class.java), anyInt()
-        )).thenReturn(ArrayList<BroadcastParams>().apply {
-            this.add(BroadcastParams(packagesToChange[0], uidsToChange[0], IntArray(0),
-                    TEST_USER_ID).apply {
-                this.addPackage(packagesToChange[1], uidsToChange[1])
-            })
-        })
-    }
-
-    protected fun mockDividedSeparatedBroadcastList(allowlist1: IntArray?, allowlist2: IntArray?) {
-        whenever(broadcastHelper.getBroadcastParams(any(Computer::class.java),
-                any() as Array<String>, any(IntArray::class.java), anyInt()
-        )).thenReturn(ArrayList<BroadcastParams>().apply {
-            this.add(BroadcastParams(packagesToChange[0], uidsToChange[0],
-                    allowlist1 ?: IntArray(0), TEST_USER_ID))
-            this.add(BroadcastParams(packagesToChange[1], uidsToChange[1],
-                    allowlist2 ?: IntArray(0), TEST_USER_ID))
-        })
-    }
-}
\ No newline at end of file
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
index df53dfe..dc74469 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackageHelperTest.kt
@@ -45,11 +45,13 @@
         verify(pms).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
         verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_PACKAGES_SUSPENDED),
             nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-            nullable(), nullable(), nullable())
+            nullable(), nullable(), nullable(), nullable())
         verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_SUSPENDED), nullable(),
-            nullable(), any(), eq(TEST_PACKAGE_1), nullable(), any(), any(), nullable(), nullable())
+            nullable(), any(), eq(TEST_PACKAGE_1), nullable(), any(), any(), nullable(),
+            nullable(), nullable())
         verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_SUSPENDED), nullable(),
-            nullable(), any(), eq(TEST_PACKAGE_2), nullable(), any(), any(), nullable(), nullable())
+            nullable(), any(), eq(TEST_PACKAGE_2), nullable(), any(), any(), nullable(),
+            nullable(), nullable())
 
         var modifiedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
         assertThat(modifiedPackages).asList().containsExactly(TEST_PACKAGE_1, TEST_PACKAGE_2)
@@ -135,13 +137,13 @@
         verify(pms, times(2)).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
         verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_PACKAGES_UNSUSPENDED),
             nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-            nullable(), nullable(), nullable())
+            nullable(), nullable(), nullable(), nullable())
         verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_UNSUSPENDED),
             nullable(), nullable(), any(), eq(TEST_PACKAGE_1), nullable(), any(), any(),
-            nullable(), nullable())
+            nullable(), nullable(), nullable())
         verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_UNSUSPENDED),
             nullable(), nullable(), any(), eq(TEST_PACKAGE_2), nullable(), any(), any(),
-            nullable(), nullable())
+            nullable(), nullable(), nullable())
 
         var modifiedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
         assertThat(modifiedPackages).asList().containsExactly(TEST_PACKAGE_1, TEST_PACKAGE_2)
@@ -217,13 +219,13 @@
         verify(pms, times(2)).scheduleWritePackageRestrictions(eq(TEST_USER_ID))
         verify(broadcastHelper).sendPackageBroadcast(eq(Intent.ACTION_PACKAGES_UNSUSPENDED),
             nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-            nullable(), nullable(), nullable())
+            nullable(), nullable(), nullable(), nullable())
         verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_UNSUSPENDED),
             nullable(), nullable(), any(), eq(TEST_PACKAGE_1), nullable(), any(), any(),
-            nullable(), nullable())
+            nullable(), nullable(), nullable())
         verify(broadcastHelper).doSendBroadcast(eq(Intent.ACTION_MY_PACKAGE_UNSUSPENDED),
             nullable(), nullable(), any(), eq(TEST_PACKAGE_2), nullable(), any(), any(),
-            nullable(), nullable())
+            nullable(), nullable(), nullable())
 
         assertThat(suspendPackageHelper.getSuspendingPackage(pms.snapshotComputer(),
             TEST_PACKAGE_1, TEST_USER_ID, deviceOwnerUid)).isNull()
@@ -297,12 +299,13 @@
 
     @Test
     @Throws(Exception::class)
-    fun sendPackagesSuspendedForUser_withSameVisibilityAllowList() {
-        suspendPackageHelper.sendPackagesSuspendedForUser(pms.snapshotComputer(),
+    fun sendPackagesSuspendedForUser() {
+        suspendPackageHelper.sendPackagesSuspendedForUser(
             Intent.ACTION_PACKAGES_SUSPENDED, packagesToChange, uidsToChange, TEST_USER_ID)
         testHandler.flush()
         verify(broadcastHelper).sendPackageBroadcast(any(), nullable(), bundleCaptor.capture(),
-                anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable())
+                anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable(),
+                nullable())
 
         var changedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
         var changedUids = bundleCaptor.value.getIntArray(Intent.EXTRA_CHANGED_UID_LIST)
@@ -313,59 +316,14 @@
 
     @Test
     @Throws(Exception::class)
-    fun sendPackagesSuspendedForUser_withDifferentVisibilityAllowList() {
-        mockDividedSeparatedBroadcastList(
-                intArrayOf(10001, 10002, 10003), intArrayOf(10001, 10002, 10007))
-
-        suspendPackageHelper.sendPackagesSuspendedForUser(pms.snapshotComputer(),
-            Intent.ACTION_PACKAGES_SUSPENDED, packagesToChange, uidsToChange, TEST_USER_ID)
-        testHandler.flush()
-        verify(broadcastHelper, times(2)).sendPackageBroadcast(
-                any(), nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-                nullable(), any(), nullable())
-
-        bundleCaptor.allValues.forEachIndexed { i, it ->
-            var changedPackages = it.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-            var changedUids = it.getIntArray(Intent.EXTRA_CHANGED_UID_LIST)
-            assertThat(changedPackages?.size).isEqualTo(1)
-            assertThat(changedUids?.size).isEqualTo(1)
-            assertThat(changedPackages?.get(0)).isEqualTo(packagesToChange[i])
-            assertThat(changedUids?.get(0)).isEqualTo(uidsToChange[i])
-        }
-    }
-
-    @Test
-    @Throws(Exception::class)
-    fun sendPackagesSuspendedForUser_withNullVisibilityAllowList() {
-        mockDividedSeparatedBroadcastList(intArrayOf(10001, 10002, 10003), null)
-
-        suspendPackageHelper.sendPackagesSuspendedForUser(pms.snapshotComputer(),
-            Intent.ACTION_PACKAGES_SUSPENDED, packagesToChange, uidsToChange, TEST_USER_ID)
-        testHandler.flush()
-        verify(broadcastHelper, times(2)).sendPackageBroadcast(
-                any(), nullable(), bundleCaptor.capture(), anyInt(), nullable(), nullable(), any(),
-                nullable(), nullable(), nullable())
-
-        bundleCaptor.allValues.forEachIndexed { i, it ->
-            var changedPackages = it.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
-            var changedUids = it.getIntArray(Intent.EXTRA_CHANGED_UID_LIST)
-            assertThat(changedPackages?.size).isEqualTo(1)
-            assertThat(changedUids?.size).isEqualTo(1)
-            assertThat(changedPackages?.get(0)).isEqualTo(packagesToChange[i])
-            assertThat(changedUids?.get(0)).isEqualTo(uidsToChange[i])
-        }
-    }
-
-    @Test
-    @Throws(Exception::class)
     fun sendPackagesSuspendModifiedForUser() {
-        suspendPackageHelper.sendPackagesSuspendedForUser(pms.snapshotComputer(),
-            Intent.ACTION_PACKAGES_SUSPENSION_CHANGED, packagesToChange, uidsToChange,
-            TEST_USER_ID)
+        suspendPackageHelper.sendPackagesSuspendedForUser(
+            Intent.ACTION_PACKAGES_SUSPENSION_CHANGED, packagesToChange, uidsToChange, TEST_USER_ID)
         testHandler.flush()
         verify(broadcastHelper).sendPackageBroadcast(
                 eq(Intent.ACTION_PACKAGES_SUSPENSION_CHANGED), nullable(), bundleCaptor.capture(),
-                anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable())
+                anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable(),
+                nullable())
 
         var modifiedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST)
         var modifiedUids = bundleCaptor.value.getIntArray(Intent.EXTRA_CHANGED_UID_LIST)
diff --git a/services/tests/servicestests/res/xml/usertypes_test_profile.xml b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
index daa7d7b..b27f49d 100644
--- a/services/tests/servicestests/res/xml/usertypes_test_profile.xml
+++ b/services/tests/servicestests/res/xml/usertypes_test_profile.xml
@@ -30,6 +30,10 @@
             <item res='@*android:color/profile_badge_2' />
         </badge-colors>
         <default-restrictions no_remove_user='true' no_bluetooth='true' />
+        <user-properties
+            showInLauncher='2020'
+            startWithParent='false'
+        />
     </profile-type>
     <profile-type name='custom.test.1' max-allowed-per-parent='14' />
 
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 bce99a0..2b6be37 100644
--- a/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/BroadcastRecordTest.java
@@ -430,7 +430,8 @@
                 userId,
                 false /* allowBackgroundActivityStarts */,
                 null /* activityStartsToken */,
-                false /* timeoutExempt */ );
+                false /* timeoutExempt */,
+                null /* filterExtrasForReceiver */);
     }
 
     private static int getAppId(int i) {
diff --git a/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java b/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java
index 5be05d8..e8dd541 100644
--- a/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java
+++ b/services/tests/servicestests/src/com/android/server/app/GameManagerServiceSettingsTests.java
@@ -16,9 +16,13 @@
 
 package com.android.server.app;
 
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertThat;
+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.assertTrue;
 
+import android.app.GameManager;
 import android.content.Context;
 import android.platform.test.annotations.Presubmit;
 import android.util.AtomicFile;
@@ -28,6 +32,9 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.server.app.GameManagerService.GamePackageConfiguration;
+import com.android.server.app.GameManagerService.GamePackageConfiguration.GameModeConfiguration;
+
 import org.junit.After;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -66,6 +73,9 @@
                         + "  <package name=\"com.android.app1\" gameMode=\"1\">\n"
                         + "  </package>\n"
                         + "  <package name=\"com.android.app2\" gameMode=\"2\">\n"
+                        + "     <gameModeConfig gameMode=\"2\" scaling=\"0.99\" "
+                        + "useAngle=\"true\" fps=\"90\" loadingBoost=\"123\"></gameModeConfig>\n"
+                        + "     <gameModeConfig gameMode=\"3\"></gameModeConfig>\n"
                         + "  </package>\n"
                         + "  <package name=\"com.android.app3\" gameMode=\"3\">\n"
                         + "  </package>\n"
@@ -92,40 +102,159 @@
         writeGameServiceXml();
     }
 
-    private void verifyGameServiceSettingsData(GameManagerSettings settings) {
-        assertThat(settings.getGameModeLocked(PACKAGE_NAME_1), is(1));
-        assertThat(settings.getGameModeLocked(PACKAGE_NAME_2), is(2));
-        assertThat(settings.getGameModeLocked(PACKAGE_NAME_3), is(3));
-    }
-
     @After
     public void tearDown() throws Exception {
         deleteFolder(InstrumentationRegistry.getTargetContext().getFilesDir());
     }
 
-    /** read in data and verify */
     @Test
     public void testReadGameServiceSettings() {
-        /* write out files and read */
         writeOldFiles();
         final Context context = InstrumentationRegistry.getContext();
         GameManagerSettings settings = new GameManagerSettings(context.getFilesDir());
-        assertThat(settings.readPersistentDataLocked(), is(true));
-        verifyGameServiceSettingsData(settings);
+        assertTrue(settings.readPersistentDataLocked());
+
+        // test game modes
+        assertEquals(1, settings.getGameModeLocked(PACKAGE_NAME_1));
+        assertEquals(2, settings.getGameModeLocked(PACKAGE_NAME_2));
+        assertEquals(3, settings.getGameModeLocked(PACKAGE_NAME_3));
+
+        // test game mode configs
+        assertNull(settings.getConfigOverride(PACKAGE_NAME_1));
+        assertNull(settings.getConfigOverride(PACKAGE_NAME_3));
+        final GamePackageConfiguration config = settings.getConfigOverride(PACKAGE_NAME_2);
+        assertNotNull(config);
+
+        assertNull(config.getGameModeConfiguration(GameManager.GAME_MODE_STANDARD));
+        final GameModeConfiguration performanceConfig = config.getGameModeConfiguration(
+                GameManager.GAME_MODE_PERFORMANCE);
+        assertNotNull(performanceConfig);
+        assertEquals(performanceConfig.getScaling(), 0.99, 0.01f);
+        assertEquals(performanceConfig.getLoadingBoostDuration(), 123);
+        assertEquals(performanceConfig.getFpsStr(), "90");
+        assertTrue(performanceConfig.getUseAngle());
+        final GameModeConfiguration batteryConfig = config.getGameModeConfiguration(
+                GameManager.GAME_MODE_BATTERY);
+        assertNotNull(batteryConfig);
+        assertEquals(batteryConfig.getScaling(), GameModeConfiguration.DEFAULT_SCALING, 0.01f);
+        assertEquals(batteryConfig.getLoadingBoostDuration(),
+                GameModeConfiguration.DEFAULT_LOADING_BOOST_DURATION);
+        assertEquals(batteryConfig.getFpsStr(), GameModeConfiguration.DEFAULT_FPS);
+        assertFalse(batteryConfig.getUseAngle());
     }
 
-    /** read in data, write it out, and read it back in.  Verify same. */
     @Test
-    public void testWriteGameServiceSettings() {
-        // write out files and read
-        writeOldFiles();
+    public void testReadGameServiceSettings_invalidConfigAttributes() {
+        writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(),
+                        "system/game-manager-service.xml"),
+                ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
+                        + "<packages>\n"
+                        + "  <package name=\"com.android.app1\" gameMode=\"1\">\n"
+                        + "     <gameModeConfig gameMode=\"3\" scaling=\"invalid\" "
+                        + "useAngle=\"invalid\" fps=\"invalid\" "
+                        + "loadingBoost=\"invalid\"></gameModeConfig>\n"
+                        + "  </package>\n"
+                        + "</packages>\n").getBytes());
         final Context context = InstrumentationRegistry.getContext();
         GameManagerSettings settings = new GameManagerSettings(context.getFilesDir());
-        assertThat(settings.readPersistentDataLocked(), is(true));
+        assertTrue(settings.readPersistentDataLocked());
 
-        // write out, read back in and verify the same
+        final GamePackageConfiguration config = settings.getConfigOverride(PACKAGE_NAME_1);
+        assertNotNull(config);
+        final GameModeConfiguration batteryConfig = config.getGameModeConfiguration(
+                GameManager.GAME_MODE_BATTERY);
+        assertNotNull(batteryConfig);
+        assertEquals(batteryConfig.getScaling(), GameModeConfiguration.DEFAULT_SCALING, 0.01f);
+        assertEquals(batteryConfig.getLoadingBoostDuration(),
+                GameModeConfiguration.DEFAULT_LOADING_BOOST_DURATION);
+        assertEquals(batteryConfig.getFpsStr(), "invalid");
+        assertFalse(batteryConfig.getUseAngle());
+    }
+
+    @Test
+    public void testReadGameServiceSettings_invalidTags() {
+        writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(),
+                        "system/game-manager-service.xml"),
+                ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
+                        + "<packages>\n"
+                        + "  <package gameMode=\"1\">\n"
+                        + "  </package>\n"
+                        + "  <package name=\"com.android.app2\" gameMode=\"2\">\n"
+                        + "     <unknown></unknown>"
+                        + "     <gameModeConfig gameMode=\"3\" fps=\"90\"></gameModeConfig>\n"
+                        + "     foo bar"
+                        + "  </package>\n"
+                        + "  <unknownTag></unknownTag>\n"
+                        + "    foo bar\n"
+                        + "  <package name=\"com.android.app3\" gameMode=\"3\">\n"
+                        + "  </package>\n"
+                        + "</packages>\n").getBytes());
+        final Context context = InstrumentationRegistry.getContext();
+        GameManagerSettings settings = new GameManagerSettings(context.getFilesDir());
+        assertTrue(settings.readPersistentDataLocked());
+        assertEquals(0, settings.getGameModeLocked(PACKAGE_NAME_1));
+        assertEquals(2, settings.getGameModeLocked(PACKAGE_NAME_2));
+        assertEquals(3, settings.getGameModeLocked(PACKAGE_NAME_3));
+
+        final GamePackageConfiguration config = settings.getConfigOverride(PACKAGE_NAME_2);
+        assertNotNull(config);
+        final GameModeConfiguration batteryConfig = config.getGameModeConfiguration(
+                GameManager.GAME_MODE_BATTERY);
+        assertNotNull(batteryConfig);
+        assertEquals(batteryConfig.getFpsStr(), "90");
+    }
+
+
+    @Test
+    public void testWriteGameServiceSettings() {
+        final Context context = InstrumentationRegistry.getContext();
+        GameManagerSettings settings = new GameManagerSettings(context.getFilesDir());
+
+        // set package settings and write out to file
+        settings.setGameModeLocked(PACKAGE_NAME_1, GameManager.GAME_MODE_BATTERY);
+        settings.setGameModeLocked(PACKAGE_NAME_2, GameManager.GAME_MODE_PERFORMANCE);
+        settings.setGameModeLocked(PACKAGE_NAME_3, GameManager.GAME_MODE_STANDARD);
+        GamePackageConfiguration config = new GamePackageConfiguration(PACKAGE_NAME_2);
+        GameModeConfiguration performanceConfig = config.getOrAddDefaultGameModeConfiguration(
+                GameManager.GAME_MODE_PERFORMANCE);
+        performanceConfig.setLoadingBoostDuration(321);
+        performanceConfig.setScaling(0.66f);
+        performanceConfig.setUseAngle(true);
+        performanceConfig.setFpsStr("60");
+        GameModeConfiguration batteryConfig = config.getOrAddDefaultGameModeConfiguration(
+                GameManager.GAME_MODE_BATTERY);
+        batteryConfig.setScaling(0.77f);
+        settings.setConfigOverride(PACKAGE_NAME_2, config);
         settings.writePersistentDataLocked();
-        assertThat(settings.readPersistentDataLocked(), is(true));
-        verifyGameServiceSettingsData(settings);
+
+        // clear the settings in memory
+        settings.removeGame(PACKAGE_NAME_1);
+        settings.removeGame(PACKAGE_NAME_2);
+        settings.removeGame(PACKAGE_NAME_3);
+
+        // read back in and verify
+        assertTrue(settings.readPersistentDataLocked());
+        assertEquals(3, settings.getGameModeLocked(PACKAGE_NAME_1));
+        assertEquals(2, settings.getGameModeLocked(PACKAGE_NAME_2));
+        assertEquals(1, settings.getGameModeLocked(PACKAGE_NAME_3));
+
+        config = settings.getConfigOverride(PACKAGE_NAME_1);
+        assertNull(config);
+        config = settings.getConfigOverride(PACKAGE_NAME_2);
+        assertNotNull(config);
+        batteryConfig = config.getGameModeConfiguration(GameManager.GAME_MODE_BATTERY);
+        assertNotNull(batteryConfig);
+        assertEquals(batteryConfig.getScaling(), 0.77f, 0.01f);
+        assertEquals(batteryConfig.getLoadingBoostDuration(),
+                GameModeConfiguration.DEFAULT_LOADING_BOOST_DURATION);
+        assertEquals(batteryConfig.getFpsStr(), GameModeConfiguration.DEFAULT_FPS);
+        assertFalse(batteryConfig.getUseAngle());
+
+        performanceConfig = config.getGameModeConfiguration(GameManager.GAME_MODE_PERFORMANCE);
+        assertNotNull(performanceConfig);
+        assertEquals(performanceConfig.getScaling(), 0.66f, 0.01f);
+        assertEquals(performanceConfig.getLoadingBoostDuration(), 321);
+        assertEquals(performanceConfig.getFpsStr(), "60");
+        assertTrue(performanceConfig.getUseAngle());
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java
new file mode 100644
index 0000000..903ed90
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/FaceServiceRegistryTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.biometrics.sensors.face;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
+import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
+import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.face.FaceSensorProperties;
+import android.hardware.face.FaceSensorPropertiesInternal;
+import android.hardware.face.IFaceAuthenticatorsRegisteredCallback;
+import android.hardware.face.IFaceService;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Presubmit
+@SmallTest
+public class FaceServiceRegistryTest {
+
+    private static final int SENSOR_ID_1 = 1;
+    private static final int SENSOR_ID_2 = 2;
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private IBiometricService mBiometricService;
+    @Mock
+    private IFaceService mFaceService;
+    @Mock
+    private ServiceProvider mProvider1;
+    @Mock
+    private ServiceProvider mProvider2;
+    @Captor
+    private ArgumentCaptor<Integer> mIdCaptor;
+    @Captor
+    private ArgumentCaptor<Integer> mStrengthCaptor;
+
+    private FaceSensorPropertiesInternal mProvider1Props;
+    private FaceSensorPropertiesInternal mProvider2Props;
+    private FaceServiceRegistry mRegistry;
+
+    @Before
+    public void setup() {
+        mProvider1Props = new FaceSensorPropertiesInternal(SENSOR_ID_1,
+                STRENGTH_WEAK, 5 /* maxEnrollmentsPerUser */,
+                List.of(), FaceSensorProperties.TYPE_RGB,
+                true /* supportsFace Detection */,
+                true /* supportsSelfIllumination */,
+                false /* resetLockoutRequiresHardwareAuthToken */);
+        mProvider2Props = new FaceSensorPropertiesInternal(SENSOR_ID_2,
+                STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */,
+                List.of(), FaceSensorProperties.TYPE_IR,
+                true /* supportsFace Detection */,
+                true /* supportsSelfIllumination */,
+                false /* resetLockoutRequiresHardwareAuthToken */);
+
+        when(mProvider1.getSensorProperties()).thenReturn(List.of(mProvider1Props));
+        when(mProvider1.containsSensor(eq(SENSOR_ID_1))).thenReturn(true);
+        when(mProvider2.getSensorProperties()).thenReturn(List.of(mProvider2Props));
+        when(mProvider2.containsSensor(eq(SENSOR_ID_2))).thenReturn(true);
+        mRegistry = new FaceServiceRegistry(mFaceService, () -> mBiometricService);
+    }
+
+    @Test
+    public void registersAllProviders() throws Exception {
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2));
+
+        assertThat(mRegistry.getProviders()).containsExactly(mProvider1, mProvider2);
+        assertThat(mRegistry.getAllProperties()).containsExactly(mProvider1Props, mProvider2Props);
+        verify(mBiometricService, times(2)).registerAuthenticator(
+                mIdCaptor.capture(), eq(TYPE_FACE), mStrengthCaptor.capture(), any());
+        assertThat(mIdCaptor.getAllValues()).containsExactly(SENSOR_ID_1, SENSOR_ID_2);
+        assertThat(mStrengthCaptor.getAllValues())
+                .containsExactly(BIOMETRIC_WEAK, BIOMETRIC_STRONG);
+    }
+
+    @Test
+    public void getsProviderById() {
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2));
+
+        assertThat(mRegistry.getProviderForSensor(SENSOR_ID_1)).isSameInstanceAs(mProvider1);
+        assertThat(mRegistry.getProviderForSensor(SENSOR_ID_2)).isSameInstanceAs(mProvider2);
+        assertThat(mRegistry.getProviderForSensor(500)).isNull();
+    }
+
+    @Test
+    public void getsSingleProvider() {
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1));
+
+        assertThat(mRegistry.getSingleProvider().second).isSameInstanceAs(mProvider1);
+        assertThat(mRegistry.getProviders()).containsExactly(mProvider1);
+        assertThat(mRegistry.getProviderForSensor(SENSOR_ID_1)).isSameInstanceAs(mProvider1);
+    }
+
+    @Test
+    public void getSingleProviderFindsFirstWhenMultiple() {
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2));
+
+        assertThat(mRegistry.getSingleProvider().second).isSameInstanceAs(mProvider1);
+    }
+
+    @Test
+    public void registersListenerBeforeAllRegistered() {
+        final List<FaceSensorPropertiesInternal> all = new ArrayList<>();
+        mRegistry.addAllRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() {
+            @Override
+            public void onAllAuthenticatorsRegistered(
+                    List<FaceSensorPropertiesInternal> sensors) {
+                all.addAll(sensors);
+            }
+        });
+
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2));
+
+        assertThat(all).containsExactly(mProvider1Props, mProvider2Props);
+    }
+
+    @Test
+    public void registersListenerAfterAllRegistered() {
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2));
+
+        final List<FaceSensorPropertiesInternal> all = new ArrayList<>();
+        mRegistry.addAllRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() {
+            @Override
+            public void onAllAuthenticatorsRegistered(
+                    List<FaceSensorPropertiesInternal> sensors) {
+                all.addAll(sensors);
+            }
+        });
+
+        assertThat(all).containsExactly(mProvider1Props, mProvider2Props);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java
index 5f88c99..0e30782 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/BiometricStateCallbackTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors.fingerprint;
 
+import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
+
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -24,38 +26,73 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.pm.UserInfo;
 import android.hardware.biometrics.BiometricStateListener;
+import android.hardware.biometrics.SensorPropertiesInternal;
+import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.server.biometrics.sensors.AuthenticationClient;
+import com.android.server.biometrics.sensors.BiometricServiceProvider;
 import com.android.server.biometrics.sensors.BiometricStateCallback;
 import com.android.server.biometrics.sensors.EnrollClient;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
 
 @Presubmit
 @SmallTest
 public class BiometricStateCallbackTest {
 
-    private BiometricStateCallback mCallback;
+    private static final int USER_ID = 10;
+    private static final int SENSOR_ID = 2;
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    private BiometricStateCallback<FakeProvider, SensorPropertiesInternal> mCallback;
 
     @Mock
-    BiometricStateListener mBiometricStateListener;
+    private UserManager mUserManager;
+    @Mock
+    private BiometricStateListener mBiometricStateListener;
+    @Mock
+    private FakeProvider mFakeProvider;
+
+    private SensorPropertiesInternal mFakeProviderProps;
 
     @Before
     public void setup() {
-        MockitoAnnotations.initMocks(this);
+        mFakeProviderProps = new SensorPropertiesInternal(SENSOR_ID, STRENGTH_STRONG,
+                5 /* maxEnrollmentsPerUser */, List.of(),
+                false /* resetLockoutRequiresHardwareAuthToken */,
+                false /* resetLockoutRequiresChallenge */);
+        when(mFakeProvider.getSensorProperties()).thenReturn(List.of(mFakeProviderProps));
+        when(mFakeProvider.getSensorProperties(eq(SENSOR_ID))).thenReturn(mFakeProviderProps);
+        when(mFakeProvider.hasEnrollments(eq(SENSOR_ID), eq(USER_ID))).thenReturn(true);
+        when(mUserManager.getAliveUsers()).thenReturn(
+                List.of(new UserInfo(USER_ID, "name", 0)));
 
-        mCallback = new BiometricStateCallback();
+        mCallback = new BiometricStateCallback<>(mUserManager);
         mCallback.registerBiometricStateListener(mBiometricStateListener);
     }
 
     @Test
+    public void startNotifiesEnrollments() {
+        mCallback.start(List.of(mFakeProvider));
+
+        verify(mBiometricStateListener).onEnrollmentsChanged(eq(USER_ID), eq(SENSOR_ID), eq(true));
+    }
+
+    @Test
     public void testNoEnrollmentsToEnrollments_callbackNotified() {
         testEnrollmentCallback(true /* changed */, true /* isNowEnrolled */,
                 true /* expectCallback */, true /* expectedCallbackValue */);
@@ -102,4 +139,6 @@
         verify(mBiometricStateListener, never()).onEnrollmentsChanged(anyInt(), anyInt(),
                 anyBoolean());
     }
+
+    private interface FakeProvider extends BiometricServiceProvider<SensorPropertiesInternal> {}
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java
new file mode 100644
index 0000000..13c3f64
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceRegistryTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.biometrics.sensors.fingerprint;
+
+import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG;
+import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK;
+import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
+import static android.hardware.biometrics.SensorProperties.STRENGTH_WEAK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.biometrics.IBiometricService;
+import android.hardware.fingerprint.FingerprintSensorProperties;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
+import android.hardware.fingerprint.IFingerprintService;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Presubmit
+@SmallTest
+public class FingerprintServiceRegistryTest {
+
+    private static final int SENSOR_ID_1 = 1;
+    private static final int SENSOR_ID_2 = 2;
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private IBiometricService mBiometricService;
+    @Mock
+    private IFingerprintService mFingerprintService;
+    @Mock
+    private ServiceProvider mProvider1;
+    @Mock
+    private ServiceProvider mProvider2;
+    @Captor
+    private ArgumentCaptor<Integer> mIdCaptor;
+    @Captor
+    private ArgumentCaptor<Integer> mStrengthCaptor;
+
+    private FingerprintSensorPropertiesInternal mProvider1Props;
+    private FingerprintSensorPropertiesInternal mProvider2Props;
+    private FingerprintServiceRegistry mRegistry;
+
+    @Before
+    public void setup() {
+        mProvider1Props = new FingerprintSensorPropertiesInternal(SENSOR_ID_1,
+                STRENGTH_WEAK, 5 /* maxEnrollmentsPerUser */,
+                List.of(), FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
+                false /* resetLockoutRequiresHardwareAuthToken */);
+        mProvider2Props = new FingerprintSensorPropertiesInternal(SENSOR_ID_2,
+                STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */,
+                List.of(), FingerprintSensorProperties.TYPE_UNKNOWN,
+                false /* resetLockoutRequiresHardwareAuthToken */);
+
+        when(mProvider1.getSensorProperties()).thenReturn(List.of(mProvider1Props));
+        when(mProvider1.containsSensor(eq(SENSOR_ID_1))).thenReturn(true);
+        when(mProvider2.getSensorProperties()).thenReturn(List.of(mProvider2Props));
+        when(mProvider2.containsSensor(eq(SENSOR_ID_2))).thenReturn(true);
+        mRegistry = new FingerprintServiceRegistry(mFingerprintService, () -> mBiometricService);
+    }
+
+    @Test
+    public void registersAllProviders() throws Exception {
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2));
+
+        assertThat(mRegistry.getProviders()).containsExactly(mProvider1, mProvider2);
+        assertThat(mRegistry.getAllProperties()).containsExactly(mProvider1Props, mProvider2Props);
+        verify(mBiometricService, times(2)).registerAuthenticator(
+                mIdCaptor.capture(), eq(TYPE_FINGERPRINT), mStrengthCaptor.capture(), any());
+        assertThat(mIdCaptor.getAllValues()).containsExactly(SENSOR_ID_1, SENSOR_ID_2);
+        assertThat(mStrengthCaptor.getAllValues())
+                .containsExactly(BIOMETRIC_WEAK, BIOMETRIC_STRONG);
+    }
+
+    @Test
+    public void getsProviderById() {
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2));
+
+        assertThat(mRegistry.getProviderForSensor(SENSOR_ID_1)).isSameInstanceAs(mProvider1);
+        assertThat(mRegistry.getProviderForSensor(SENSOR_ID_2)).isSameInstanceAs(mProvider2);
+        assertThat(mRegistry.getProviderForSensor(500)).isNull();
+    }
+
+    @Test
+    public void getsSingleProvider() {
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1));
+
+        assertThat(mRegistry.getSingleProvider().second).isSameInstanceAs(mProvider1);
+        assertThat(mRegistry.getProviders()).containsExactly(mProvider1);
+        assertThat(mRegistry.getProviderForSensor(SENSOR_ID_1)).isSameInstanceAs(mProvider1);
+    }
+
+    @Test
+    public void getSingleProviderFindsFirstWhenMultiple() {
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2));
+
+        assertThat(mRegistry.getSingleProvider().second).isSameInstanceAs(mProvider1);
+    }
+
+    @Test
+    public void registersListenerBeforeAllRegistered() {
+        final List<FingerprintSensorPropertiesInternal> all = new ArrayList<>();
+        mRegistry.addAllRegisteredCallback(new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
+            @Override
+            public void onAllAuthenticatorsRegistered(
+                    List<FingerprintSensorPropertiesInternal> sensors) {
+                all.addAll(sensors);
+            }
+        });
+
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2));
+
+        assertThat(all).containsExactly(mProvider1Props, mProvider2Props);
+    }
+
+    @Test
+    public void registersListenerAfterAllRegistered() {
+        mRegistry.registerAllInBackground(() -> List.of(mProvider1, mProvider2));
+
+        final List<FingerprintSensorPropertiesInternal> all = new ArrayList<>();
+        mRegistry.addAllRegisteredCallback(new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
+            @Override
+            public void onAllAuthenticatorsRegistered(
+                    List<FingerprintSensorPropertiesInternal> sensors) {
+                all.addAll(sensors);
+            }
+        });
+
+        assertThat(all).containsExactly(mProvider1Props, mProvider2Props);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
index ca3677e..a4048a2 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/FingerprintServiceTest.java
@@ -32,7 +32,8 @@
 import android.hardware.biometrics.fingerprint.IFingerprint;
 import android.hardware.biometrics.fingerprint.SensorLocation;
 import android.hardware.biometrics.fingerprint.SensorProps;
-import android.os.RemoteException;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
 import android.platform.test.annotations.Presubmit;
 import android.provider.Settings;
 import android.testing.TestableContext;
@@ -52,6 +53,8 @@
 import org.mockito.junit.MockitoRule;
 
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 @Presubmit
 @SmallTest
@@ -94,9 +97,12 @@
 
         mContext.getTestablePermissions().setPermission(
                 USE_BIOMETRIC_INTERNAL, PackageManager.PERMISSION_GRANTED);
+    }
 
+    private void initServiceWith(String... aidlInstances) {
         mService = new FingerprintService(mContext, mBiometricContext,
                 () -> mIBiometricService,
+                () -> aidlInstances,
                 (fqName) -> {
                     if (fqName.endsWith(NAME_DEFAULT)) return mIFingerprintDefault;
                     if (fqName.endsWith(NAME_VIRTUAL)) return mIFingerprintVirtual;
@@ -105,29 +111,50 @@
     }
 
     @Test
-    public void registerAuthenticators_defaultOnly() throws RemoteException {
-        mService.registerAuthenticatorsForService(List.of(NAME_DEFAULT, NAME_VIRTUAL), List.of());
+    public void registerAuthenticators_defaultOnly() throws Exception {
+        initServiceWith(NAME_DEFAULT, NAME_VIRTUAL);
+
+        mService.mServiceWrapper.registerAuthenticators(List.of());
+        waitForRegistration();
 
         verify(mIBiometricService).registerAuthenticator(eq(ID_DEFAULT), anyInt(), anyInt(), any());
     }
 
     @Test
-    public void registerAuthenticators_virtualOnly() throws RemoteException {
+    public void registerAuthenticators_virtualOnly() throws Exception {
+        initServiceWith(NAME_DEFAULT, NAME_VIRTUAL);
         Settings.Secure.putInt(mSettingsRule.mockContentResolver(mContext),
                 Settings.Secure.BIOMETRIC_VIRTUAL_ENABLED, 1);
 
-        mService.registerAuthenticatorsForService(List.of(NAME_DEFAULT, NAME_VIRTUAL), List.of());
+        mService.mServiceWrapper.registerAuthenticators(List.of());
+        waitForRegistration();
 
         verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any());
     }
 
     @Test
-    public void registerAuthenticators_virtualAlwaysWhenNoOther() throws RemoteException {
-        mService.registerAuthenticatorsForService(List.of(NAME_VIRTUAL), List.of());
+    public void registerAuthenticators_virtualAlwaysWhenNoOther() throws Exception {
+        initServiceWith(NAME_VIRTUAL);
+
+        mService.mServiceWrapper.registerAuthenticators(List.of());
+        waitForRegistration();
 
         verify(mIBiometricService).registerAuthenticator(eq(ID_VIRTUAL), anyInt(), anyInt(), any());
     }
 
+    private void waitForRegistration() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        mService.mServiceWrapper.addAuthenticatorsRegisteredCallback(
+                new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
+                    @Override
+                    public void onAllAuthenticatorsRegistered(
+                            List<FingerprintSensorPropertiesInternal> sensors) {
+                        latch.countDown();
+                    }
+                });
+        latch.await(5, TimeUnit.SECONDS);
+    }
+
     private static SensorProps createProps(int id, byte strength, byte type) {
         final SensorProps props = new SensorProps();
         props.commonProps = new CommonProps();
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index e2c3a94..4c939f0 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -83,6 +83,7 @@
                 VirtualDeviceParams.ACTIVITY_POLICY_DEFAULT_ALLOWED,
                 /* activityListener= */ null,
                 /* activityBlockedCallback= */ null,
+                /* secureWindowCallback= */ null,
                 /* deviceProfile= */ DEVICE_PROFILE_APP_STREAMING);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java
index c4435b7..f69c5c2 100644
--- a/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/brightness/BrightnessEventTest.java
@@ -63,7 +63,7 @@
         BrightnessEvent secondBrightnessEvent = new BrightnessEvent(1);
         secondBrightnessEvent.copyFrom(mBrightnessEvent);
         secondBrightnessEvent.setTime(0);
-        assertEquals(secondBrightnessEvent.equalsMainData(mBrightnessEvent), true);
+        assertEquals(true, secondBrightnessEvent.equalsMainData(mBrightnessEvent));
     }
 
     @Test
@@ -74,9 +74,28 @@
                 + " preBrt=NaN, lux=100.0, fastLux=90.0, slowLux=85.0, preLux=150.0, hbmMax=0.62,"
                 + " hbmMode=off, rbcStrength=-1, thrmMax=0.65, powerFactor=0.2, flags=, reason=doze"
                 + " [ low_pwr ], autoBrightness=true";
-        assertEquals(actualString, expectedString);
+        assertEquals(expectedString, actualString);
     }
 
+    @Test
+    public void testFlagsToString() {
+        mBrightnessEvent.reset();
+        mBrightnessEvent.setFlags(mBrightnessEvent.getFlags() | BrightnessEvent.FLAG_IDLE_CURVE);
+        String actualString = mBrightnessEvent.flagsToString();
+        String expectedString = "idle_curve ";
+        assertEquals(expectedString, actualString);
+    }
+
+    @Test
+    public void testFlagsToString_multipleFlags() {
+        mBrightnessEvent.reset();
+        mBrightnessEvent.setFlags(mBrightnessEvent.getFlags()
+                    | BrightnessEvent.FLAG_IDLE_CURVE
+                    | BrightnessEvent.FLAG_LOW_POWER_MODE);
+        String actualString = mBrightnessEvent.flagsToString();
+        String expectedString = "idle_curve low_power_mode ";
+        assertEquals(expectedString, actualString);
+    }
 
 
     private BrightnessReason getReason(int reason, int modifier) {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index 0f6addb..fe9e0b6 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -47,7 +47,6 @@
 import org.junit.runners.JUnit4;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.concurrent.TimeUnit;
 
 @SmallTest
@@ -79,6 +78,7 @@
     private TestLooper mTestLooper = new TestLooper();
     private FakePowerManagerWrapper mPowerManager;
     private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
+    private ArrayList<Integer> mLocalDeviceTypes = new ArrayList<>();
     private int mPlaybackPhysicalAddress;
     private int mPlaybackLogicalAddress;
     private boolean mWokenUp;
@@ -91,9 +91,11 @@
         Context context = InstrumentationRegistry.getTargetContext();
         mMyLooper = mTestLooper.getLooper();
 
+        mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK);
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
+                        mLocalDeviceTypes, new FakeAudioDeviceVolumeManagerWrapper()) {
+
                     @Override
                     void wakeUp() {
                         mWokenUp = true;
@@ -121,16 +123,6 @@
                     }
 
                     @Override
-                    boolean isPowerStandby() {
-                        return false;
-                    }
-
-                    @Override
-                    boolean isPowerStandbyOrTransient() {
-                        return false;
-                    }
-
-                    @Override
                     boolean canGoToStandby() {
                         return true;
                     }
@@ -165,6 +157,7 @@
         mNativeWrapper.clearResultMessages();
         mHdmiCecLocalDevicePlayback.mPlaybackDeviceActionOnRoutingControl =
                 HdmiProperties.playback_device_action_on_routing_control_values.NONE;
+        mHdmiControlService.setPowerStatus(HdmiControlManager.POWER_STATUS_ON);
     }
 
     @Test
@@ -1199,6 +1192,9 @@
         HdmiCecMessage setStreamPath = HdmiCecMessageBuilder.buildSetStreamPath(ADDR_TV,
                 mPlaybackPhysicalAddress);
         mHdmiCecLocalDevicePlayback.dispatchMessage(setStreamPath);
+        // The ActiveSourceAction created from the message above is deferred until the device wakes
+        // up.
+        mHdmiControlService.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON);
         mTestLooper.dispatchAll();
         HdmiCecMessage activeSource =
                 HdmiCecMessageBuilder.buildActiveSource(
@@ -2088,4 +2084,111 @@
         assertThat(mHdmiControlService.getHdmiCecNetwork().getDeviceInfoList(false))
                 .isEmpty();
     }
+
+    @Test
+    public void handleRoutingChange_addressNotAllocated_removeActiveSourceAction() {
+        long allocationDelay = TimeUnit.SECONDS.toMillis(60);
+        mHdmiCecLocalDevicePlayback.mPlaybackDeviceActionOnRoutingControl =
+                HdmiProperties
+                        .playback_device_action_on_routing_control_values
+                        .WAKE_UP_AND_SEND_ACTIVE_SOURCE;
+        mHdmiCecLocalDevicePlayback.setActiveSource(ADDR_TV, 0x0000,
+                "HdmiCecLocalDevicePlaybackTest");
+        HdmiCecMessage routingChangeToPlayback =
+                HdmiCecMessageBuilder.buildRoutingChange(ADDR_TV, 0x0000,
+                        mPlaybackPhysicalAddress);
+        HdmiCecMessage routingChangeToTv =
+                HdmiCecMessageBuilder.buildRoutingChange(ADDR_TV, mPlaybackPhysicalAddress,
+                        0x0000);
+        HdmiCecMessage unexpectedMessage =
+                HdmiCecMessageBuilder.buildActiveSource(mPlaybackLogicalAddress,
+                        mPlaybackPhysicalAddress);
+        // 1. DUT goes to sleep.
+        mHdmiControlService.onStandby(HdmiControlService.STANDBY_SCREEN_OFF);
+        // Delay allocate logical address in order to trigger message buffering.
+        mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+        mNativeWrapper.onCecMessage(routingChangeToPlayback);
+        mTestLooper.dispatchAll();
+        // 2. DUT wakes up and defer ActiveSourceAction.
+        mHdmiControlService.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON);
+        mTestLooper.dispatchAll();
+        assertThat(mHdmiCecLocalDevicePlayback.getActions(ActiveSourceAction.class)).isNotEmpty();
+        // 3. DUT buffers <Routing Change> message to TV.
+        mNativeWrapper.onCecMessage(routingChangeToTv);
+        mTestLooper.dispatchAll();
+        // 4. Allocation is finished and the ActiveSourceAction is removed from the queue.
+        // No <Active Source> message is sent by the DUT.
+        mTestLooper.moveTimeForward(allocationDelay);
+        mTestLooper.dispatchAll();
+        assertThat(mHdmiCecLocalDevicePlayback.getActions(ActiveSourceAction.class)).isEmpty();
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(unexpectedMessage);
+    }
+
+    @Test
+    public void handleSetStreamPath_addressNotAllocated_removeActiveSourceAction() {
+        long allocationDelay = TimeUnit.SECONDS.toMillis(60);
+        mHdmiCecLocalDevicePlayback.setActiveSource(ADDR_TV, 0x0000,
+                "HdmiCecLocalDevicePlaybackTest");
+        HdmiCecMessage setStreamPathToPlayback =
+                HdmiCecMessageBuilder.buildSetStreamPath(ADDR_TV, mPlaybackPhysicalAddress);
+        HdmiCecMessage setStreamPathToTv =
+                HdmiCecMessageBuilder.buildSetStreamPath(ADDR_TV, 0x0000);
+        HdmiCecMessage unexpectedMessage =
+                HdmiCecMessageBuilder.buildActiveSource(mPlaybackLogicalAddress,
+                        mPlaybackPhysicalAddress);
+        // 1. DUT goes to sleep.
+        mHdmiControlService.onStandby(HdmiControlService.STANDBY_SCREEN_OFF);
+        // Delay allocate logical address in order to trigger message buffering.
+        mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+        mNativeWrapper.onCecMessage(setStreamPathToPlayback);
+        mTestLooper.dispatchAll();
+        // 2. DUT wakes up and defer ActiveSourceAction.
+        mHdmiControlService.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON);
+        mTestLooper.dispatchAll();
+        assertThat(mHdmiCecLocalDevicePlayback.getActions(ActiveSourceAction.class)).isNotEmpty();
+        // 3. DUT buffers <Set Stream Path> message to TV.
+        mNativeWrapper.onCecMessage(setStreamPathToTv);
+        mTestLooper.dispatchAll();
+        // 4. Allocation is finished and the ActiveSourceAction is removed from the queue.
+        // No <Active Source> message is sent by the DUT.
+        mTestLooper.moveTimeForward(allocationDelay);
+        mTestLooper.dispatchAll();
+        assertThat(mHdmiCecLocalDevicePlayback.getActions(ActiveSourceAction.class)).isEmpty();
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(unexpectedMessage);
+    }
+
+    @Test
+    public void handleActiveSource_addressNotAllocated_removeActiveSourceAction() {
+        long allocationDelay = TimeUnit.SECONDS.toMillis(60);
+
+        mHdmiCecLocalDevicePlayback.setActiveSource(ADDR_TV, 0x0000,
+                "HdmiCecLocalDevicePlaybackTest");
+
+        HdmiCecMessage setStreamPathToPlayback =
+                HdmiCecMessageBuilder.buildSetStreamPath(ADDR_TV, mPlaybackPhysicalAddress);
+        HdmiCecMessage activeSourceFromTv =
+                HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
+        HdmiCecMessage unexpectedMessage =
+                HdmiCecMessageBuilder.buildActiveSource(mPlaybackLogicalAddress,
+                        mPlaybackPhysicalAddress);
+        // 1. DUT goes to sleep.
+        mHdmiControlService.onStandby(HdmiControlService.STANDBY_SCREEN_OFF);
+        // Delay allocate logical address in order to trigger message buffering.
+        mHdmiCecController.setLogicalAddressAllocationDelay(allocationDelay);
+        mNativeWrapper.onCecMessage(setStreamPathToPlayback);
+        mTestLooper.dispatchAll();
+        // 2. DUT wakes up and defer ActiveSourceAction.
+        mHdmiControlService.onWakeUp(HdmiControlService.WAKE_UP_SCREEN_ON);
+        mTestLooper.dispatchAll();
+        assertThat(mHdmiCecLocalDevicePlayback.getActions(ActiveSourceAction.class)).isNotEmpty();
+        // 3. DUT buffers <Active Source> message from TV.
+        mNativeWrapper.onCecMessage(activeSourceFromTv);
+        mTestLooper.dispatchAll();
+        // 4. Allocation is finished and the ActiveSourceAction is removed from the queue.
+        // No <Active Source> message is sent by the DUT.
+        mTestLooper.moveTimeForward(allocationDelay);
+        mTestLooper.dispatchAll();
+        assertThat(mHdmiCecLocalDevicePlayback.getActions(ActiveSourceAction.class)).isEmpty();
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(unexpectedMessage);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/input/OWNERS b/services/tests/servicestests/src/com/android/server/input/OWNERS
index d701f23..6e9aa1d 100644
--- a/services/tests/servicestests/src/com/android/server/input/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/input/OWNERS
@@ -1 +1,2 @@
-include /core/java/android/hardware/input/OWNERS
+include /services/core/java/com/android/server/input/OWNERS
+
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
index a4cccb3..3477288 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -369,6 +369,8 @@
                     throws Exception {
         final LockscreenCredential parentPassword = newPassword("parentPassword");
         final LockscreenCredential profilePassword = newPattern("12345");
+        mService.setSeparateProfileChallengeEnabled(
+                MANAGED_PROFILE_USER_ID, true, profilePassword);
         initializeStorageWithCredential(PRIMARY_USER_ID, parentPassword);
         // Create and verify separate profile credentials.
         testCreateCredential(MANAGED_PROFILE_USER_ID, profilePassword);
@@ -550,11 +552,12 @@
             throws RemoteException {
         assertEquals(0, mGateKeeperService.getSecureUserId(userId));
         synchronized (mService.mSpManager) {
-            mService.initializeSyntheticPasswordLocked(credential, userId);
+            mService.initializeSyntheticPasswordLocked(userId);
         }
         if (credential.isNone()) {
             assertEquals(0, mGateKeeperService.getSecureUserId(userId));
         } else {
+            assertTrue(mService.setLockCredential(credential, nonePassword(), userId));
             assertNotEquals(0, mGateKeeperService.getSecureUserId(userId));
         }
     }
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 34b40c7..b1ad8ec 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -16,53 +16,79 @@
 
 package com.android.server.pm;
 
+import static android.os.UserManager.DISALLOW_USER_SWITCH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.ActivityManager;
+import android.app.PropertyInvalidatedCache;
+import android.content.Context;
 import android.content.pm.UserInfo;
 import android.os.Bundle;
 import android.os.FileUtils;
+import android.os.Looper;
 import android.os.Parcelable;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.Postsubmit;
 import android.support.test.uiautomator.UiDevice;
-import android.test.AndroidTestCase;
-import android.text.TextUtils;
 import android.util.AtomicFile;
 
 import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.LocalServices;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 
 import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
 
+/** Test {@link UserManagerService} functionality. */
 @Postsubmit
-@SmallTest
-public class UserManagerServiceTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class UserManagerServiceTest {
     private static String[] STRING_ARRAY = new String[] {"<tag", "<![CDATA["};
     private File restrictionsFile;
     private int tempUserId = UserHandle.USER_NULL;
+    private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
+    private UserManagerService mUserManagerService;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    @Before
+    public void setup() throws Exception {
+        // Currently UserManagerService cannot be instantiated twice inside a VM without a cleanup
+        // TODO: Remove once UMS supports proper dependency injection
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+        // Disable binder caches in this process.
+        PropertyInvalidatedCache.disableForTestMode();
+
+        LocalServices.removeServiceForTest(UserManagerInternal.class);
+        mUserManagerService = new UserManagerService(InstrumentationRegistry.getContext());
+
         restrictionsFile = new File(mContext.getCacheDir(), "restrictions.xml");
         restrictionsFile.delete();
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void teardown() throws Exception {
         restrictionsFile.delete();
         if (tempUserId != UserHandle.USER_NULL) {
             UserManager.get(mContext).removeUser(tempUserId);
         }
-        super.tearDown();
     }
 
+    @Test
     public void testWriteReadApplicationRestrictions() throws IOException {
         AtomicFile atomicFile = new AtomicFile(restrictionsFile);
         Bundle bundle = createBundle();
         UserManagerService.writeApplicationRestrictionsLAr(bundle, atomicFile);
-        assertTrue(atomicFile.getBaseFile().exists());
+        assertThat(atomicFile.getBaseFile().exists()).isTrue();
         String s = FileUtils.readTextFile(restrictionsFile, 10000, "");
         System.out.println("restrictionsFile: " + s);
         bundle = UserManagerService.readApplicationRestrictionsLAr(atomicFile);
@@ -70,22 +96,22 @@
         assertBundle(bundle);
     }
 
+    @Test
     public void testAddUserWithAccount() {
         UserManager um = UserManager.get(mContext);
         UserInfo user = um.createUser("Test User", 0);
-        assertNotNull(user);
+        assertThat(user).isNotNull();
         tempUserId = user.id;
         String accountName = "Test Account";
         um.setUserAccount(tempUserId, accountName);
-        assertEquals(accountName, um.getUserAccount(tempUserId));
+        assertThat(um.getUserAccount(tempUserId)).isEqualTo(accountName);
     }
 
+    @Test
     public void testUserSystemPackageWhitelist() throws Exception {
         String cmd = "cmd user report-system-user-package-whitelist-problems --critical-only";
         final String result = runShellCommand(cmd);
-        if (!TextUtils.isEmpty(result)) {
-            fail("Command '" + cmd + " reported errors:\n" + result);
-        }
+        assertThat(result).isEmpty();
     }
 
     private Bundle createBundle() {
@@ -114,26 +140,141 @@
     }
 
     private void assertBundle(Bundle bundle) {
-        assertFalse(bundle.getBoolean("boolean_0"));
-        assertTrue(bundle.getBoolean("boolean_1"));
-        assertEquals(100, bundle.getInt("integer"));
-        assertEquals("", bundle.getString("empty"));
-        assertEquals("text", bundle.getString("string"));
-        assertEquals(Arrays.asList(STRING_ARRAY), Arrays.asList(bundle.getStringArray("string[]")));
+        assertThat(bundle.getBoolean("boolean_0")).isFalse();
+        assertThat(bundle.getBoolean("boolean_1")).isTrue();
+        assertThat(bundle.getInt("integer")).isEqualTo(100);
+        assertThat(bundle.getString("empty")).isEqualTo("");
+        assertThat(bundle.getString("string")).isEqualTo("text");
+        assertThat(Arrays.asList(bundle.getStringArray("string[]")))
+                .isEqualTo(Arrays.asList(STRING_ARRAY));
         Parcelable[] bundle_array = bundle.getParcelableArray("bundle_array");
-        assertEquals(2, bundle_array.length);
+        assertThat(bundle_array.length).isEqualTo(2);
         Bundle bundle1 = (Bundle) bundle_array[0];
-        assertEquals("bundle_array_string", bundle1.getString("bundle_array_string"));
-        assertNotNull(bundle1.getBundle("bundle_array_bundle"));
+        assertThat(bundle1.getString("bundle_array_string"))
+                .isEqualTo("bundle_array_string");
+        assertThat(bundle1.getBundle("bundle_array_bundle")).isNotNull();
         Bundle bundle2 = (Bundle) bundle_array[1];
-        assertEquals("bundle_array_string2", bundle2.getString("bundle_array_string2"));
+        assertThat(bundle2.getString("bundle_array_string2"))
+                .isEqualTo("bundle_array_string2");
         Bundle childBundle = bundle.getBundle("bundle");
-        assertEquals("bundle_string", childBundle.getString("bundle_string"));
-        assertEquals(1, childBundle.getInt("bundle_int"));
+        assertThat(childBundle.getString("bundle_string"))
+                .isEqualTo("bundle_string");
+        assertThat(childBundle.getInt("bundle_int")).isEqualTo(1);
     }
 
+    @Test
+    public void assertHasUserRestriction() throws Exception {
+        int userId = ActivityManager.getCurrentUser();
+
+        mUserManagerService.setUserRestriction(DISALLOW_USER_SWITCH, true, userId);
+        assertThat(mUserManagerService.hasUserRestriction(DISALLOW_USER_SWITCH, userId)).isTrue();
+
+        mUserManagerService.setUserRestriction(DISALLOW_USER_SWITCH, false, userId);
+        assertThat(mUserManagerService.hasUserRestriction(DISALLOW_USER_SWITCH, userId)).isFalse();
+    }
+
+    @Test
+    public void assertIsUserSwitcherEnabledOnMultiUserSettings() throws Exception {
+        int userId = ActivityManager.getCurrentUser();
+        resetUserSwitcherEnabled();
+
+        setUserSwitch(false);
+        assertThat(mUserManagerService.isUserSwitcherEnabled(userId)).isFalse();
+
+        setUserSwitch(true);
+        assertThat(mUserManagerService.isUserSwitcherEnabled(userId)).isTrue();
+    }
+
+    @Test
+    public void assertIsUserSwitcherEnabledOnMaxSupportedUsers()  throws Exception {
+        int userId = ActivityManager.getCurrentUser();
+        setMaxSupportedUsers(1);
+
+        assertThat(UserManager.supportsMultipleUsers()).isFalse();
+        assertThat(mUserManagerService.isUserSwitcherEnabled(userId)).isFalse();
+
+        setMaxSupportedUsers(8);
+
+        assertThat(UserManager.supportsMultipleUsers()).isTrue();
+        assertThat(mUserManagerService.isUserSwitcherEnabled(userId)).isTrue();
+    }
+
+
+    @Test
+    public void assertIsUserSwitcherEnabledOnShowMultiuserUI()  throws Exception {
+        int userId = ActivityManager.getCurrentUser();
+        setShowMultiuserUI(false);
+
+        assertThat(UserManager.supportsMultipleUsers()).isFalse();
+        assertThat(mUserManagerService.isUserSwitcherEnabled(userId)).isFalse();
+
+        setShowMultiuserUI(true);
+
+        assertThat(UserManager.supportsMultipleUsers()).isTrue();
+        assertThat(mUserManagerService.isUserSwitcherEnabled(userId)).isTrue();
+    }
+
+    @Test
+    public void assertIsUserSwitcherEnabledOnUserRestrictions() throws Exception {
+        int userId = ActivityManager.getCurrentUser();
+        resetUserSwitcherEnabled();
+
+        mUserManagerService.setUserRestriction(DISALLOW_USER_SWITCH, true, userId);
+        assertThat(mUserManagerService.isUserSwitcherEnabled(userId)).isFalse();
+
+        mUserManagerService.setUserRestriction(DISALLOW_USER_SWITCH, false, userId);
+        assertThat(mUserManagerService.isUserSwitcherEnabled(userId)).isTrue();
+    }
+
+    @Test
+    public void assertIsUserSwitcherEnabledOnDemoMode()  throws Exception {
+        int userId = ActivityManager.getCurrentUser();
+        resetUserSwitcherEnabled();
+
+        setDeviceDemoMode(true);
+        assertThat(mUserManagerService.isUserSwitcherEnabled(userId)).isFalse();
+
+        setDeviceDemoMode(false);
+        assertThat(mUserManagerService.isUserSwitcherEnabled(userId)).isTrue();
+    }
+
+    private void resetUserSwitcherEnabled() throws Exception {
+        int userId = ActivityManager.getCurrentUser();
+        setUserSwitch(true);
+        setShowMultiuserUI(true);
+        setDeviceDemoMode(false);
+        setMaxSupportedUsers(8);
+        mUserManagerService.setUserRestriction(DISALLOW_USER_SWITCH, false, userId);
+    }
+
+    private void setUserSwitch(boolean enabled) {
+        android.provider.Settings.Global.putInt(mContext.getContentResolver(),
+                android.provider.Settings.Global.USER_SWITCHER_ENABLED, enabled ? 1 : 0);
+    }
+
+    private void setDeviceDemoMode(boolean enabled) {
+        android.provider.Settings.Global.putInt(mContext.getContentResolver(),
+                android.provider.Settings.Global.DEVICE_DEMO_MODE, enabled ? 1 : 0);
+    }
+
+
     private static String runShellCommand(String cmd) throws Exception {
         return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
                 .executeShellCommand(cmd);
     }
+
+    private static String setSystemProperty(String name, String value) throws Exception {
+        final String oldValue = runShellCommand("getprop " + name);
+        assertThat(runShellCommand("setprop " + name + " " + value))
+                .isEqualTo("");
+        return oldValue;
+    }
+
+    private static void setMaxSupportedUsers(int max) throws Exception {
+        setSystemProperty("fw.max_users", String.valueOf(max));
+    }
+
+    public static void setShowMultiuserUI(boolean show) throws Exception {
+        setSystemProperty("fw.show_multiuserui", String.valueOf(show));
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
new file mode 100644
index 0000000..13a7a3e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.pm.UserProperties;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+import android.util.TypedXmlPullParser;
+import android.util.TypedXmlSerializer;
+import android.util.Xml;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Supplier;
+
+/**
+ * Tests for UserManager's {@link UserProperties}.
+ *
+ * Additional test coverage (that actually exercises the functionality) can be found in
+ * {@link UserManagerTest} and
+ * {@link UserManagerServiceUserTypeTest} (for {@link UserProperties#updateFromXml}).
+ *
+ * <p>Run with: atest UserManagerServiceUserPropertiesTest
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class UserManagerServiceUserPropertiesTest {
+
+    /** Test that UserProperties can properly read the xml information that it writes. */
+    @Test
+    public void testWriteReadXml() throws Exception {
+        final UserProperties defaultProps = new UserProperties.Builder()
+                .setShowInLauncher(21)
+                .setStartWithParent(false)
+                .build();
+        final UserProperties actualProps = new UserProperties(defaultProps);
+        actualProps.setShowInLauncher(14);
+
+        // Write the properties to xml.
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        final TypedXmlSerializer out = Xml.newFastSerializer();
+        out.setOutput(baos, StandardCharsets.UTF_8.name());
+        out.startDocument(null, true);
+        out.startTag(null, "testTag");
+        actualProps.writeToXml(out);
+        out.endTag(null, "testTag");
+        out.endDocument();
+
+        // Now read those properties from xml.
+        final ByteArrayInputStream input = new ByteArrayInputStream(baos.toByteArray());
+        final TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(input, StandardCharsets.UTF_8.name());
+        parser.nextTag();
+        final UserProperties readProps = new UserProperties(parser, defaultProps);
+
+        assertUserPropertiesEquals(actualProps, readProps);
+    }
+
+    /** Tests parcelling an object in which all properties are present. */
+    @Test
+    public void testParcelUnparcel() throws Exception {
+        final UserProperties originalProps = new UserProperties.Builder()
+                .setShowInLauncher(2145)
+                .build();
+        final UserProperties readProps = parcelThenUnparcel(originalProps);
+        assertUserPropertiesEquals(originalProps, readProps);
+    }
+
+    /** Tests copying a UserProperties object varying permissions. */
+    @Test
+    public void testCopyLacksPermissions() throws Exception {
+        final UserProperties defaultProps = new UserProperties.Builder()
+                .setShowInLauncher(2145)
+                .setStartWithParent(true)
+                .build();
+        final UserProperties orig = new UserProperties(defaultProps);
+        orig.setShowInLauncher(2841);
+        orig.setStartWithParent(false);
+
+        // Test every permission level. (Currently, it's linear so it's easy.)
+        for (int permLevel = 0; permLevel < 4; permLevel++) {
+            final boolean exposeAll = permLevel >= 3;
+            final boolean hasManage = permLevel >= 2;
+            final boolean hasQuery = permLevel >= 1;
+
+            // Make a possibly-not-full-permission (i.e. partial) copy and check that it is correct.
+            final UserProperties copy = new UserProperties(orig, exposeAll, hasManage, hasQuery);
+            verifyTestCopyLacksPermissions(orig, copy, exposeAll, hasManage, hasQuery);
+            if (permLevel < 1) {
+                // PropertiesPresent should definitely be different since not all items were copied.
+                assertThat(orig.getPropertiesPresent()).isNotEqualTo(copy.getPropertiesPresent());
+            }
+
+            // Now, just like in the SystemServer, parcel/unparcel the copy and make sure that the
+            // unparcelled version behaves just like the partial copy did.
+            final UserProperties readProps = parcelThenUnparcel(copy);
+            verifyTestCopyLacksPermissions(orig, readProps, exposeAll, hasManage, hasQuery);
+        }
+    }
+
+    /**
+     * Verifies that the copy of orig has the expected properties
+     * for the test {@link #testCopyLacksPermissions}.
+     */
+    private void verifyTestCopyLacksPermissions(
+            UserProperties orig,
+            UserProperties copy,
+            boolean exposeAll,
+            boolean hasManagePermission,
+            boolean hasQueryPermission) {
+
+        // Items requiring exposeAll.
+        assertEqualGetterOrThrows(orig::getStartWithParent, copy::getStartWithParent, exposeAll);
+
+        // Items requiring hasManagePermission - put them here using hasManagePermission.
+        // Items requiring hasQueryPermission - put them here using hasQueryPermission.
+
+        // Items with no permission requirements.
+        assertEqualGetterOrThrows(orig::getShowInLauncher, copy::getShowInLauncher, true);
+    }
+
+    /**
+     * If hasPerm, then asserts that value of actualGetter equals value of expectedGetter.
+     * If !hasPerm, then asserts that actualGetter throws a SecurityException.
+     */
+    @SuppressWarnings("ReturnValueIgnored")
+    private void assertEqualGetterOrThrows(
+            Supplier expectedGetter,
+            Supplier actualGetter,
+            boolean hasPerm) {
+        if (hasPerm) {
+            assertThat(expectedGetter.get()).isEqualTo(actualGetter.get());
+        } else {
+            assertThrows(SecurityException.class, actualGetter::get);
+        }
+    }
+
+    private UserProperties parcelThenUnparcel(UserProperties originalProps) {
+        final Parcel out = Parcel.obtain();
+        originalProps.writeToParcel(out, 0);
+        final byte[] data = out.marshall();
+        out.recycle();
+
+        final Parcel in = Parcel.obtain();
+        in.unmarshall(data, 0, data.length);
+        in.setDataPosition(0);
+        final UserProperties readProps = UserProperties.CREATOR.createFromParcel(in);
+        in.recycle();
+
+        return readProps;
+    }
+
+    /** Checks that two UserProperties get the same values. */
+    private void assertUserPropertiesEquals(UserProperties expected, UserProperties actual) {
+        assertThat(expected.getPropertiesPresent()).isEqualTo(actual.getPropertiesPresent());
+        assertThat(expected.getShowInLauncher()).isEqualTo(actual.getShowInLauncher());
+        assertThat(expected.getStartWithParent()).isEqualTo(actual.getStartWithParent());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
index 971b036..5f48004 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -35,6 +35,7 @@
 import static org.testng.Assert.assertThrows;
 
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.os.Bundle;
@@ -81,6 +82,8 @@
                 DefaultCrossProfileIntentFilter.Direction.TO_PARENT,
                 /* flags= */0,
                 /* letsPersonalDataIntoProfile= */false).build());
+        final UserProperties.Builder userProps = new UserProperties.Builder()
+                .setShowInLauncher(17);
         final UserTypeDetails type = new UserTypeDetails.Builder()
                 .setName("a.name")
                 .setEnabled(1)
@@ -98,6 +101,7 @@
                 .setDefaultSystemSettings(systemSettings)
                 .setDefaultSecureSettings(secureSettings)
                 .setDefaultCrossProfileIntentFilters(filters)
+                .setDefaultUserProperties(userProps)
                 .createUserTypeDetails();
 
         assertEquals("a.name", type.getName());
@@ -135,6 +139,8 @@
             assertEquals(filters.get(i), type.getDefaultCrossProfileIntentFilters().get(i));
         }
 
+        assertEquals(17, type.getDefaultUserPropertiesReference().getShowInLauncher());
+
         assertEquals(23, type.getBadgeLabel(0));
         assertEquals(24, type.getBadgeLabel(1));
         assertEquals(25, type.getBadgeLabel(2));
@@ -173,6 +179,11 @@
         assertTrue(type.getDefaultSecureSettings().isEmpty());
         assertTrue(type.getDefaultCrossProfileIntentFilters().isEmpty());
 
+        final UserProperties props = type.getDefaultUserPropertiesReference();
+        assertNotNull(props);
+        assertFalse(props.getStartWithParent());
+        assertEquals(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT, props.getShowInLauncher());
+
         assertFalse(type.hasBadge());
     }
 
@@ -250,19 +261,24 @@
 
         // Mock some "AOSP defaults".
         final Bundle restrictions = makeRestrictionsBundle("no_config_vpn", "no_config_tethering");
+        final UserProperties.Builder props = new UserProperties.Builder()
+                .setShowInLauncher(19)
+                .setStartWithParent(true);
         final ArrayMap<String, UserTypeDetails.Builder> builders = new ArrayMap<>();
         builders.put(userTypeAosp1, new UserTypeDetails.Builder()
                 .setName(userTypeAosp1)
                 .setBaseType(FLAG_PROFILE)
                 .setMaxAllowedPerParent(31)
-                .setDefaultRestrictions(restrictions));
+                .setDefaultRestrictions(restrictions)
+                .setDefaultUserProperties(props));
         builders.put(userTypeAosp2, new UserTypeDetails.Builder()
                 .setName(userTypeAosp1)
                 .setBaseType(FLAG_PROFILE)
                 .setMaxAllowedPerParent(32)
                 .setIconBadge(401)
                 .setBadgeColors(402, 403, 404)
-                .setDefaultRestrictions(restrictions));
+                .setDefaultRestrictions(restrictions)
+                .setDefaultUserProperties(props));
 
         final XmlResourceParser parser = mResources.getXml(R.xml.usertypes_test_profile);
         UserTypeFactory.customizeBuilders(builders, parser);
@@ -272,6 +288,8 @@
         assertEquals(31, aospType.getMaxAllowedPerParent());
         assertEquals(Resources.ID_NULL, aospType.getIconBadge());
         assertTrue(UserRestrictionsUtils.areEqual(restrictions, aospType.getDefaultRestrictions()));
+        assertEquals(19, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
+        assertEquals(true, aospType.getDefaultUserPropertiesReference().getStartWithParent());
 
         // userTypeAosp2 should be modified.
         aospType = builders.get(userTypeAosp2).createUserTypeDetails();
@@ -300,6 +318,8 @@
         assertTrue(UserRestrictionsUtils.areEqual(
                 makeRestrictionsBundle("no_remove_user", "no_bluetooth"),
                 aospType.getDefaultRestrictions()));
+        assertEquals(2020, aospType.getDefaultUserPropertiesReference().getShowInLauncher());
+        assertEquals(false, aospType.getDefaultUserPropertiesReference().getStartWithParent());
 
         // userTypeOem1 should be created.
         UserTypeDetails.Builder customType = builders.get(userTypeOem1);
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 5d48501..f567e80 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -31,6 +31,7 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
 import android.content.res.Resources;
 import android.os.Bundle;
 import android.os.UserHandle;
@@ -564,6 +565,33 @@
         assertThat(userManagerForUser.isProfile()).isEqualTo(userTypeDetails.isProfile());
     }
 
+    /** Test that UserManager returns the correct UserProperties for a new managed profile. */
+    @MediumTest
+    @Test
+    public void testUserProperties() throws Exception {
+        assumeManagedUsersSupported();
+
+        // Get the default properties for a user type.
+        final UserTypeDetails userTypeDetails =
+                UserTypeFactory.getUserTypes().get(UserManager.USER_TYPE_PROFILE_MANAGED);
+        assertWithMessage("No %s type on device", UserManager.USER_TYPE_PROFILE_MANAGED)
+                .that(userTypeDetails).isNotNull();
+        final UserProperties typeProps = userTypeDetails.getDefaultUserPropertiesReference();
+
+        // Create an actual user (of this user type) and get its properties.
+        final int primaryUserId = mUserManager.getPrimaryUser().id;
+        final UserInfo userInfo = createProfileForUser("Managed",
+                UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId);
+        assertThat(userInfo).isNotNull();
+        final int userId = userInfo.id;
+        final UserProperties userProps = mUserManager.getUserProperties(UserHandle.of(userId));
+
+        // Check that this new user has the expected properties (relative to the defaults)
+        // provided that the test caller has the necessary permissions.
+        assertThat(userProps.getShowInLauncher()).isEqualTo(typeProps.getShowInLauncher());
+        assertThrows(SecurityException.class, userProps::getStartWithParent);
+    }
+
     // Make sure only max managed profiles can be created
     @MediumTest
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
index 36e988f..3848bab 100644
--- a/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/ThermalManagerServiceTest.java
@@ -381,6 +381,7 @@
         assertEquals(0, Arrays.asList(mService.mService.getCurrentTemperaturesWithType(
                         Temperature.TYPE_SKIN)).size());
         assertEquals(Temperature.THROTTLING_NONE, mService.mService.getCurrentThermalStatus());
+        assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(0)));
     }
 
     @Test
@@ -397,6 +398,14 @@
     }
 
     @Test
+    public void testGetThermalHeadroomInputRange() throws RemoteException {
+        assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(
+                ThermalManagerService.MIN_FORECAST_SEC - 1)));
+        assertTrue(Float.isNaN(mService.mService.getThermalHeadroom(
+                ThermalManagerService.MAX_FORECAST_SEC + 1)));
+    }
+
+    @Test
     public void testTemperatureWatcherUpdateSevereThresholds() throws RemoteException {
         ThermalManagerService.TemperatureWatcher watcher = mService.mTemperatureWatcher;
         watcher.mSevereThresholds.erase();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
index 911fb6a..08c2c6e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java
@@ -1301,6 +1301,21 @@
     }
 
     @Test
+    public void testA11yCrossUserEventNotSent() throws Exception {
+        final Notification n = new Builder(getContext(), "test")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+        int userId = mUser.getIdentifier() + 1;
+        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 0, mTag, mUid,
+                mPid, n, UserHandle.of(userId), null, System.currentTimeMillis());
+        NotificationRecord r = new NotificationRecord(getContext(), sbn,
+                new NotificationChannel("test", "test", IMPORTANCE_HIGH));
+
+        mService.buzzBeepBlinkLocked(r);
+
+        verify(mAccessibilityService, never()).sendAccessibilityEvent(any(), anyInt());
+    }
+
+    @Test
     public void testLightsScreenOn() {
         mService.mScreenOn = true;
         NotificationRecord r = getLightsNotification();
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 44ca9f4..ec48e23 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -150,6 +150,7 @@
 import android.graphics.Color;
 import android.graphics.drawable.Icon;
 import android.media.AudioManager;
+import android.media.IRingtonePlayer;
 import android.media.session.MediaSession;
 import android.net.Uri;
 import android.os.Binder;
@@ -4238,6 +4239,59 @@
     }
 
     @Test
+    public void testSubstituteAppName_hasPermission() throws RemoteException {
+        String subName = "Substitute Name";
+        when(mPackageManager.checkPermission(
+                eq(android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME), any(), anyInt()))
+                .thenReturn(PERMISSION_GRANTED);
+        Bundle extras = new Bundle();
+        extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName);
+        Notification.Builder nb = new Notification.Builder(mContext,
+                mTestNotificationChannel.getId())
+                .addExtras(extras);
+        StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+                "testSubstituteAppNamePermission", mUid, 0,
+                nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+        NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, sbn.getTag(),
+                nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+        waitForIdle();
+        NotificationRecord posted = mService.findNotificationLocked(
+                PKG, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getUserId());
+
+        assertTrue(posted.getNotification().extras
+                .containsKey(Notification.EXTRA_SUBSTITUTE_APP_NAME));
+        assertEquals(posted.getNotification().extras
+                .getString(Notification.EXTRA_SUBSTITUTE_APP_NAME), subName);
+    }
+
+    @Test
+    public void testSubstituteAppName_noPermission() throws RemoteException {
+        when(mPackageManager.checkPermission(
+                eq(android.Manifest.permission.SUBSTITUTE_NOTIFICATION_APP_NAME), any(), anyInt()))
+                .thenReturn(PERMISSION_DENIED);
+        Bundle extras = new Bundle();
+        extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, "Substitute Name");
+        Notification.Builder nb = new Notification.Builder(mContext,
+                mTestNotificationChannel.getId())
+                .addExtras(extras);
+        StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1,
+                "testSubstituteAppNamePermission", mUid, 0,
+                nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+        NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel);
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, sbn.getTag(),
+                nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+        waitForIdle();
+        NotificationRecord posted = mService.findNotificationLocked(
+                PKG, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getUserId());
+
+        assertFalse(posted.getNotification().extras
+                .containsKey(Notification.EXTRA_SUBSTITUTE_APP_NAME));
+    }
+
+    @Test
     public void testGetNotificationCountLocked() {
         String sampleTagToExclude = null;
         int sampleIdToExclude = 0;
@@ -7738,6 +7792,34 @@
     }
 
     @Test
+    public void testOnBubbleMetadataChangedToSuppressNotification_soundStopped()
+            throws RemoteException {
+        IRingtonePlayer mockPlayer = mock(IRingtonePlayer.class);
+        when(mAudioManager.getRingtonePlayer()).thenReturn(mockPlayer);
+        // Set up volume to be above 0 for the sound to actually play
+        when(mAudioManager.getStreamVolume(anyInt())).thenReturn(10);
+
+        setUpPrefsForBubbles(PKG, mUid,
+                true /* global */,
+                BUBBLE_PREFERENCE_ALL /* app */,
+                true /* channel */);
+
+        // Post a bubble notification
+        NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "tag");
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
+                nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+        waitForIdle();
+
+        // Test: suppress notification via bubble metadata update
+        mService.mNotificationDelegate.onBubbleMetadataFlagChanged(nr.getKey(),
+                Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
+        waitForIdle();
+
+        // Check audio is stopped
+        verify(mockPlayer).stopAsync();
+    }
+
+    @Test
     public void testGrantInlineReplyUriPermission_recordExists() throws Exception {
         int userId = UserManager.isHeadlessSystemUserMode()
                 ? UserHandle.getUserId(UID_HEADLESS)
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 598a22b..d62ac99 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -93,6 +93,7 @@
 import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Parcel;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -2447,6 +2448,35 @@
     }
 
     @Test
+    public void testGetNotificationChannelGroup() throws Exception {
+        NotificationChannelGroup notDeleted = new NotificationChannelGroup("not", "deleted");
+        NotificationChannel base =
+                new NotificationChannel("not deleted", "belongs to notDeleted", IMPORTANCE_DEFAULT);
+        base.setGroup("not");
+        NotificationChannel convo =
+                new NotificationChannel("convo", "belongs to notDeleted", IMPORTANCE_DEFAULT);
+        convo.setGroup("not");
+        convo.setConversationId("not deleted", "banana");
+
+        mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, notDeleted, true);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, base, true, false);
+        mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, convo, true, false);
+        mHelper.createNotificationChannelGroup(PKG_N_MR1, UID_N_MR1, notDeleted, true);
+
+        NotificationChannelGroup g
+                = mHelper.getNotificationChannelGroup(notDeleted.getId(), PKG_N_MR1, UID_N_MR1);
+        Parcel parcel = Parcel.obtain();
+        g.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        NotificationChannelGroup g2
+                = mHelper.getNotificationChannelGroup(notDeleted.getId(), PKG_N_MR1, UID_N_MR1);
+        Parcel parcel2 = Parcel.obtain();
+        g2.writeToParcel(parcel2, 0);
+        parcel2.setDataPosition(0);
+    }
+
+    @Test
     public void testOnUserRemoved() throws Exception {
         int[] user0Uids = {98, 235, 16, 3782};
         int[] user1Uids = new int[user0Uids.length];
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index 7415460..f61effa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -474,7 +474,7 @@
     }
 
     @Test
-    public void testActivityRecordReparentToTaskFragment() {
+    public void testActivityRecordReparentedToTaskFragment() {
         final ActivityRecord activity = createActivityRecord(mDc);
         final SurfaceControl activityLeash = mock(SurfaceControl.class);
         doNothing().when(activity).setDropInputMode(anyInt());
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 7a73f08..7244d94 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1916,10 +1916,10 @@
         testPlayer.start();
         assertNotEquals(origRot, dc.getConfiguration().windowConfiguration.getRotation());
         assertNotNull(testPlayer.mLastReady);
-        assertEquals(dc, DisplayRotation.getDisplayFromTransition(testPlayer.mLastTransit));
         WindowContainerToken dcToken = dc.mRemoteToken.toWindowContainerToken();
         assertNotEquals(testPlayer.mLastReady.getChange(dcToken).getEndRotation(),
                 testPlayer.mLastReady.getChange(dcToken).getStartRotation());
+        assertTrue(testPlayer.mLastTransit.applyDisplayChangeIfNeeded());
         testPlayer.finish();
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
index e502f2f..d400a4c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
@@ -56,6 +56,7 @@
     private boolean mHasWallpaperBackground = false;
     private int mBlurRadius = 0;
     private float mDarkScrimAlpha = 0.5f;
+    private SurfaceControl mParentSurface = mock(SurfaceControl.class);
 
     @Before
     public void setUp() throws Exception {
@@ -63,7 +64,8 @@
         mLetterbox = new Letterbox(mSurfaces, StubTransaction::new,
                 () -> mAreCornersRounded, () -> Color.valueOf(mColor),
                 () -> mHasWallpaperBackground, () -> mBlurRadius, () -> mDarkScrimAlpha,
-                /* doubleTapCallbackX= */ x -> {}, /* doubleTapCallbackY= */ y -> {});
+                /* doubleTapCallbackX= */ x -> {}, /* doubleTapCallbackY= */ y -> {},
+                () -> mParentSurface);
         mTransaction = spy(StubTransaction.class);
     }
 
@@ -205,6 +207,22 @@
     }
 
     @Test
+    public void testNeedsApplySurfaceChanges_setParentSurface() {
+        mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000));
+        mLetterbox.applySurfaceChanges(mTransaction);
+
+        verify(mTransaction).reparent(mSurfaces.top, mParentSurface);
+        assertFalse(mLetterbox.needsApplySurfaceChanges());
+
+        mParentSurface = mock(SurfaceControl.class);
+
+        assertTrue(mLetterbox.needsApplySurfaceChanges());
+
+        mLetterbox.applySurfaceChanges(mTransaction);
+        verify(mTransaction).reparent(mSurfaces.top, mParentSurface);
+    }
+
+    @Test
     public void testApplySurfaceChanges_cornersNotRounded_surfaceFullWindowSurfaceNotCreated() {
         mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000));
         mLetterbox.applySurfaceChanges(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 8b3cff8..da72030 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -158,7 +158,7 @@
         mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentAppeared(any());
+        verify(mOrganizer, never()).onTaskFragmentAppeared(any(), any());
 
         // Send callback when the TaskFragment is attached.
         setupMockParent(mTaskFragment, mTask);
@@ -166,7 +166,7 @@
         mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentAppeared(any());
+        verify(mOrganizer).onTaskFragmentAppeared(any(), any());
     }
 
     @Test
@@ -179,13 +179,13 @@
                 mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
 
         // Call onTaskFragmentAppeared first.
         mController.onTaskFragmentAppeared(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentAppeared(any());
+        verify(mOrganizer).onTaskFragmentAppeared(any(), any());
 
         // No callback if the info is not changed.
         doReturn(true).when(mTaskFragmentInfo).equalsForTaskFragmentOrganizer(any());
@@ -195,7 +195,7 @@
                 mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
 
         // Trigger callback if the info is changed.
         doReturn(false).when(mTaskFragmentInfo).equalsForTaskFragmentOrganizer(any());
@@ -204,7 +204,7 @@
                 mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentInfoChanged(mTaskFragmentInfo);
+        verify(mOrganizer).onTaskFragmentInfoChanged(any(), eq(mTaskFragmentInfo));
     }
 
     @Test
@@ -215,7 +215,7 @@
         mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentVanished(any());
+        verify(mOrganizer).onTaskFragmentVanished(any(), any());
     }
 
     @Test
@@ -228,10 +228,10 @@
         mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentAppeared(any());
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
-        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(anyInt(), any());
-        verify(mOrganizer).onTaskFragmentVanished(mTaskFragmentInfo);
+        verify(mOrganizer, never()).onTaskFragmentAppeared(any(), any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
+        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(any(), anyInt(), any());
+        verify(mOrganizer).onTaskFragmentVanished(any(), eq(mTaskFragmentInfo));
 
         // Not trigger onTaskFragmentInfoChanged.
         // Call onTaskFragmentAppeared before calling onTaskFragmentInfoChanged.
@@ -244,10 +244,10 @@
         mController.onTaskFragmentVanished(mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentAppeared(any());
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
-        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(anyInt(), any());
-        verify(mOrganizer).onTaskFragmentVanished(mTaskFragmentInfo);
+        verify(mOrganizer, never()).onTaskFragmentAppeared(any(), any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
+        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(any(), anyInt(), any());
+        verify(mOrganizer).onTaskFragmentVanished(any(), eq(mTaskFragmentInfo));
     }
 
     @Test
@@ -260,7 +260,7 @@
                 mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentParentInfoChanged(eq(mTask.mTaskId), any());
+        verify(mOrganizer).onTaskFragmentParentInfoChanged(any(), eq(mTask.mTaskId), any());
 
         // No extra callback if the info is not changed.
         clearInvocations(mOrganizer);
@@ -269,7 +269,7 @@
                 mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(anyInt(), any());
+        verify(mOrganizer, never()).onTaskFragmentParentInfoChanged(any(), anyInt(), any());
 
         // Trigger callback if the size is changed.
         mTask.getConfiguration().smallestScreenWidthDp = 100;
@@ -277,7 +277,7 @@
                 mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentParentInfoChanged(eq(mTask.mTaskId), any());
+        verify(mOrganizer).onTaskFragmentParentInfoChanged(any(), eq(mTask.mTaskId), any());
 
         // Trigger callback if the windowing mode is changed.
         clearInvocations(mOrganizer);
@@ -286,7 +286,7 @@
                 mTaskFragment.getTaskFragmentOrganizer(), mTaskFragment);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentParentInfoChanged(eq(mTask.mTaskId), any());
+        verify(mOrganizer).onTaskFragmentParentInfoChanged(any(), eq(mTask.mTaskId), any());
     }
 
     @Test
@@ -298,11 +298,12 @@
                 mErrorToken, null /* taskFragment */, -1 /* opType */, exception);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), eq(null), eq(-1), eq(exception));
+        verify(mOrganizer).onTaskFragmentError(any(), eq(mErrorToken), eq(null), eq(-1),
+                eq(exception));
     }
 
     @Test
-    public void testOnActivityReparentToTask_activityInOrganizerProcess_useActivityToken() {
+    public void testOnActivityReparentedToTask_activityInOrganizerProcess_useActivityToken() {
         // Make sure the activity pid/uid is the same as the organizer caller.
         final int pid = Binder.getCallingPid();
         final int uid = Binder.getCallingUid();
@@ -314,17 +315,18 @@
         task.effectiveUid = uid;
 
         // No need to notify organizer if it is not embedded.
-        mController.onActivityReparentToTask(activity);
+        mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer, never()).onActivityReparentToTask(anyInt(), any(), any());
+        verify(mOrganizer, never()).onActivityReparentedToTask(any(), anyInt(), any(), any());
 
         // Notify organizer if it was embedded before entered Pip.
         activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer;
-        mController.onActivityReparentToTask(activity);
+        mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
-        verify(mOrganizer).onActivityReparentToTask(task.mTaskId, activity.intent, activity.token);
+        verify(mOrganizer).onActivityReparentedToTask(any(), eq(task.mTaskId), eq(activity.intent),
+                eq(activity.token));
 
         // Notify organizer if there is any embedded in the Task.
         final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
@@ -335,15 +337,16 @@
                 DEFAULT_TASK_FRAGMENT_ORGANIZER_PROCESS_NAME);
         activity.reparent(taskFragment, POSITION_TOP);
         activity.mLastTaskFragmentOrganizerBeforePip = null;
-        mController.onActivityReparentToTask(activity);
+        mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
         verify(mOrganizer, times(2))
-                .onActivityReparentToTask(task.mTaskId, activity.intent, activity.token);
+                .onActivityReparentedToTask(any(), eq(task.mTaskId), eq(activity.intent),
+                        eq(activity.token));
     }
 
     @Test
-    public void testOnActivityReparentToTask_activityNotInOrganizerProcess_useTemporaryToken() {
+    public void testOnActivityReparentedToTask_activityNotInOrganizerProcess_useTemporaryToken() {
         final int pid = Binder.getCallingPid();
         final int uid = Binder.getCallingUid();
         mTaskFragment.setTaskFragmentOrganizer(mOrganizer.getOrganizerToken(), uid,
@@ -364,11 +367,11 @@
         // Notify organizer if it was embedded before entered Pip.
         // Create a temporary token since the activity doesn't belong to the same process.
         activity.mLastTaskFragmentOrganizerBeforePip = mIOrganizer;
-        mController.onActivityReparentToTask(activity);
+        mController.onActivityReparentedToTask(activity);
         mController.dispatchPendingEvents();
 
         // Allow organizer to reparent activity in other process using the temporary token.
-        verify(mOrganizer).onActivityReparentToTask(eq(task.mTaskId), eq(activity.intent),
+        verify(mOrganizer).onActivityReparentedToTask(any(), eq(task.mTaskId), eq(activity.intent),
                 token.capture());
         final IBinder temporaryToken = token.getValue();
         assertNotEquals(activity.token, temporaryToken);
@@ -798,7 +801,7 @@
         mController.dispatchPendingEvents();
 
         // Verifies that event was not sent
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
     }
 
     @Test
@@ -824,7 +827,7 @@
         mController.dispatchPendingEvents();
 
         // Verifies that event was not sent
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
 
         // Mock the task becomes visible, and activity resumed
         doReturn(true).when(task).shouldBeVisible(any());
@@ -832,7 +835,7 @@
 
         // Verifies that event is sent.
         mController.dispatchPendingEvents();
-        verify(mOrganizer).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
     }
 
     /**
@@ -866,7 +869,7 @@
         reset(mOrganizer);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-        verify(mOrganizer).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
     }
 
     /**
@@ -884,8 +887,8 @@
                 .createActivityCount(1)
                 .build();
         final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity();
-        // Add another activity in the Task so that it always contains a non-finishing activitiy.
-        final ActivityRecord nonEmbeddedActivity = createActivityRecord(task);
+        // Add another activity in the Task so that it always contains a non-finishing activity.
+        createActivityRecord(task);
         assertTrue(task.shouldBeVisible(null));
 
         // Dispatch pending info changed event from creating the activity
@@ -893,21 +896,21 @@
         taskFragment.mTaskFragmentAppearedSent = true;
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-        verify(mOrganizer).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
 
         // Verify the info changed callback is not called when the task is invisible
         reset(mOrganizer);
         doReturn(false).when(task).shouldBeVisible(any());
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer, never()).onTaskFragmentInfoChanged(any(), any());
 
         // Finish the embedded activity, and verify the info changed callback is called because the
         // TaskFragment is becoming empty.
         embeddedActivity.finishing = true;
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-        verify(mOrganizer).onTaskFragmentInfoChanged(any());
+        verify(mOrganizer).onTaskFragmentInfoChanged(any(), any());
     }
 
     /**
@@ -1017,7 +1020,7 @@
         // The pending event will be dispatched on the handler (from requestTraversal).
         waitHandlerIdle(mWm.mAnimationHandler);
 
-        verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), any(),
+        verify(mOrganizer).onTaskFragmentError(any(), eq(mErrorToken), any(),
                 eq(HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT),
                 any(SecurityException.class));
     }
@@ -1056,7 +1059,7 @@
         // The pending event will be dispatched on the handler (from requestTraversal).
         waitHandlerIdle(mWm.mAnimationHandler);
 
-        verify(mOrganizer).onTaskFragmentError(eq(mErrorToken), any(),
+        verify(mOrganizer).onTaskFragmentError(any(), eq(mErrorToken), any(),
                 eq(HIERARCHY_OP_TYPE_REPARENT_CHILDREN), any(SecurityException.class));
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 88eadfc..83f1789 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -134,6 +134,23 @@
     }
 
     @Test
+    public void testStartChangeTransition_doNotFreezeWhenOnlyMoved() {
+        final Rect startBounds = new Rect(0, 0, 1000, 1000);
+        final Rect endBounds = new Rect(startBounds);
+        endBounds.offset(500, 0);
+        mTaskFragment.setBounds(startBounds);
+        doReturn(true).when(mTaskFragment).isVisible();
+        doReturn(true).when(mTaskFragment).isVisibleRequested();
+
+        clearInvocations(mTransaction);
+        mTaskFragment.setBounds(endBounds);
+
+        // No change transition, but update the organized surface position.
+        verify(mTaskFragment, never()).initializeChangeTransition(any(), any());
+        verify(mTransaction).setPosition(mLeash, endBounds.left, endBounds.top);
+    }
+
+    @Test
     public void testNotOkToAnimate_doNotStartChangeTransition() {
         mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer);
         final Rect startBounds = new Rect(0, 0, 1000, 1000);
@@ -323,7 +340,7 @@
         activity.reparent(task, POSITION_TOP);
 
         // Notify the organizer about the reparent.
-        verify(mAtm.mTaskFragmentOrganizerController).onActivityReparentToTask(activity);
+        verify(mAtm.mTaskFragmentOrganizerController).onActivityReparentedToTask(activity);
         assertNull(activity.mLastTaskFragmentOrganizerBeforePip);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index 851be9d..d2cb7ba 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -245,7 +245,7 @@
     }
 
     @Override
-    public void userActivity() {
+    public void userActivity(int displayGroupId, int event) {
     }
 
     @Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 5a2d456..85ac7bd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -36,8 +36,10 @@
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 import static android.window.TransitionInfo.isIndependent;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -1082,6 +1084,39 @@
         assertTrue((info.getChanges().get(1).getFlags() & FLAG_IS_EMBEDDED) != 0);
     }
 
+    @Test
+    public void testIncludeEmbeddedActivityReparent() {
+        final Transition transition = createTestTransition(TRANSIT_OPEN);
+        final Task task = createTask(mDisplayContent);
+        task.setBounds(new Rect(0, 0, 2000, 1000));
+        final ActivityRecord activity = createActivityRecord(task);
+        activity.mVisibleRequested = true;
+        // Skip manipulate the SurfaceControl.
+        doNothing().when(activity).setDropInputMode(anyInt());
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        mAtm.mTaskFragmentOrganizerController.registerOrganizer(
+                ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()));
+        final TaskFragment embeddedTf = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
+                .setOrganizer(organizer)
+                .build();
+        // TaskFragment with different bounds from Task.
+        embeddedTf.setBounds(new Rect(0, 0, 1000, 1000));
+
+        // Start states.
+        transition.collect(activity);
+        transition.collectExistenceChange(embeddedTf);
+
+        // End states.
+        activity.reparent(embeddedTf, POSITION_TOP);
+
+        // Verify that both activity and TaskFragment are included.
+        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+                transition.mParticipants, transition.mChanges);
+        assertTrue(targets.contains(embeddedTf));
+        assertTrue(targets.contains(activity));
+    }
+
     private static void makeTaskOrganized(Task... tasks) {
         final ITaskOrganizer organizer = mock(ITaskOrganizer.class);
         for (Task t : tasks) {
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index f28408e1a..4fb6587 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -139,24 +139,19 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static final Uri CONTENT_URI = SimInfo.CONTENT_URI;
 
-    /** @hide */
-    public static final String CACHE_KEY_DEFAULT_SUB_ID_PROPERTY =
+    private static final String CACHE_KEY_DEFAULT_SUB_ID_PROPERTY =
             "cache_key.telephony.get_default_sub_id";
 
-    /** @hide */
-    public static final String CACHE_KEY_DEFAULT_DATA_SUB_ID_PROPERTY =
+    private static final String CACHE_KEY_DEFAULT_DATA_SUB_ID_PROPERTY =
             "cache_key.telephony.get_default_data_sub_id";
 
-    /** @hide */
-    public static final String CACHE_KEY_DEFAULT_SMS_SUB_ID_PROPERTY =
+    private static final String CACHE_KEY_DEFAULT_SMS_SUB_ID_PROPERTY =
             "cache_key.telephony.get_default_sms_sub_id";
 
-    /** @hide */
-    public static final String CACHE_KEY_ACTIVE_DATA_SUB_ID_PROPERTY =
+    private static final String CACHE_KEY_ACTIVE_DATA_SUB_ID_PROPERTY =
             "cache_key.telephony.get_active_data_sub_id";
 
-    /** @hide */
-    public static final String CACHE_KEY_SLOT_INDEX_PROPERTY =
+    private static final String CACHE_KEY_SLOT_INDEX_PROPERTY =
             "cache_key.telephony.get_slot_index";
 
     /** @hide */
diff --git a/tests/FlickerTests/libs/window-extensions-release.aar b/tests/FlickerTests/libs/window-extensions-release.aar
index 6fc9a67..918e514 100644
--- a/tests/FlickerTests/libs/window-extensions-release.aar
+++ b/tests/FlickerTests/libs/window-extensions-release.aar
Binary files differ
diff --git a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
index 3b201f9..e4add80 100644
--- a/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
+++ b/tests/vcn/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtilsTest.java
@@ -16,6 +16,9 @@
 
 package android.net.vcn.persistablebundleutils;
 
+import static android.net.vcn.persistablebundleutils.IkeSessionParamsUtils.IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION;
+import static android.net.vcn.persistablebundleutils.IkeSessionParamsUtils.IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES;
+import static android.net.vcn.persistablebundleutils.IkeSessionParamsUtils.isIkeOptionValid;
 import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.AF_INET6;
 import static android.telephony.TelephonyManager.APPTYPE_USIM;
@@ -134,15 +137,37 @@
         verifyPersistableBundleEncodeDecodeIsLossless(params);
     }
 
+    private static IkeSessionParams.Builder createBuilderMinimumWithEap() throws Exception {
+        final X509Certificate serverCaCert = createCertFromPemFile("self-signed-ca.pem");
+
+        final byte[] eapId = "test@android.net".getBytes(StandardCharsets.US_ASCII);
+        final int subId = 1;
+        final EapSessionConfig eapConfig =
+                new EapSessionConfig.Builder()
+                        .setEapIdentity(eapId)
+                        .setEapSimConfig(subId, APPTYPE_USIM)
+                        .setEapAkaConfig(subId, APPTYPE_USIM)
+                        .build();
+        return createBuilderMinimum().setAuthEap(serverCaCert, eapConfig);
+    }
+
     @Test
     public void testEncodeDecodeParamsWithIkeOptions() throws Exception {
-        final IkeSessionParams params =
-                createBuilderMinimum()
+        final IkeSessionParams.Builder builder =
+                createBuilderMinimumWithEap()
                         .addIkeOption(IkeSessionParams.IKE_OPTION_ACCEPT_ANY_REMOTE_ID)
+                        .addIkeOption(IkeSessionParams.IKE_OPTION_EAP_ONLY_AUTH)
                         .addIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE)
+                        .addIkeOption(IkeSessionParams.IKE_OPTION_FORCE_PORT_4500)
                         .addIkeOption(IkeSessionParams.IKE_OPTION_INITIAL_CONTACT)
-                        .build();
-        verifyPersistableBundleEncodeDecodeIsLossless(params);
+                        .addIkeOption(IkeSessionParams.IKE_OPTION_REKEY_MOBILITY);
+        if (isIkeOptionValid(IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION)) {
+            builder.addIkeOption(IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION);
+        }
+        if (isIkeOptionValid(IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES)) {
+            builder.addIkeOption(IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES);
+        }
+        verifyPersistableBundleEncodeDecodeIsLossless(builder.build());
     }
 
     private static InputStream openAssetsFile(String fileName) throws Exception {
@@ -176,19 +201,7 @@
 
     @Test
     public void testEncodeRecodeParamsWithEapAuth() throws Exception {
-        final X509Certificate serverCaCert = createCertFromPemFile("self-signed-ca.pem");
-
-        final byte[] eapId = "test@android.net".getBytes(StandardCharsets.US_ASCII);
-        final int subId = 1;
-        final EapSessionConfig eapConfig =
-                new EapSessionConfig.Builder()
-                        .setEapIdentity(eapId)
-                        .setEapSimConfig(subId, APPTYPE_USIM)
-                        .setEapAkaConfig(subId, APPTYPE_USIM)
-                        .build();
-
-        final IkeSessionParams params =
-                createBuilderMinimum().setAuthEap(serverCaCert, eapConfig).build();
+        final IkeSessionParams params = createBuilderMinimumWithEap().build();
         verifyPersistableBundleEncodeDecodeIsLossless(params);
     }
 }