Merge "Revert "WallpaperManagerService - avoid leaking whether a package is installed""
diff --git a/Android.bp b/Android.bp
index a5367fe..0c1095d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -249,6 +249,12 @@
         "android.hardware.radio-V1.4-java",
         "android.hardware.radio-V1.5-java",
         "android.hardware.radio-V1.6-java",
+        "android.hardware.radio.data-V1-java",
+        "android.hardware.radio.messaging-V1-java",
+        "android.hardware.radio.modem-V1-java",
+        "android.hardware.radio.network-V1-java",
+        "android.hardware.radio.sim-V1-java",
+        "android.hardware.radio.voice-V1-java",
         "android.hardware.thermal-V1.0-java-constants",
         "android.hardware.thermal-V1.0-java",
         "android.hardware.thermal-V1.1-java",
diff --git a/ApiDocs.bp b/ApiDocs.bp
index c181646..1e5ae7b 100644
--- a/ApiDocs.bp
+++ b/ApiDocs.bp
@@ -127,6 +127,7 @@
         ":framework-sdkextensions-sources",
         ":framework-statsd-sources",
         ":framework-tethering-srcs",
+        ":framework-uwb-updatable-sources",
         ":framework-wifi-updatable-sources",
         ":ike-srcs",
         ":updatable-media-srcs",
@@ -180,6 +181,7 @@
         ":framework-sdkextensions{.public.stubs.source}",
         ":framework-statsd{.public.stubs.source}",
         ":framework-tethering{.public.stubs.source}",
+        ":framework-uwb{.public.stubs.source}",
         ":framework-wifi{.public.stubs.source}",
     ],
     aidl: {
@@ -192,6 +194,43 @@
     },
 }
 
+// This produces the same annotations.zip as framework-doc-stubs, but by using
+// outputs from individual modules instead of all the source code.
+genrule {
+    name: "sdk-annotations.zip",
+    srcs: [
+        ":android-non-updatable-doc-stubs{.annotations.zip}",
+
+        // Conscrypt and i18n currently do not enable annotations
+        // ":conscrypt.module.public.api{.public.annotations.zip}",
+        // ":i18n.module.public.api{.public.annotations.zip}",
+
+        // Modules that enable annotations below
+        ":android.net.ipsec.ike{.public.annotations.zip}",
+        ":art.module.public.api{.public.annotations.zip}",
+        ":framework-appsearch{.public.annotations.zip}",
+        ":framework-connectivity{.public.annotations.zip}",
+        ":framework-graphics{.public.annotations.zip}",
+        ":framework-media{.public.annotations.zip}",
+        ":framework-mediaprovider{.public.annotations.zip}",
+        ":framework-permission{.public.annotations.zip}",
+        ":framework-permission-s{.public.annotations.zip}",
+        ":framework-scheduling{.public.annotations.zip}",
+        ":framework-sdkextensions{.public.annotations.zip}",
+        ":framework-statsd{.public.annotations.zip}",
+        ":framework-tethering{.public.annotations.zip}",
+        ":framework-uwb{.public.annotations.zip}",
+        ":framework-wifi{.public.annotations.zip}",
+    ],
+    out: ["annotations.zip"],
+    tools: [
+        "merge_annotation_zips",
+        "soong_zip",
+    ],
+    cmd: "$(location merge_annotation_zips) $(genDir)/out $(in) && " +
+        "$(location soong_zip) -o $(out) -C $(genDir)/out -D $(genDir)/out",
+}
+
 /////////////////////////////////////////////////////////////////////
 // API docs are created from the generated stub source files
 // using droiddoc
diff --git a/OWNERS b/OWNERS
index ab5bd18..47d8c83 100644
--- a/OWNERS
+++ b/OWNERS
@@ -29,10 +29,7 @@
 # Support bulk translation updates
 per-file */res*/values*/*.xml = byi@google.com, delphij@google.com
 
-per-file Android.bp = file:platform/build/soong:/OWNERS
-per-file Android.mk = file:platform/build/soong:/OWNERS
-per-file ApiDocs.bp = file:platform/build/soong:/OWNERS
-per-file StubLibraries.bp = file:platform/build/soong:/OWNERS
-per-file ProtoLibraries.bp = file:platform/build/soong:/OWNERS
+per-file *.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
+per-file Android.mk = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION}
 per-file TestProtoLibraries.bp = file:platform/platform_testing:/libraries/health/OWNERS
 per-file TestProtoLibraries.bp = file:platform/tools/tradefederation:/OWNERS
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 646a027..0fa4087 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -29,6 +29,9 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.compat.Compatibility;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ClipData;
 import android.content.ComponentName;
@@ -63,6 +66,25 @@
 public class JobInfo implements Parcelable {
     private static String TAG = "JobInfo";
 
+    /**
+     * Disallow setting a deadline (via {@link Builder#setOverrideDeadline(long)}) for prefetch
+     * jobs ({@link Builder#setPrefetch(boolean)}. Prefetch jobs are meant to run close to the next
+     * app launch, so there's no good reason to allow them to have deadlines.
+     *
+     * We don't drop or cancel any previously scheduled prefetch jobs with a deadline.
+     * There's no way for an app to keep a perpetually scheduled prefetch job with a deadline.
+     * Prefetch jobs with a deadline will run and apps under this restriction won't be able to
+     * schedule new prefetch jobs with a deadline. If a job is rescheduled (by providing
+     * {@code true} via {@link JobService#jobFinished(JobParameters, boolean)} or
+     * {@link JobService#onStopJob(JobParameters)}'s return value),the deadline is dropped.
+     * Periodic jobs require all constraints to be met, so there's no issue with their deadlines.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU)
+    public static final long DISALLOW_DEADLINES_FOR_PREFETCH_JOBS = 194532703L;
+
     /** @hide */
     @IntDef(prefix = { "NETWORK_TYPE_" }, value = {
             NETWORK_TYPE_NONE,
@@ -1445,7 +1467,9 @@
         /**
          * Specify that this job should recur with the provided interval, not more than once per
          * period. You have no control over when within this interval this job will be executed,
-         * only the guarantee that it will be executed at most once within this interval.
+         * only the guarantee that it will be executed at most once within this interval, as long
+         * as the constraints are satisfied. If the constraints are not satisfied within this
+         * interval, the job will wait until the constraints are satisfied.
          * Setting this function on the builder with {@link #setMinimumLatency(long)} or
          * {@link #setOverrideDeadline(long)} will result in an error.
          * @param intervalMillis Millisecond interval for which this job will repeat.
@@ -1641,6 +1665,9 @@
          * the specific user of this device. For example, fetching top headlines
          * of interest to the current user.
          * <p>
+         * Starting with Android version {@link Build.VERSION_CODES#TIRAMISU}, prefetch jobs are
+         * not allowed to have deadlines (set via {@link #setOverrideDeadline(long)}.
+         * <p>
          * The system may use this signal to relax the network constraints you
          * originally requested, such as allowing a
          * {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over a metered
@@ -1675,6 +1702,11 @@
          * @return The job object to hand to the JobScheduler. This object is immutable.
          */
         public JobInfo build() {
+            return build(Compatibility.isChangeEnabled(DISALLOW_DEADLINES_FOR_PREFETCH_JOBS));
+        }
+
+        /** @hide */
+        public JobInfo build(boolean disallowPrefetchDeadlines) {
             // This check doesn't need to be inside enforceValidity. It's an unnecessary legacy
             // check that would ideally be phased out instead.
             if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
@@ -1683,7 +1715,7 @@
                         " setRequiresDeviceIdle is an error.");
             }
             JobInfo jobInfo = new JobInfo(this);
-            jobInfo.enforceValidity();
+            jobInfo.enforceValidity(disallowPrefetchDeadlines);
             return jobInfo;
         }
 
@@ -1701,7 +1733,7 @@
     /**
      * @hide
      */
-    public final void enforceValidity() {
+    public final void enforceValidity(boolean disallowPrefetchDeadlines) {
         // Check that network estimates require network type and are reasonable values.
         if ((networkDownloadBytes > 0 || networkUploadBytes > 0 || minimumNetworkChunkBytes > 0)
                 && networkRequest == null) {
@@ -1725,9 +1757,10 @@
             throw new IllegalArgumentException("Minimum chunk size must be positive");
         }
 
+        final boolean hasDeadline = maxExecutionDelayMillis != 0L;
         // Check that a deadline was not set on a periodic job.
         if (isPeriodic) {
-            if (maxExecutionDelayMillis != 0L) {
+            if (hasDeadline) {
                 throw new IllegalArgumentException(
                         "Can't call setOverrideDeadline() on a periodic job.");
             }
@@ -1741,6 +1774,12 @@
             }
         }
 
+        // Prefetch jobs should not have deadlines
+        if (disallowPrefetchDeadlines && hasDeadline && (flags & FLAG_PREFETCH) != 0) {
+            throw new IllegalArgumentException(
+                    "Can't call setOverrideDeadline() on a prefetch job.");
+        }
+
         if (isPersisted) {
             // We can't serialize network specifiers
             if (networkRequest != null
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index acc661e..1429c45 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -235,6 +235,14 @@
     public static final int STOP_REASON_USER = 13;
     /** The system is doing some processing that requires stopping this job. */
     public static final int STOP_REASON_SYSTEM_PROCESSING = 14;
+    /**
+     * The system's estimate of when the app will be launched changed significantly enough to
+     * decide this job shouldn't be running right now. This will mostly apply to prefetch jobs.
+     *
+     * @see JobInfo#isPrefetch()
+     * @see JobInfo.Builder#setPrefetch(boolean)
+     */
+    public static final int STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED = 15;
 
     /** @hide */
     @IntDef(prefix = {"STOP_REASON_"}, value = {
@@ -253,6 +261,7 @@
             STOP_REASON_APP_STANDBY,
             STOP_REASON_USER,
             STOP_REASON_SYSTEM_PROCESSING,
+            STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface StopReason {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java b/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java
index 1f0900d..8f7cda9 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java
@@ -499,7 +499,7 @@
             mCurDataSet.decActive(job.getSourceUid(), job.getSourcePackageName(), now, stopReason);
         }
         rebatchIfNeeded(now);
-        addEvent(job.getJob().isPeriodic() ? EVENT_STOP_JOB :  EVENT_STOP_PERIODIC_JOB,
+        addEvent(job.getJob().isPeriodic() ? EVENT_STOP_PERIODIC_JOB : EVENT_STOP_JOB,
                 job.getSourceUid(), job.getBatteryName(), job.getJobId(), stopReason, debugReason);
     }
 
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 9c4cada..c460312 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -18,6 +18,7 @@
 
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
 import android.annotation.NonNull;
@@ -28,6 +29,7 @@
 import android.app.ActivityManagerInternal;
 import android.app.AppGlobals;
 import android.app.IUidObserver;
+import android.app.compat.CompatChanges;
 import android.app.job.IJobScheduler;
 import android.app.job.JobInfo;
 import android.app.job.JobParameters;
@@ -105,6 +107,7 @@
 import com.android.server.job.controllers.DeviceIdleJobsController;
 import com.android.server.job.controllers.IdleController;
 import com.android.server.job.controllers.JobStatus;
+import com.android.server.job.controllers.PrefetchController;
 import com.android.server.job.controllers.QuotaController;
 import com.android.server.job.controllers.RestrictingController;
 import com.android.server.job.controllers.StateController;
@@ -252,6 +255,8 @@
     private final StorageController mStorageController;
     /** Need directly for sending uid state changes */
     private final DeviceIdleJobsController mDeviceIdleJobsController;
+    /** Needed to get next estimated launch time. */
+    private final PrefetchController mPrefetchController;
     /** Needed to get remaining quota time. */
     private final QuotaController mQuotaController;
     /** Needed to get max execution time and expedited-job allowance. */
@@ -427,6 +432,9 @@
                         case Constants.KEY_CONN_PREFETCH_RELAX_FRAC:
                             mConstants.updateConnectivityConstantsLocked();
                             break;
+                        case Constants.KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS:
+                            mConstants.updatePrefetchConstantsLocked();
+                            break;
                         case Constants.KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS:
                         case Constants.KEY_RUNTIME_MIN_GUARANTEE_MS:
                         case Constants.KEY_RUNTIME_MIN_EJ_GUARANTEE_MS:
@@ -482,6 +490,8 @@
         private static final String KEY_MIN_EXP_BACKOFF_TIME_MS = "min_exp_backoff_time_ms";
         private static final String KEY_CONN_CONGESTION_DELAY_FRAC = "conn_congestion_delay_frac";
         private static final String KEY_CONN_PREFETCH_RELAX_FRAC = "conn_prefetch_relax_frac";
+        private static final String KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS =
+                "prefetch_force_batch_relax_threshold_ms";
         private static final String KEY_ENABLE_API_QUOTAS = "enable_api_quotas";
         private static final String KEY_API_QUOTA_SCHEDULE_COUNT = "aq_schedule_count";
         private static final String KEY_API_QUOTA_SCHEDULE_WINDOW_MS = "aq_schedule_window_ms";
@@ -503,6 +513,7 @@
         private static final long DEFAULT_MIN_EXP_BACKOFF_TIME_MS = JobInfo.MIN_BACKOFF_MILLIS;
         private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f;
         private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f;
+        private static final long DEFAULT_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = HOUR_IN_MILLIS;
         private static final boolean DEFAULT_ENABLE_API_QUOTAS = true;
         private static final int DEFAULT_API_QUOTA_SCHEDULE_COUNT = 250;
         private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS;
@@ -557,6 +568,14 @@
         public float CONN_PREFETCH_RELAX_FRAC = DEFAULT_CONN_PREFETCH_RELAX_FRAC;
 
         /**
+         * The amount of time within which we would consider the app to be launching relatively soon
+         * and will relax the force batching policy on prefetch jobs. If the app is not going to be
+         * launched within this amount of time from now, then we will force batch the prefetch job.
+         */
+        public long PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS =
+                DEFAULT_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS;
+
+        /**
          * Whether to enable quota limits on APIs.
          */
         public boolean ENABLE_API_QUOTAS = DEFAULT_ENABLE_API_QUOTAS;
@@ -635,6 +654,13 @@
                     DEFAULT_CONN_PREFETCH_RELAX_FRAC);
         }
 
+        private void updatePrefetchConstantsLocked() {
+            PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS = DeviceConfig.getLong(
+                    DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+                    KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS,
+                    DEFAULT_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS);
+        }
+
         private void updateApiQuotaConstantsLocked() {
             ENABLE_API_QUOTAS = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_JOB_SCHEDULER,
                     KEY_ENABLE_API_QUOTAS, DEFAULT_ENABLE_API_QUOTAS);
@@ -700,6 +726,8 @@
             pw.print(KEY_MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME_MS).println();
             pw.print(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println();
             pw.print(KEY_CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC).println();
+            pw.print(KEY_PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS,
+                    PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS).println();
 
             pw.print(KEY_ENABLE_API_QUOTAS, ENABLE_API_QUOTAS).println();
             pw.print(KEY_API_QUOTA_SCHEDULE_COUNT, API_QUOTA_SCHEDULE_COUNT).println();
@@ -1577,6 +1605,8 @@
         mControllers.add(new ContentObserverController(this));
         mDeviceIdleJobsController = new DeviceIdleJobsController(this);
         mControllers.add(mDeviceIdleJobsController);
+        mPrefetchController = new PrefetchController(this);
+        mControllers.add(mPrefetchController);
         mQuotaController =
                 new QuotaController(this, backgroundJobsController, connectivityController);
         mControllers.add(mQuotaController);
@@ -2333,6 +2363,15 @@
                 } else if (job.getEffectiveStandbyBucket() == RESTRICTED_INDEX) {
                     // Restricted jobs must always be batched
                     shouldForceBatchJob = true;
+                } else if (job.getJob().isPrefetch()) {
+                    // Only relax batching on prefetch jobs if we expect the app to be launched
+                    // relatively soon. PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS defines what
+                    // "relatively soon" means.
+                    final long relativelySoonCutoffTime = sSystemClock.millis()
+                            + mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS;
+                    shouldForceBatchJob =
+                            mPrefetchController.getNextEstimatedLaunchTimeLocked(job)
+                                    > relativelySoonCutoffTime;
                 } else if (job.getNumFailures() > 0) {
                     shouldForceBatchJob = false;
                 } else {
@@ -2894,7 +2933,9 @@
         }
 
         private void validateJobFlags(JobInfo job, int callingUid) {
-            job.enforceValidity();
+            job.enforceValidity(
+                    CompatChanges.isChangeEnabled(
+                            JobInfo.DISALLOW_DEADLINES_FOR_PREFETCH_JOBS, callingUid));
             if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) {
                 getContext().enforceCallingOrSelfPermission(
                         android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
index d1afc80..7c1e4c9f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -49,7 +49,6 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.BitUtils;
 import com.android.server.IoThread;
-import com.android.server.LocalServices;
 import com.android.server.job.JobSchedulerInternal.JobStorePersistStats;
 import com.android.server.job.controllers.JobStatus;
 
@@ -978,10 +977,17 @@
 
             final JobInfo builtJob;
             try {
-                builtJob = jobBuilder.build();
+                // Don't perform prefetch-deadline check here. Apps targeting S- shouldn't have
+                // any prefetch-with-deadline jobs accidentally dropped. It's not worth doing
+                // target SDK version checks here for apps targeting T+. There's no way for an
+                // app to keep a perpetually scheduled prefetch job with a deadline. Prefetch jobs
+                // with a deadline would run and then any newly scheduled prefetch jobs wouldn't
+                // have a deadline. If a job is rescheduled (via jobFinished(true) or onStopJob()'s
+                // return value), the deadline is dropped. Periodic jobs require all constraints
+                // to be met, so there's no issue with their deadlines.
+                builtJob = jobBuilder.build(false);
             } catch (Exception e) {
-                Slog.w(TAG, "Unable to build job from XML, ignoring: "
-                        + jobBuilder.summarize());
+                Slog.w(TAG, "Unable to build job from XML, ignoring: " + jobBuilder.summarize(), e);
                 return null;
             }
 
@@ -997,11 +1003,10 @@
             }
 
             // And now we're done
-            JobSchedulerInternal service = LocalServices.getService(JobSchedulerInternal.class);
             final int appBucket = JobSchedulerService.standbyBucketForPackage(sourcePackageName,
                     sourceUserId, elapsedNow);
             JobStatus js = new JobStatus(
-                    jobBuilder.build(), uid, sourcePackageName, sourceUserId,
+                    builtJob, uid, sourcePackageName, sourceUserId,
                     appBucket, sourceTag,
                     elapsedRuntimes.first, elapsedRuntimes.second,
                     lastSuccessfulRunTime, lastFailedRunTime,
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 98a39a6..aca381f 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
@@ -113,7 +113,6 @@
         userFilter.addAction(Intent.ACTION_USER_STOPPED);
         mContext.registerReceiverAsUser(
                 mBroadcastReceiver, UserHandle.ALL, userFilter, null, null);
-
     }
 
     @Override
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 3801885..b4651a9 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
@@ -93,6 +93,7 @@
     static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26;
     static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint
     static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24;      // Implicit constraint
+    static final int CONSTRAINT_PREFETCH = 1 << 23;
     static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint
 
     // The following set of dynamic constraints are for specific use cases (as explained in their
@@ -147,6 +148,7 @@
     private static final int STATSD_CONSTRAINTS_TO_LOG = CONSTRAINT_CONTENT_TRIGGER
             | CONSTRAINT_DEADLINE
             | CONSTRAINT_IDLE
+            | CONSTRAINT_PREFETCH
             | CONSTRAINT_TARE_WEALTH
             | CONSTRAINT_TIMING_DELAY
             | CONSTRAINT_WITHIN_QUOTA;
@@ -553,7 +555,9 @@
             requestBuilder.setUids(
                     Collections.singleton(new Range<Integer>(this.sourceUid, this.sourceUid)));
             builder.setRequiredNetwork(requestBuilder.build());
-            job = builder.build();
+            // Don't perform prefetch-deadline check at this point. We've already passed the
+            // initial validation check.
+            job = builder.build(false);
         }
 
         final JobSchedulerInternal jsi = LocalServices.getService(JobSchedulerInternal.class);
@@ -1176,6 +1180,11 @@
     }
 
     /** @return true if the constraint was changed, false otherwise. */
+    boolean setPrefetchConstraintSatisfied(final long nowElapsed, boolean state) {
+        return setConstraintSatisfied(CONSTRAINT_PREFETCH, nowElapsed, state);
+    }
+
+    /** @return true if the constraint was changed, false otherwise. */
     boolean setTimingDelayConstraintSatisfied(final long nowElapsed, boolean state) {
         return setConstraintSatisfied(CONSTRAINT_TIMING_DELAY, nowElapsed, state);
     }
@@ -1387,6 +1396,9 @@
             case CONSTRAINT_DEVICE_NOT_DOZING:
                 return JobParameters.STOP_REASON_DEVICE_STATE;
 
+            case CONSTRAINT_PREFETCH:
+                return JobParameters.STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED;
+
             case CONSTRAINT_TARE_WEALTH:
             case CONSTRAINT_WITHIN_QUOTA:
                 return JobParameters.STOP_REASON_QUOTA;
@@ -1581,12 +1593,12 @@
     /** All constraints besides implicit and deadline. */
     static final int CONSTRAINTS_OF_INTEREST = CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW
             | CONSTRAINT_STORAGE_NOT_LOW | CONSTRAINT_TIMING_DELAY | CONSTRAINT_CONNECTIVITY
-            | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER;
+            | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER | CONSTRAINT_PREFETCH;
 
     // Soft override covers all non-"functional" constraints
     static final int SOFT_OVERRIDE_CONSTRAINTS =
             CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_STORAGE_NOT_LOW
-                    | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE;
+                    | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE | CONSTRAINT_PREFETCH;
 
     /** Returns true whenever all dynamically set constraints are satisfied. */
     public boolean areDynamicConstraintsSatisfied() {
@@ -1775,6 +1787,9 @@
         if ((constraints&CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
             pw.print(" BACKGROUND_NOT_RESTRICTED");
         }
+        if ((constraints & CONSTRAINT_PREFETCH) != 0) {
+            pw.print(" PREFETCH");
+        }
         if ((constraints & CONSTRAINT_TARE_WEALTH) != 0) {
             pw.print(" TARE_WEALTH");
         }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java
new file mode 100644
index 0000000..78a77fe
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/Package.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.job.controllers;
+
+import java.util.Objects;
+
+/** Wrapper class to represent a userId-pkgName combo. */
+final class Package {
+    public final String packageName;
+    public final int userId;
+
+    Package(int userId, String packageName) {
+        this.userId = userId;
+        this.packageName = packageName;
+    }
+
+    @Override
+    public String toString() {
+        return packageToString(userId, packageName);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof Package)) {
+            return false;
+        }
+        Package other = (Package) obj;
+        return userId == other.userId && Objects.equals(packageName, other.packageName);
+    }
+
+    @Override
+    public int hashCode() {
+        return packageName.hashCode() + userId;
+    }
+
+    /**
+     * Standardize the output of userId-packageName combo.
+     */
+    static String packageToString(int userId, String packageName) {
+        return "<" + userId + ">" + packageName;
+    }
+}
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
new file mode 100644
index 0000000..6232dfb
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.job.controllers;
+
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+
+import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+import static com.android.server.job.JobSchedulerService.sSystemClock;
+import static com.android.server.job.controllers.Package.packageToString;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArrayMap;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.JobSchedulerBackgroundThread;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.utils.AlarmQueue;
+
+import java.util.function.Predicate;
+
+/**
+ * Controller to delay prefetch jobs until we get close to an expected app launch.
+ */
+public class PrefetchController extends StateController {
+    private static final String TAG = "JobScheduler.Prefetch";
+    private static final boolean DEBUG = JobSchedulerService.DEBUG
+            || Log.isLoggable(TAG, Log.DEBUG);
+
+    private final PcConstants mPcConstants;
+
+    @GuardedBy("mLock")
+    private final SparseArrayMap<String, ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>();
+    /**
+     * Cached set of the estimated next launch times of each app. Time are in the current time
+     * millis ({@link CurrentTimeMillisLong}) timebase.
+     */
+    @GuardedBy("mLock")
+    private final SparseArrayMap<String, Long> mEstimatedLaunchTimes = new SparseArrayMap<>();
+    private final ThresholdAlarmListener mThresholdAlarmListener;
+
+    /**
+     * The cutoff point to decide if a prefetch job is worth running or not. If the app is expected
+     * to launch within this amount of time into the future, then we will let a prefetch job run.
+     */
+    @GuardedBy("mLock")
+    @CurrentTimeMillisLong
+    private long mLaunchTimeThresholdMs = PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS;
+
+    public PrefetchController(JobSchedulerService service) {
+        super(service);
+        mPcConstants = new PcConstants();
+        mThresholdAlarmListener = new ThresholdAlarmListener(
+                mContext, JobSchedulerBackgroundThread.get().getLooper());
+    }
+
+    @Override
+    @GuardedBy("mLock")
+    public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
+        if (jobStatus.getJob().isPrefetch()) {
+            final int userId = jobStatus.getSourceUserId();
+            final String pkgName = jobStatus.getSourcePackageName();
+            ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
+            if (jobs == null) {
+                jobs = new ArraySet<>();
+                mTrackedJobs.add(userId, pkgName, jobs);
+            }
+            final long now = sSystemClock.millis();
+            final long nowElapsed = sElapsedRealtimeClock.millis();
+            if (jobs.add(jobStatus) && jobs.size() == 1
+                    && !willBeLaunchedSoonLocked(userId, pkgName, now)) {
+                updateThresholdAlarmLocked(userId, pkgName, now, nowElapsed);
+            }
+            updateConstraintLocked(jobStatus, now, nowElapsed);
+        }
+    }
+
+    @Override
+    @GuardedBy("mLock")
+    public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
+            boolean forUpdate) {
+        final int userId = jobStatus.getSourceUserId();
+        final String pkgName = jobStatus.getSourcePackageName();
+        final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
+        if (jobs != null && jobs.remove(jobStatus) && jobs.size() == 0) {
+            mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+        }
+    }
+
+    @Override
+    @GuardedBy("mLock")
+    public void onAppRemovedLocked(String packageName, int uid) {
+        if (packageName == null) {
+            Slog.wtf(TAG, "Told app removed but given null package name.");
+            return;
+        }
+        final int userId = UserHandle.getUserId(uid);
+        mTrackedJobs.delete(userId, packageName);
+        mEstimatedLaunchTimes.delete(userId, packageName);
+        mThresholdAlarmListener.removeAlarmForKey(new Package(userId, packageName));
+    }
+
+    @Override
+    @GuardedBy("mLock")
+    public void onUserRemovedLocked(int userId) {
+        mTrackedJobs.delete(userId);
+        mEstimatedLaunchTimes.delete(userId);
+        mThresholdAlarmListener.removeAlarmsForUserId(userId);
+    }
+
+    /** Return the app's next estimated launch time. */
+    @GuardedBy("mLock")
+    @CurrentTimeMillisLong
+    public long getNextEstimatedLaunchTimeLocked(@NonNull JobStatus jobStatus) {
+        final int userId = jobStatus.getSourceUserId();
+        final String pkgName = jobStatus.getSourcePackageName();
+        return getNextEstimatedLaunchTimeLocked(userId, pkgName, sSystemClock.millis());
+    }
+
+    @GuardedBy("mLock")
+    @CurrentTimeMillisLong
+    private long getNextEstimatedLaunchTimeLocked(int userId, @NonNull String pkgName,
+            @CurrentTimeMillisLong long now) {
+        Long nextEstimatedLaunchTime = mEstimatedLaunchTimes.get(userId, pkgName);
+        if (nextEstimatedLaunchTime == null || nextEstimatedLaunchTime < now) {
+            // TODO(194532703): get estimated time from UsageStats
+            nextEstimatedLaunchTime = now + 2 * HOUR_IN_MILLIS;
+            mEstimatedLaunchTimes.add(userId, pkgName, nextEstimatedLaunchTime);
+        }
+        return nextEstimatedLaunchTime;
+    }
+
+    @GuardedBy("mLock")
+    private boolean maybeUpdateConstraintForPkgLocked(@CurrentTimeMillisLong long now,
+            @ElapsedRealtimeLong long nowElapsed, int userId, String pkgName) {
+        final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
+        if (jobs == null) {
+            return false;
+        }
+        boolean changed = false;
+        for (int i = 0; i < jobs.size(); i++) {
+            final JobStatus js = jobs.valueAt(i);
+            changed |= updateConstraintLocked(js, now, nowElapsed);
+        }
+        return changed;
+    }
+
+    @GuardedBy("mLock")
+    private boolean updateConstraintLocked(@NonNull JobStatus jobStatus,
+            @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
+        return jobStatus.setPrefetchConstraintSatisfied(nowElapsed,
+                willBeLaunchedSoonLocked(
+                        jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), now));
+    }
+
+    @GuardedBy("mLock")
+    private void updateThresholdAlarmLocked(int userId, @NonNull String pkgName,
+            @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
+        final ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
+        if (jobs == null || jobs.size() == 0) {
+            mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+            return;
+        }
+
+        final long nextEstimatedLaunchTime = getNextEstimatedLaunchTimeLocked(userId, pkgName, now);
+        if (nextEstimatedLaunchTime - now > mLaunchTimeThresholdMs) {
+            // Set alarm to be notified when this crosses the threshold.
+            final long timeToCrossThresholdMs =
+                    nextEstimatedLaunchTime - (now + mLaunchTimeThresholdMs);
+            mThresholdAlarmListener.addAlarm(new Package(userId, pkgName),
+                    nowElapsed + timeToCrossThresholdMs);
+        } else {
+            mThresholdAlarmListener.removeAlarmForKey(new Package(userId, pkgName));
+        }
+    }
+
+    /**
+     * Returns true if the app is expected to be launched soon, where "soon" is within the next
+     * {@link #mLaunchTimeThresholdMs} time.
+     */
+    @GuardedBy("mLock")
+    private boolean willBeLaunchedSoonLocked(int userId, @NonNull String pkgName,
+            @CurrentTimeMillisLong long now) {
+        return getNextEstimatedLaunchTimeLocked(userId, pkgName, now)
+                <= now + mLaunchTimeThresholdMs;
+    }
+
+    @Override
+    @GuardedBy("mLock")
+    public void prepareForUpdatedConstantsLocked() {
+        mPcConstants.mShouldReevaluateConstraints = false;
+    }
+
+    @Override
+    @GuardedBy("mLock")
+    public void processConstantLocked(DeviceConfig.Properties properties, String key) {
+        mPcConstants.processConstantLocked(properties, key);
+    }
+
+    @Override
+    @GuardedBy("mLock")
+    public void onConstantsUpdatedLocked() {
+        if (mPcConstants.mShouldReevaluateConstraints) {
+            // Update job bookkeeping out of band.
+            JobSchedulerBackgroundThread.getHandler().post(() -> {
+                final ArraySet<JobStatus> changedJobs = new ArraySet<>();
+                synchronized (mLock) {
+                    final long nowElapsed = sElapsedRealtimeClock.millis();
+                    final long now = sSystemClock.millis();
+                    for (int u = 0; u < mTrackedJobs.numMaps(); ++u) {
+                        final int userId = mTrackedJobs.keyAt(u);
+                        for (int p = 0; p < mTrackedJobs.numElementsForKey(userId); ++p) {
+                            final String packageName = mTrackedJobs.keyAt(u, p);
+                            if (maybeUpdateConstraintForPkgLocked(
+                                    now, nowElapsed, userId, packageName)) {
+                                changedJobs.addAll(mTrackedJobs.valueAt(u, p));
+                            }
+                            if (!willBeLaunchedSoonLocked(userId, packageName, now)) {
+                                updateThresholdAlarmLocked(userId, packageName, now, nowElapsed);
+                            }
+                        }
+                    }
+                }
+                if (changedJobs.size() > 0) {
+                    mStateChangedListener.onControllerStateChanged(changedJobs);
+                }
+            });
+        }
+    }
+
+    /** Track when apps will cross the "will run soon" threshold. */
+    private class ThresholdAlarmListener extends AlarmQueue<Package> {
+        private ThresholdAlarmListener(Context context, Looper looper) {
+            super(context, looper, "*job.prefetch*", "Prefetch threshold", false,
+                    PcConstants.DEFAULT_LAUNCH_TIME_THRESHOLD_MS / 10);
+        }
+
+        @Override
+        protected boolean isForUser(@NonNull Package key, int userId) {
+            return key.userId == userId;
+        }
+
+        @Override
+        protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+            final ArraySet<JobStatus> changedJobs = new ArraySet<>();
+            synchronized (mLock) {
+                final long now = sSystemClock.millis();
+                final long nowElapsed = sElapsedRealtimeClock.millis();
+                for (int i = 0; i < expired.size(); ++i) {
+                    Package p = expired.valueAt(i);
+                    if (!willBeLaunchedSoonLocked(p.userId, p.packageName, now)) {
+                        Slog.e(TAG, "Alarm expired for "
+                                + packageToString(p.userId, p.packageName) + " at the wrong time");
+                        updateThresholdAlarmLocked(p.userId, p.packageName, now, nowElapsed);
+                    } else if (maybeUpdateConstraintForPkgLocked(
+                            now, nowElapsed, p.userId, p.packageName)) {
+                        changedJobs.addAll(mTrackedJobs.get(p.userId, p.packageName));
+                    }
+                }
+            }
+            if (changedJobs.size() > 0) {
+                mStateChangedListener.onControllerStateChanged(changedJobs);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    class PcConstants {
+        private boolean mShouldReevaluateConstraints = false;
+
+        /** Prefix to use with all constant keys in order to "sub-namespace" the keys. */
+        private static final String PC_CONSTANT_PREFIX = "pc_";
+
+        @VisibleForTesting
+        static final String KEY_LAUNCH_TIME_THRESHOLD_MS =
+                PC_CONSTANT_PREFIX + "launch_time_threshold_ms";
+
+        private static final long DEFAULT_LAUNCH_TIME_THRESHOLD_MS = 7 * HOUR_IN_MILLIS;
+
+        /** How much time each app will have to run jobs within their standby bucket window. */
+        public long LAUNCH_TIME_THRESHOLD_MS = DEFAULT_LAUNCH_TIME_THRESHOLD_MS;
+
+        @GuardedBy("mLock")
+        public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
+                @NonNull String key) {
+            switch (key) {
+                case KEY_LAUNCH_TIME_THRESHOLD_MS:
+                    LAUNCH_TIME_THRESHOLD_MS =
+                            properties.getLong(key, DEFAULT_LAUNCH_TIME_THRESHOLD_MS);
+                    // Limit the threshold to the range [1, 24] hours.
+                    long newLaunchTimeThresholdMs = Math.min(24 * HOUR_IN_MILLIS,
+                            Math.max(HOUR_IN_MILLIS, LAUNCH_TIME_THRESHOLD_MS));
+                    if (mLaunchTimeThresholdMs != newLaunchTimeThresholdMs) {
+                        mLaunchTimeThresholdMs = newLaunchTimeThresholdMs;
+                        mShouldReevaluateConstraints = true;
+                        // Give a leeway of 10% of the launch time threshold between alarms.
+                        mThresholdAlarmListener.setMinTimeBetweenAlarmsMs(
+                                mLaunchTimeThresholdMs / 10);
+                    }
+                    break;
+            }
+        }
+
+        private void dump(IndentingPrintWriter pw) {
+            pw.println();
+            pw.print(PrefetchController.class.getSimpleName());
+            pw.println(":");
+            pw.increaseIndent();
+
+            pw.print(KEY_LAUNCH_TIME_THRESHOLD_MS, LAUNCH_TIME_THRESHOLD_MS).println();
+
+            pw.decreaseIndent();
+        }
+    }
+
+    //////////////////////// TESTING HELPERS /////////////////////////////
+
+    @VisibleForTesting
+    long getLaunchTimeThresholdMs() {
+        return mLaunchTimeThresholdMs;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    PcConstants getPcConstants() {
+        return mPcConstants;
+    }
+
+    //////////////////////////// DATA DUMP //////////////////////////////
+
+    @Override
+    @GuardedBy("mLock")
+    public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
+        final long now = sSystemClock.millis();
+
+        pw.println("Cached launch times:");
+        pw.increaseIndent();
+        for (int u = 0; u < mEstimatedLaunchTimes.numMaps(); ++u) {
+            final int userId = mEstimatedLaunchTimes.keyAt(u);
+            for (int p = 0; p < mEstimatedLaunchTimes.numElementsForKey(userId); ++p) {
+                final String pkgName = mEstimatedLaunchTimes.keyAt(u, p);
+                final long estimatedLaunchTime = mEstimatedLaunchTimes.valueAt(u, p);
+
+                pw.print("<" + userId + ">" + pkgName + ": ");
+                pw.print(estimatedLaunchTime);
+                pw.print(" (");
+                TimeUtils.formatDuration(estimatedLaunchTime - now, pw,
+                        TimeUtils.HUNDRED_DAY_FIELD_LEN);
+                pw.println(" from now)");
+            }
+        }
+        pw.decreaseIndent();
+
+        pw.println();
+        mTrackedJobs.forEach((jobs) -> {
+            for (int j = 0; j < jobs.size(); j++) {
+                final JobStatus js = jobs.valueAt(j);
+                if (!predicate.test(js)) {
+                    continue;
+                }
+                pw.print("#");
+                js.printUniqueId(pw);
+                pw.print(" from ");
+                UserHandle.formatUid(pw, js.getSourceUid());
+                pw.println();
+            }
+        });
+
+        pw.println();
+        mThresholdAlarmListener.dump(pw);
+    }
+
+    @Override
+    public void dumpConstants(IndentingPrintWriter pw) {
+        mPcConstants.dump(pw);
+    }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 4ea46eb..31da526 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -27,6 +27,7 @@
 import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
 import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+import static com.android.server.job.controllers.Package.packageToString;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -56,7 +57,6 @@
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
-import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseArrayMap;
@@ -76,11 +76,10 @@
 import com.android.server.job.StateControllerProto;
 import com.android.server.usage.AppStandbyInternal;
 import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
+import com.android.server.utils.AlarmQueue;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
-import java.util.PriorityQueue;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -124,52 +123,6 @@
             PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                     | PackageManager.GET_PERMISSIONS | PackageManager.MATCH_KNOWN_PACKAGES;
 
-    /**
-     * Standardize the output of userId-packageName combo.
-     */
-    private static String string(int userId, String packageName) {
-        return "<" + userId + ">" + packageName;
-    }
-
-    private static final class Package {
-        public final String packageName;
-        public final int userId;
-
-        Package(int userId, String packageName) {
-            this.userId = userId;
-            this.packageName = packageName;
-        }
-
-        @Override
-        public String toString() {
-            return string(userId, packageName);
-        }
-
-        public void dumpDebug(ProtoOutputStream proto, long fieldId) {
-            final long token = proto.start(fieldId);
-
-            proto.write(StateControllerProto.QuotaController.Package.USER_ID, userId);
-            proto.write(StateControllerProto.QuotaController.Package.NAME, packageName);
-
-            proto.end(token);
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (obj instanceof Package) {
-                Package other = (Package) obj;
-                return userId == other.userId && Objects.equals(packageName, other.packageName);
-            } else {
-                return false;
-            }
-        }
-
-        @Override
-        public int hashCode() {
-            return packageName.hashCode() + userId;
-        }
-    }
-
     private static int hashLong(long val) {
         return (int) (val ^ (val >>> 32));
     }
@@ -323,10 +276,10 @@
             new SparseArrayMap<>();
 
     /**
-     * Listener to track and manage when each package comes back within quota.
+     * Queue to track and manage when each package comes back within quota.
      */
     @GuardedBy("mLock")
-    private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener();
+    private final InQuotaAlarmQueue mInQuotaAlarmQueue;
 
     /** Cached calculation results for each app, with the standby buckets as the array indices. */
     private final SparseArrayMap<String, ExecutionStats[]> mExecutionStatsCache =
@@ -589,11 +542,12 @@
         mHandler = new QcHandler(mContext.getMainLooper());
         mChargeTracker = new ChargingTracker();
         mChargeTracker.startTracking();
-        mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+        mAlarmManager = mContext.getSystemService(AlarmManager.class);
         mQcConstants = new QcConstants();
         mBackgroundJobsController = backgroundJobsController;
         mConnectivityController = connectivityController;
         mIsEnabled = !mConstants.USE_TARE_POLICY;
+        mInQuotaAlarmQueue = new InQuotaAlarmQueue(mContext, mContext.getMainLooper());
 
         // Set up the app standby bucketing tracker
         AppStandbyInternal appStandby = LocalServices.getService(AppStandbyInternal.class);
@@ -742,7 +696,7 @@
         mEJPkgTimers.delete(userId);
         mTimingSessions.delete(userId);
         mEJTimingSessions.delete(userId);
-        mInQuotaAlarmListener.removeAlarmsLocked(userId);
+        mInQuotaAlarmQueue.removeAlarmsForUserId(userId);
         mExecutionStatsCache.delete(userId);
         mEJStats.delete(userId);
         mSystemInstallers.remove(userId);
@@ -768,7 +722,7 @@
         }
         mTimingSessions.delete(userId, packageName);
         mEJTimingSessions.delete(userId, packageName);
-        mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
+        mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
         mExecutionStatsCache.delete(userId, packageName);
         mEJStats.delete(userId, packageName);
         mTopAppTrackers.delete(userId, packageName);
@@ -1657,7 +1611,7 @@
             // exempted.
             maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket);
         } else {
-            mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
+            mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
         }
         return changed;
     }
@@ -1695,7 +1649,7 @@
             if (isWithinQuotaLocked(userId, packageName, realStandbyBucket) && isWithinEJQuota) {
                 // TODO(141645789): we probably shouldn't cancel the alarm until we've verified
                 // that all jobs for the userId-package are within quota.
-                mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
+                mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
             } else {
                 mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket);
             }
@@ -1741,7 +1695,6 @@
             return;
         }
 
-        final String pkgString = string(userId, packageName);
         ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
         final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket);
         final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats,
@@ -1755,12 +1708,13 @@
         if (inRegularQuota && remainingEJQuota > 0) {
             // Already in quota. Why was this method called?
             if (DEBUG) {
-                Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
+                Slog.e(TAG, "maybeScheduleStartAlarmLocked called for "
+                        + packageToString(userId, packageName)
                         + " even though it already has "
                         + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
                         + "ms in its quota.");
             }
-            mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
+            mInQuotaAlarmQueue.removeAlarmForKey(new Package(userId, packageName));
             mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
             return;
         }
@@ -1811,8 +1765,8 @@
                 // In some strange cases, an app may end be in the NEVER bucket but could have run
                 // some regular jobs. This results in no EJ timing sessions and QC having a bad
                 // time.
-                Slog.wtf(TAG,
-                        string(userId, packageName) + " has 0 EJ quota without running anything");
+                Slog.wtf(TAG, packageToString(userId, packageName)
+                        + " has 0 EJ quota without running anything");
                 return;
             }
         }
@@ -1825,7 +1779,7 @@
                             + nowElapsed + ", inQuotaTime=" + inQuotaTimeElapsed + ": " + stats);
             inQuotaTimeElapsed = nowElapsed + 5 * MINUTE_IN_MILLIS;
         }
-        mInQuotaAlarmListener.addAlarmLocked(userId, packageName, inQuotaTimeElapsed);
+        mInQuotaAlarmQueue.addAlarm(new Package(userId, packageName), inQuotaTimeElapsed);
     }
 
     private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, long nowElapsed,
@@ -2272,7 +2226,6 @@
         public void dump(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate) {
             final long token = proto.start(fieldId);
 
-            mPkg.dumpDebug(proto, StateControllerProto.QuotaController.Timer.PKG);
             proto.write(StateControllerProto.QuotaController.Timer.IS_ACTIVE, isActive());
             proto.write(StateControllerProto.QuotaController.Timer.START_TIME_ELAPSED,
                     mStartTimeElapsed);
@@ -2381,7 +2334,6 @@
         public void dump(ProtoOutputStream proto, long fieldId) {
             final long token = proto.start(fieldId);
 
-            mPkg.dumpDebug(proto, StateControllerProto.QuotaController.TopAppTimer.PKG);
             proto.write(StateControllerProto.QuotaController.TopAppTimer.IS_ACTIVE, isActive());
             proto.write(StateControllerProto.QuotaController.TopAppTimer.START_TIME_ELAPSED,
                     mStartTimeElapsed);
@@ -2413,7 +2365,7 @@
     void updateStandbyBucket(
             final int userId, final @NonNull String packageName, final int bucketIndex) {
         if (DEBUG) {
-            Slog.i(TAG, "Moving pkg " + string(userId, packageName)
+            Slog.i(TAG, "Moving pkg " + packageToString(userId, packageName)
                     + " to bucketIndex " + bucketIndex);
         }
         List<JobStatus> restrictedChanges = new ArrayList<>();
@@ -2641,7 +2593,7 @@
                         String packageName = (String) msg.obj;
                         int userId = msg.arg1;
                         if (DEBUG) {
-                            Slog.d(TAG, "Checking pkg " + string(userId, packageName));
+                            Slog.d(TAG, "Checking pkg " + packageToString(userId, packageName));
                         }
                         if (maybeUpdateConstraintForPkgLocked(sElapsedRealtimeClock.millis(),
                                 userId, packageName)) {
@@ -2722,7 +2674,7 @@
                         final String pkgName = event.getPackageName();
                         if (DEBUG) {
                             Slog.d(TAG, "Processing event " + event.getEventType()
-                                    + " for " + string(userId, pkgName));
+                                    + " for " + packageToString(userId, pkgName));
                         }
                         switch (event.getEventType()) {
                             case UsageEvents.Event.ACTIVITY_RESUMED:
@@ -2805,176 +2757,25 @@
         }
     }
 
-    static class AlarmQueue extends PriorityQueue<Pair<Package, Long>> {
-        AlarmQueue() {
-            super(1, (o1, o2) -> (int) (o1.second - o2.second));
-        }
-
-        /**
-         * Remove any instances of the Package from the queue.
-         *
-         * @return true if an instance was removed, false otherwise.
-         */
-        boolean remove(@NonNull Package pkg) {
-            boolean removed = false;
-            Pair[] alarms = toArray(new Pair[size()]);
-            for (int i = alarms.length - 1; i >= 0; --i) {
-                if (pkg.equals(alarms[i].first)) {
-                    remove(alarms[i]);
-                    removed = true;
-                }
-            }
-            return removed;
-        }
-    }
-
     /** Track when UPTCs are expected to come back into quota. */
-    private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener {
-        @GuardedBy("mLock")
-        private final AlarmQueue mAlarmQueue = new AlarmQueue();
-        /** The next time the alarm is set to go off, in the elapsed realtime timebase. */
-        @GuardedBy("mLock")
-        private long mTriggerTimeElapsed = 0;
-        /** The minimum amount of time between quota check alarms. */
-        @GuardedBy("mLock")
-        private long mMinQuotaCheckDelayMs = QcConstants.DEFAULT_MIN_QUOTA_CHECK_DELAY_MS;
-
-        @GuardedBy("mLock")
-        void addAlarmLocked(int userId, @NonNull String pkgName, long inQuotaTimeElapsed) {
-            final Package pkg = new Package(userId, pkgName);
-            mAlarmQueue.remove(pkg);
-            mAlarmQueue.offer(new Pair<>(pkg, inQuotaTimeElapsed));
-            setNextAlarmLocked();
-        }
-
-        @GuardedBy("mLock")
-        void setMinQuotaCheckDelayMs(long minDelayMs) {
-            mMinQuotaCheckDelayMs = minDelayMs;
-        }
-
-        @GuardedBy("mLock")
-        void removeAlarmLocked(@NonNull Package pkg) {
-            if (mAlarmQueue.remove(pkg)) {
-                setNextAlarmLocked();
-            }
-        }
-
-        @GuardedBy("mLock")
-        void removeAlarmLocked(int userId, @NonNull String packageName) {
-            removeAlarmLocked(new Package(userId, packageName));
-        }
-
-        @GuardedBy("mLock")
-        void removeAlarmsLocked(int userId) {
-            boolean removed = false;
-            Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
-            for (int i = alarms.length - 1; i >= 0; --i) {
-                final Package pkg = (Package) alarms[i].first;
-                if (userId == pkg.userId) {
-                    mAlarmQueue.remove(alarms[i]);
-                    removed = true;
-                }
-            }
-            if (removed) {
-                setNextAlarmLocked();
-            }
-        }
-
-        @GuardedBy("mLock")
-        private void setNextAlarmLocked() {
-            setNextAlarmLocked(sElapsedRealtimeClock.millis());
-        }
-
-        @GuardedBy("mLock")
-        private void setNextAlarmLocked(long earliestTriggerElapsed) {
-            if (mAlarmQueue.size() > 0) {
-                final Pair<Package, Long> alarm = mAlarmQueue.peek();
-                final long nextTriggerTimeElapsed = Math.max(earliestTriggerElapsed, alarm.second);
-                // Only schedule the alarm if one of the following is true:
-                // 1. There isn't one currently scheduled
-                // 2. The new alarm is significantly earlier than the previous alarm. If it's
-                // earlier but not significantly so, then we essentially delay the job a few extra
-                // minutes.
-                // 3. The alarm is after the current alarm.
-                if (mTriggerTimeElapsed == 0
-                        || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS
-                        || mTriggerTimeElapsed < nextTriggerTimeElapsed) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "Scheduling start alarm at " + nextTriggerTimeElapsed
-                                + " for app " + alarm.first);
-                    }
-                    mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed,
-                            ALARM_TAG_QUOTA_CHECK, this, mHandler);
-                    mTriggerTimeElapsed = nextTriggerTimeElapsed;
-                }
-            } else {
-                mAlarmManager.cancel(this);
-                mTriggerTimeElapsed = 0;
-            }
+    private class InQuotaAlarmQueue extends AlarmQueue<Package> {
+        private InQuotaAlarmQueue(Context context, Looper looper) {
+            super(context, looper, ALARM_TAG_QUOTA_CHECK, "In quota", false,
+                    QcConstants.DEFAULT_MIN_QUOTA_CHECK_DELAY_MS);
         }
 
         @Override
-        public void onAlarm() {
-            synchronized (mLock) {
-                while (mAlarmQueue.size() > 0) {
-                    final Pair<Package, Long> alarm = mAlarmQueue.peek();
-                    if (alarm.second <= sElapsedRealtimeClock.millis()) {
-                        mHandler.obtainMessage(MSG_CHECK_PACKAGE, alarm.first.userId, 0,
-                                alarm.first.packageName).sendToTarget();
-                        mAlarmQueue.remove(alarm);
-                    } else {
-                        break;
-                    }
-                }
-                setNextAlarmLocked(sElapsedRealtimeClock.millis() + mMinQuotaCheckDelayMs);
-            }
+        protected boolean isForUser(@NonNull Package key, int userId) {
+            return key.userId == userId;
         }
 
-        @GuardedBy("mLock")
-        void dumpLocked(IndentingPrintWriter pw) {
-            pw.println("In quota alarms:");
-            pw.increaseIndent();
-
-            if (mAlarmQueue.size() == 0) {
-                pw.println("NOT WAITING");
-            } else {
-                Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
-                for (int i = 0; i < alarms.length; ++i) {
-                    final Package pkg = (Package) alarms[i].first;
-                    pw.print(pkg);
-                    pw.print(": ");
-                    pw.print(alarms[i].second);
-                    pw.println();
-                }
+        @Override
+        protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+            for (int i = 0; i < expired.size(); ++i) {
+                Package p = expired.valueAt(i);
+                mHandler.obtainMessage(MSG_CHECK_PACKAGE, p.userId, 0, p.packageName)
+                        .sendToTarget();
             }
-
-            pw.decreaseIndent();
-        }
-
-        @GuardedBy("mLock")
-        void dumpLocked(ProtoOutputStream proto, long fieldId) {
-            final long token = proto.start(fieldId);
-
-            proto.write(
-                    StateControllerProto.QuotaController.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED,
-                    mTriggerTimeElapsed);
-
-            Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
-            for (int i = 0; i < alarms.length; ++i) {
-                final long aToken = proto.start(
-                        StateControllerProto.QuotaController.InQuotaAlarmListener.ALARMS);
-
-                final Package pkg = (Package) alarms[i].first;
-                pkg.dumpDebug(proto,
-                        StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.PKG);
-                proto.write(
-                        StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED,
-                        (Long) alarms[i].second);
-
-                proto.end(aToken);
-            }
-
-            proto.end(token);
         }
     }
 
@@ -3563,7 +3364,7 @@
                             properties.getLong(key, DEFAULT_MIN_QUOTA_CHECK_DELAY_MS);
                     // We don't need to re-evaluate execution stats or constraint status for this.
                     // Limit the delay to the range [0, 15] minutes.
-                    mInQuotaAlarmListener.setMinQuotaCheckDelayMs(
+                    mInQuotaAlarmQueue.setMinTimeBetweenAlarmsMs(
                             Math.min(15 * MINUTE_IN_MILLIS, Math.max(0, MIN_QUOTA_CHECK_DELAY_MS)));
                     break;
                 case KEY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS:
@@ -4104,7 +3905,7 @@
 
     @VisibleForTesting
     long getMinQuotaCheckDelayMs() {
-        return mInQuotaAlarmListener.mMinQuotaCheckDelayMs;
+        return mInQuotaAlarmQueue.getMinTimeBetweenAlarmsMs();
     }
 
     @VisibleForTesting
@@ -4270,7 +4071,7 @@
                 final String pkgName = mExecutionStatsCache.keyAt(u, p);
                 ExecutionStats[] stats = mExecutionStatsCache.valueAt(u, p);
 
-                pw.println(string(userId, pkgName));
+                pw.println(packageToString(userId, pkgName));
                 pw.increaseIndent();
                 for (int i = 0; i < stats.length; ++i) {
                     ExecutionStats executionStats = stats[i];
@@ -4294,7 +4095,7 @@
                 final String pkgName = mEJStats.keyAt(u, p);
                 ShrinkableDebits debits = mEJStats.valueAt(u, p);
 
-                pw.print(string(userId, pkgName));
+                pw.print(packageToString(userId, pkgName));
                 pw.print(": ");
                 debits.dumpLocked(pw);
             }
@@ -4302,7 +4103,7 @@
         pw.decreaseIndent();
 
         pw.println();
-        mInQuotaAlarmListener.dumpLocked(pw);
+        mInQuotaAlarmQueue.dump(pw);
         pw.decreaseIndent();
     }
 
@@ -4438,9 +4239,6 @@
             }
         }
 
-        mInQuotaAlarmListener.dumpLocked(proto,
-                StateControllerProto.QuotaController.IN_QUOTA_ALARM_LISTENER);
-
         proto.end(mToken);
         proto.end(token);
     }
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
index ad56d93..035c9d2 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
@@ -17,7 +17,6 @@
 package com.android.server.tare;
 
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
 import static com.android.server.tare.EconomicPolicy.REGULATION_BASIC_INCOME;
 import static com.android.server.tare.EconomicPolicy.REGULATION_BIRTHRIGHT;
@@ -32,7 +31,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.AlarmManager;
+import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.os.Handler;
@@ -43,7 +42,6 @@
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
-import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArrayMap;
 
@@ -52,13 +50,13 @@
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.usage.AppStandbyInternal;
+import com.android.server.utils.AlarmQueue;
 
 import libcore.util.EmptyArray;
 
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
-import java.util.PriorityQueue;
 import java.util.function.Consumer;
 
 /**
@@ -103,12 +101,11 @@
             mActionAffordabilityNotes = new SparseArrayMap<>();
 
     /**
-     * Listener to track and manage when apps will cross the closest affordability threshold (in
+     * Queue to track and manage when apps will cross the closest affordability threshold (in
      * both directions).
      */
     @GuardedBy("mLock")
-    private final BalanceThresholdAlarmListener mBalanceThresholdAlarmListener =
-            new BalanceThresholdAlarmListener();
+    private final BalanceThresholdAlarmQueue mBalanceThresholdAlarmQueue;
 
     /**
      * Comparator to use to sort apps before we distribute ARCs so that we try to give the most
@@ -159,7 +156,6 @@
             };
 
     private static final int MSG_CHECK_BALANCE = 0;
-    private static final int MSG_SET_BALANCE_ALARM = 1;
 
     Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe) {
         mLock = irs.getLock();
@@ -167,6 +163,8 @@
         mScribe = scribe;
         mHandler = new AgentHandler(TareHandlerThread.get().getLooper());
         mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class);
+        mBalanceThresholdAlarmQueue = new BalanceThresholdAlarmQueue(
+                mIrs.getContext(), TareHandlerThread.get().getLooper());
     }
 
     private class TotalDeltaCalculator implements Consumer<OngoingEvent> {
@@ -692,7 +690,7 @@
     @GuardedBy("mLock")
     void onPackageRemovedLocked(final int userId, @NonNull final String pkgName) {
         reclaimAssetsLocked(userId, pkgName);
-        mBalanceThresholdAlarmListener.removeAlarmLocked(userId, pkgName);
+        mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
     }
 
     /**
@@ -712,7 +710,7 @@
     @GuardedBy("mLock")
     void onUserRemovedLocked(final int userId, @NonNull final List<String> pkgNames) {
         reclaimAssetsLocked(userId, pkgNames);
-        mBalanceThresholdAlarmListener.removeAlarmsLocked(userId);
+        mBalanceThresholdAlarmQueue.removeAlarmsForUserId(userId);
     }
 
     @GuardedBy("mLock")
@@ -817,7 +815,7 @@
                 mCurrentOngoingEvents.get(userId, pkgName);
         if (ongoingEvents == null) {
             // No ongoing transactions. No reason to schedule
-            mBalanceThresholdAlarmListener.removeAlarmLocked(userId, pkgName);
+            mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
             return;
         }
         mTrendCalculator.reset(
@@ -829,7 +827,7 @@
         if (lowerTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
             if (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD) {
                 // Will never cross a threshold based on current events.
-                mBalanceThresholdAlarmListener.removeAlarmLocked(userId, pkgName);
+                mBalanceThresholdAlarmQueue.removeAlarmForKey(new Package(userId, pkgName));
                 return;
             }
             timeToThresholdMs = upperTimeMs;
@@ -837,14 +835,14 @@
             timeToThresholdMs = (upperTimeMs == TrendCalculator.WILL_NOT_CROSS_THRESHOLD)
                     ? lowerTimeMs : Math.min(lowerTimeMs, upperTimeMs);
         }
-        mBalanceThresholdAlarmListener.addAlarmLocked(userId, pkgName,
+        mBalanceThresholdAlarmQueue.addAlarm(new Package(userId, pkgName),
                 SystemClock.elapsedRealtime() + timeToThresholdMs);
     }
 
     @GuardedBy("mLock")
     void tearDownLocked() {
         mCurrentOngoingEvents.clear();
-        mBalanceThresholdAlarmListener.dropAllAlarmsLocked();
+        mBalanceThresholdAlarmQueue.removeAllAlarms();
     }
 
     @VisibleForTesting
@@ -869,261 +867,60 @@
         }
     }
 
-    /**
-     * An {@link AlarmManager.OnAlarmListener} that will queue up all pending alarms and only
-     * schedule one alarm for the earliest alarm.
-     */
-    private abstract class AlarmQueueListener implements AlarmManager.OnAlarmListener {
-        final class Package {
-            public final String packageName;
-            public final int userId;
+    private static final class Package {
+        public final String packageName;
+        public final int userId;
 
-            Package(int userId, String packageName) {
-                this.userId = userId;
-                this.packageName = packageName;
-            }
-
-            @Override
-            public String toString() {
-                return appToString(userId, packageName);
-            }
-
-            @Override
-            public boolean equals(Object obj) {
-                if (obj == null) {
-                    return false;
-                }
-                if (this == obj) {
-                    return true;
-                }
-                if (obj instanceof Package) {
-                    Package other = (Package) obj;
-                    return userId == other.userId && Objects.equals(packageName, other.packageName);
-                } else {
-                    return false;
-                }
-            }
-
-            @Override
-            public int hashCode() {
-                return packageName.hashCode() + userId;
-            }
+        Package(int userId, String packageName) {
+            this.userId = userId;
+            this.packageName = packageName;
         }
 
-        class AlarmQueue extends PriorityQueue<Pair<Package, Long>> {
-            AlarmQueue() {
-                super(1, (o1, o2) -> (int) (o1.second - o2.second));
-            }
-
-            boolean contains(@NonNull Package pkg) {
-                Pair[] alarms = toArray(new Pair[size()]);
-                for (int i = alarms.length - 1; i >= 0; --i) {
-                    if (pkg.equals(alarms[i].first)) {
-                        return true;
-                    }
-                }
-                return false;
-            }
-
-            /**
-             * Remove any instances of the Package from the queue.
-             *
-             * @return true if an instance was removed, false otherwise.
-             */
-            boolean remove(@NonNull Package pkg) {
-                boolean removed = false;
-                Pair[] alarms = toArray(new Pair[size()]);
-                for (int i = alarms.length - 1; i >= 0; --i) {
-                    if (pkg.equals(alarms[i].first)) {
-                        remove(alarms[i]);
-                        removed = true;
-                    }
-                }
-                return removed;
-            }
-        }
-
-        @GuardedBy("mLock")
-        private final AlarmQueue mAlarmQueue = new AlarmQueue();
-        private final String mAlarmTag;
-        /** Whether to use an exact alarm or an inexact alarm. */
-        private final boolean mExactAlarm;
-        /** The minimum amount of time between check alarms. */
-        private final long mMinTimeBetweenAlarmsMs;
-        /** The next time the alarm is set to go off, in the elapsed realtime timebase. */
-        @GuardedBy("mLock")
-        private long mTriggerTimeElapsed = 0;
-
-        protected AlarmQueueListener(@NonNull String alarmTag, boolean exactAlarm,
-                long minTimeBetweenAlarmsMs) {
-            mAlarmTag = alarmTag;
-            mExactAlarm = exactAlarm;
-            mMinTimeBetweenAlarmsMs = minTimeBetweenAlarmsMs;
-        }
-
-        @GuardedBy("mLock")
-        boolean hasAlarmScheduledLocked(int userId, @NonNull String pkgName) {
-            final Package pkg = new Package(userId, pkgName);
-            return mAlarmQueue.contains(pkg);
-        }
-
-        @GuardedBy("mLock")
-        void addAlarmLocked(int userId, @NonNull String pkgName, long alarmTimeElapsed) {
-            final Package pkg = new Package(userId, pkgName);
-            mAlarmQueue.remove(pkg);
-            mAlarmQueue.offer(new Pair<>(pkg, alarmTimeElapsed));
-            setNextAlarmLocked();
-        }
-
-        @GuardedBy("mLock")
-        void removeAlarmLocked(@NonNull Package pkg) {
-            if (mAlarmQueue.remove(pkg)) {
-                setNextAlarmLocked();
-            }
-        }
-
-        @GuardedBy("mLock")
-        void removeAlarmLocked(int userId, @NonNull String packageName) {
-            removeAlarmLocked(new Package(userId, packageName));
-        }
-
-        @GuardedBy("mLock")
-        void removeAlarmsLocked(int userId) {
-            boolean removed = false;
-            Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
-            for (int i = alarms.length - 1; i >= 0; --i) {
-                final Package pkg = (Package) alarms[i].first;
-                if (userId == pkg.userId) {
-                    mAlarmQueue.remove(alarms[i]);
-                    removed = true;
-                }
-            }
-            if (removed) {
-                setNextAlarmLocked();
-            }
-        }
-
-        /** Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue. */
-        @GuardedBy("mLock")
-        void setNextAlarmLocked() {
-            setNextAlarmLocked(SystemClock.elapsedRealtime());
-        }
-
-        /**
-         * Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue, using
-         * {@code earliestTriggerElapsed} as a floor.
-         */
-        @GuardedBy("mLock")
-        private void setNextAlarmLocked(long earliestTriggerElapsed) {
-            if (mAlarmQueue.size() > 0) {
-                final Pair<Package, Long> alarm = mAlarmQueue.peek();
-                final long nextTriggerTimeElapsed = Math.max(earliestTriggerElapsed, alarm.second);
-                // Only schedule the alarm if one of the following is true:
-                // 1. There isn't one currently scheduled
-                // 2. The new alarm is significantly earlier than the previous alarm. If it's
-                // earlier but not significantly so, then we essentially delay the check for some
-                // apps by up to a minute.
-                // 3. The alarm is after the current alarm.
-                if (mTriggerTimeElapsed == 0
-                        || nextTriggerTimeElapsed < mTriggerTimeElapsed - MINUTE_IN_MILLIS
-                        || mTriggerTimeElapsed < nextTriggerTimeElapsed) {
-                    if (DEBUG) {
-                        Slog.d(TAG, "Scheduling start alarm at " + nextTriggerTimeElapsed
-                                + " for app " + alarm.first);
-                    }
-                    mHandler.post(() -> {
-                        // Never call out to AlarmManager with the lock held. This sits below AM.
-                        AlarmManager alarmManager =
-                                mIrs.getContext().getSystemService(AlarmManager.class);
-                        if (alarmManager != null) {
-                            if (mExactAlarm) {
-                                alarmManager.setExact(AlarmManager.ELAPSED_REALTIME,
-                                        nextTriggerTimeElapsed, mAlarmTag, this, mHandler);
-                            } else {
-                                alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME,
-                                        nextTriggerTimeElapsed, mMinTimeBetweenAlarmsMs / 2,
-                                        mAlarmTag, this, mHandler);
-                            }
-                        } else {
-                            mHandler.sendEmptyMessageDelayed(MSG_SET_BALANCE_ALARM, 30_000);
-                        }
-                    });
-                    mTriggerTimeElapsed = nextTriggerTimeElapsed;
-                }
-            } else {
-                mHandler.post(() -> {
-                    // Never call out to AlarmManager with the lock held. This sits below AM.
-                    AlarmManager alarmManager =
-                            mIrs.getContext().getSystemService(AlarmManager.class);
-                    if (alarmManager != null) {
-                        // This should only be null at boot time. No concerns around not
-                        // cancelling if we get null here.
-                        alarmManager.cancel(this);
-                    }
-                });
-                mTriggerTimeElapsed = 0;
-            }
-        }
-
-        @GuardedBy("mLock")
-        void dropAllAlarmsLocked() {
-            mAlarmQueue.clear();
-            setNextAlarmLocked(0);
-        }
-
-        @GuardedBy("mLock")
-        protected abstract void processExpiredAlarmLocked(int userId, @NonNull String packageName);
-
         @Override
-        public void onAlarm() {
-            synchronized (mLock) {
-                final long nowElapsed = SystemClock.elapsedRealtime();
-                while (mAlarmQueue.size() > 0) {
-                    final Pair<Package, Long> alarm = mAlarmQueue.peek();
-                    if (alarm.second <= nowElapsed) {
-                        processExpiredAlarmLocked(alarm.first.userId, alarm.first.packageName);
-                        mAlarmQueue.remove(alarm);
-                    } else {
-                        break;
-                    }
-                }
-                setNextAlarmLocked(nowElapsed + mMinTimeBetweenAlarmsMs);
-            }
+        public String toString() {
+            return appToString(userId, packageName);
         }
 
-        @GuardedBy("mLock")
-        void dumpLocked(IndentingPrintWriter pw) {
-            pw.print(mAlarmTag);
-            pw.println(" alarms:");
-            pw.increaseIndent();
-
-            if (mAlarmQueue.size() == 0) {
-                pw.println("NOT WAITING");
-            } else {
-                Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
-                for (int i = 0; i < alarms.length; ++i) {
-                    final Package pkg = (Package) alarms[i].first;
-                    pw.print(pkg);
-                    pw.print(": ");
-                    pw.print(alarms[i].second);
-                    pw.println();
-                }
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) {
+                return false;
             }
+            if (this == obj) {
+                return true;
+            }
+            if (obj instanceof Package) {
+                Package other = (Package) obj;
+                return userId == other.userId && Objects.equals(packageName, other.packageName);
+            }
+            return false;
+        }
 
-            pw.decreaseIndent();
+        @Override
+        public int hashCode() {
+            return packageName.hashCode() + userId;
         }
     }
 
     /** Track when apps will cross the closest affordability threshold (in both directions). */
-    private class BalanceThresholdAlarmListener extends AlarmQueueListener {
-        private BalanceThresholdAlarmListener() {
-            super(ALARM_TAG_AFFORDABILITY_CHECK, true, 15_000L);
+    private class BalanceThresholdAlarmQueue extends AlarmQueue<Package> {
+        private BalanceThresholdAlarmQueue(Context context, Looper looper) {
+            super(context, looper, ALARM_TAG_AFFORDABILITY_CHECK, "Affordability check", true,
+                    15_000L);
         }
 
         @Override
-        @GuardedBy("mLock")
-        protected void processExpiredAlarmLocked(int userId, @NonNull String packageName) {
-            mHandler.obtainMessage(MSG_CHECK_BALANCE, userId, 0, packageName).sendToTarget();
+        protected boolean isForUser(@NonNull Package key, int userId) {
+            return key.userId == userId;
+        }
+
+        @Override
+        protected void processExpiredAlarms(@NonNull ArraySet<Package> expired) {
+            for (int i = 0; i < expired.size(); ++i) {
+                Package p = expired.valueAt(i);
+                mHandler.obtainMessage(MSG_CHECK_BALANCE, p.userId, 0, p.packageName)
+                        .sendToTarget();
+            }
         }
     }
 
@@ -1288,13 +1085,6 @@
                     }
                 }
                 break;
-
-                case MSG_SET_BALANCE_ALARM: {
-                    synchronized (mLock) {
-                        mBalanceThresholdAlarmListener.setNextAlarmLocked();
-                    }
-                }
-                break;
             }
         }
     }
@@ -1302,6 +1092,6 @@
     @GuardedBy("mLock")
     void dumpLocked(IndentingPrintWriter pw) {
         pw.println();
-        mBalanceThresholdAlarmListener.dumpLocked(pw);
+        mBalanceThresholdAlarmQueue.dump(pw);
     }
 }
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
index 1acb270..2b4938b 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java
@@ -94,6 +94,7 @@
 import static com.android.server.tare.Modifier.COST_MODIFIER_POWER_SAVE_MODE;
 import static com.android.server.tare.Modifier.COST_MODIFIER_PROCESS_STATE;
 import static com.android.server.tare.TareUtils.arcToNarc;
+import static com.android.server.tare.TareUtils.narcToString;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -342,6 +343,11 @@
 
     @Override
     void dump(IndentingPrintWriter pw) {
+        pw.print("Min satiated balance", narcToString(mMinSatiatedBalance)).println();
+        pw.print("Max satiated balance", narcToString(mMaxSatiatedBalance)).println();
+        pw.print("Min satiated circulation", narcToString(mMaxSatiatedCirculation)).println();
+
+        pw.println();
         pw.println("Actions:");
         pw.increaseIndent();
         for (int i = 0; i < mActions.size(); ++i) {
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
index e9fa926..a39fd47 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java
@@ -537,10 +537,11 @@
     private void setupHeavyWork() {
         synchronized (mLock) {
             loadInstalledPackageListLocked();
-            // TODO: base on if we have anything persisted
-            final boolean isFirstSetup = true;
+            final boolean isFirstSetup = !mScribe.recordExists();
             if (isFirstSetup) {
                 mAgent.grantBirthrightsLocked();
+            } else {
+                mScribe.loadFromDiskLocked();
             }
             scheduleUnusedWealthReclamationLocked();
         }
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
index fa732ea..728f5e8 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/JobSchedulerEconomicPolicy.java
@@ -103,6 +103,7 @@
 import static com.android.server.tare.Modifier.COST_MODIFIER_POWER_SAVE_MODE;
 import static com.android.server.tare.Modifier.COST_MODIFIER_PROCESS_STATE;
 import static com.android.server.tare.TareUtils.arcToNarc;
+import static com.android.server.tare.TareUtils.narcToString;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -316,6 +317,11 @@
 
     @Override
     void dump(IndentingPrintWriter pw) {
+        pw.print("Min satiated balance", narcToString(mMinSatiatedBalance)).println();
+        pw.print("Max satiated balance", narcToString(mMaxSatiatedBalance)).println();
+        pw.print("Min satiated circulation", narcToString(mMaxSatiatedCirculation)).println();
+
+        pw.println();
         pw.println("Actions:");
         pw.increaseIndent();
         for (int i = 0; i < mActions.size(); ++i) {
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
index f2b78c0..a234ae6 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Ledger.java
@@ -61,6 +61,11 @@
     Ledger() {
     }
 
+    Ledger(long currentBalance, @NonNull List<Transaction> transactions) {
+        mCurrentBalance = currentBalance;
+        mTransactions.addAll(transactions);
+    }
+
     long getCurrentBalance() {
         return mCurrentBalance;
     }
@@ -73,6 +78,11 @@
         return null;
     }
 
+    @NonNull
+    List<Transaction> getTransactions() {
+        return mTransactions;
+    }
+
     void recordTransaction(@NonNull Transaction transaction) {
         mTransactions.add(transaction);
         mCurrentBalance += transaction.delta;
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
index 2c133dcb..d4c6c8c 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java
@@ -21,11 +21,34 @@
 import static com.android.server.tare.TareUtils.appToString;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.PackageInfo;
+import android.os.Environment;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.AtomicFile;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseArray;
 import android.util.SparseArrayMap;
+import android.util.TypedXmlPullParser;
+import android.util.TypedXmlSerializer;
+import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Maintains the current TARE state and handles writing it to disk and reading it back from disk.
@@ -44,40 +67,76 @@
      */
     private static final long MAX_TRANSACTION_AGE_MS = 24 * HOUR_IN_MILLIS;
 
+    private static final String XML_TAG_HIGH_LEVEL_STATE = "irs-state";
+    private static final String XML_TAG_LEDGER = "ledger";
+    private static final String XML_TAG_TARE = "tare";
+    private static final String XML_TAG_TRANSACTION = "transaction";
+    private static final String XML_TAG_USER = "user";
+
+    private static final String XML_ATTR_DELTA = "delta";
+    private static final String XML_ATTR_EVENT_ID = "eventId";
+    private static final String XML_ATTR_TAG = "tag";
+    private static final String XML_ATTR_START_TIME = "startTime";
+    private static final String XML_ATTR_END_TIME = "endTime";
+    private static final String XML_ATTR_PACKAGE_NAME = "pkgName";
+    private static final String XML_ATTR_CURRENT_BALANCE = "currentBalance";
+    private static final String XML_ATTR_USER_ID = "userId";
+    private static final String XML_ATTR_VERSION = "version";
+    private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime";
+
+    /** Version of the file schema. */
+    private static final int STATE_FILE_VERSION = 0;
+    /** Minimum amount of time between consecutive writes. */
+    private static final long WRITE_DELAY = 30_000L;
+
+    private final AtomicFile mStateFile;
     private final InternalResourceService mIrs;
 
-    @GuardedBy("mIrs.mLock")
+    @GuardedBy("mIrs.getLock()")
     private long mLastReclamationTime;
-    @GuardedBy("mIrs.mLock")
+    @GuardedBy("mIrs.getLock()")
     private long mNarcsInCirculation;
-    @GuardedBy("mIrs.mLock")
+    @GuardedBy("mIrs.getLock()")
     private final SparseArrayMap<String, Ledger> mLedgers = new SparseArrayMap<>();
 
     private final Runnable mCleanRunnable = this::cleanupLedgers;
+    private final Runnable mWriteRunnable = this::writeState;
 
     Scribe(InternalResourceService irs) {
-        mIrs = irs;
+        this(irs, Environment.getDataSystemDirectory());
     }
 
-    @GuardedBy("mIrs.mLock")
+    @VisibleForTesting
+    Scribe(InternalResourceService irs, File dataDir) {
+        mIrs = irs;
+
+        final File tareDir = new File(dataDir, "tare");
+        //noinspection ResultOfMethodCallIgnored
+        tareDir.mkdirs();
+        mStateFile = new AtomicFile(new File(tareDir, "state.xml"), "tare");
+    }
+
+    @GuardedBy("mIrs.getLock()")
     void adjustNarcsInCirculationLocked(long delta) {
         if (delta != 0) {
             // No point doing any work if the change is 0.
             mNarcsInCirculation += delta;
+            postWrite();
         }
     }
 
-    @GuardedBy("mIrs.mLock")
+    @GuardedBy("mIrs.getLock()")
     void discardLedgerLocked(final int userId, @NonNull final String pkgName) {
         mLedgers.delete(userId, pkgName);
+        postWrite();
     }
 
-    @GuardedBy("mIrs.mLock")
+    @GuardedBy("mIrs.getLock()")
     long getLastReclamationTimeLocked() {
         return mLastReclamationTime;
     }
 
-    @GuardedBy("mIrs.mLock")
+    @GuardedBy("mIrs.getLock()")
     @NonNull
     Ledger getLedgerLocked(final int userId, @NonNull final String pkgName) {
         Ledger ledger = mLedgers.get(userId, pkgName);
@@ -89,33 +148,118 @@
     }
 
     /** Returns the total amount of narcs currently allocated to apps. */
-    @GuardedBy("mIrs.mLock")
+    @GuardedBy("mIrs.getLock()")
     long getNarcsInCirculationLocked() {
         return mNarcsInCirculation;
     }
 
-    @GuardedBy("mIrs.mLock")
-    void setLastReclamationTimeLocked(long time) {
-        mLastReclamationTime = time;
+    @GuardedBy("mIrs.getLock()")
+    void loadFromDiskLocked() {
+        mLedgers.clear();
+        mNarcsInCirculation = 0;
+        if (!recordExists()) {
+            return;
+        }
+
+        final SparseArray<ArraySet<String>> installedPackagesPerUser = new SparseArray<>();
+        final List<PackageInfo> installedPackages = mIrs.getInstalledPackages();
+        for (int i = 0; i < installedPackages.size(); ++i) {
+            final PackageInfo packageInfo = installedPackages.get(i);
+            if (packageInfo.applicationInfo != null) {
+                final int userId = UserHandle.getUserId(packageInfo.applicationInfo.uid);
+                ArraySet<String> pkgsForUser = installedPackagesPerUser.get(userId);
+                if (pkgsForUser == null) {
+                    pkgsForUser = new ArraySet<>();
+                    installedPackagesPerUser.put(userId, pkgsForUser);
+                }
+                pkgsForUser.add(packageInfo.packageName);
+            }
+        }
+
+        try (FileInputStream fis = mStateFile.openRead()) {
+            TypedXmlPullParser parser = Xml.resolvePullParser(fis);
+
+            int eventType = parser.getEventType();
+            while (eventType != XmlPullParser.START_TAG
+                    && eventType != XmlPullParser.END_DOCUMENT) {
+                eventType = parser.next();
+            }
+            if (eventType == XmlPullParser.END_DOCUMENT) {
+                if (DEBUG) {
+                    Slog.w(TAG, "No persisted state.");
+                }
+                return;
+            }
+
+            String tagName = parser.getName();
+            if (XML_TAG_TARE.equals(tagName)) {
+                final int version = parser.getAttributeInt(null, XML_ATTR_VERSION);
+                if (version < 0 || version > STATE_FILE_VERSION) {
+                    Slog.e(TAG, "Invalid version number (" + version + "), aborting file read");
+                    return;
+                }
+            }
+
+            final long endTimeCutoff = System.currentTimeMillis() - MAX_TRANSACTION_AGE_MS;
+            long earliestEndTime = Long.MAX_VALUE;
+            for (eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
+                    eventType = parser.next()) {
+                if (eventType != XmlPullParser.START_TAG) {
+                    continue;
+                }
+                tagName = parser.getName();
+                if (tagName == null) {
+                    continue;
+                }
+
+                switch (tagName) {
+                    case XML_TAG_HIGH_LEVEL_STATE:
+                        mLastReclamationTime =
+                                parser.getAttributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME);
+                        break;
+                    case XML_TAG_USER:
+                        earliestEndTime = Math.min(earliestEndTime,
+                                readUserFromXmlLocked(
+                                        parser, installedPackagesPerUser, endTimeCutoff));
+                        break;
+                    default:
+                        Slog.e(TAG, "Unexpected tag: " + tagName);
+                        break;
+                }
+            }
+            scheduleCleanup(earliestEndTime);
+        } catch (IOException | XmlPullParserException e) {
+            Slog.wtf(TAG, "Error reading state from disk", e);
+        }
     }
 
-    @GuardedBy("mIrs.mLock")
+    @VisibleForTesting
+    void postWrite() {
+        TareHandlerThread.getHandler().postDelayed(mWriteRunnable, WRITE_DELAY);
+    }
+
+    boolean recordExists() {
+        return mStateFile.exists();
+    }
+
+    @GuardedBy("mIrs.getLock()")
+    void setLastReclamationTimeLocked(long time) {
+        mLastReclamationTime = time;
+        postWrite();
+    }
+
+    @GuardedBy("mIrs.getLock()")
     void tearDownLocked() {
+        TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
+        TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable);
         mLedgers.clear();
         mNarcsInCirculation = 0;
         mLastReclamationTime = 0;
     }
 
-    private void scheduleCleanup(long earliestEndTime) {
-        if (earliestEndTime == Long.MAX_VALUE) {
-            return;
-        }
-        // This is just cleanup to manage memory. We don't need to do it too often or at the exact
-        // intended real time, so the delay that comes from using the Handler (and is limited
-        // to uptime) should be fine.
-        final long delayMs = Math.max(HOUR_IN_MILLIS,
-                earliestEndTime + MAX_TRANSACTION_AGE_MS - System.currentTimeMillis());
-        TareHandlerThread.getHandler().postDelayed(mCleanRunnable, delayMs);
+    @VisibleForTesting
+    void writeImmediatelyForTesting() {
+        mWriteRunnable.run();
     }
 
     private void cleanupLedgers() {
@@ -139,7 +283,226 @@
         }
     }
 
-    @GuardedBy("mIrs.mLock")
+    /**
+     * @param parser Xml parser at the beginning of a "<ledger/>" tag. The next "parser.next()" call
+     *               will take the parser into the body of the ledger tag.
+     * @return Newly instantiated ledger holding all the information we just read out of the xml
+     * tag, and the package name associated with the ledger.
+     */
+    @Nullable
+    private static Pair<String, Ledger> readLedgerFromXml(TypedXmlPullParser parser,
+            ArraySet<String> validPackages, long endTimeCutoff)
+            throws XmlPullParserException, IOException {
+        final String pkgName;
+        final long curBalance;
+        final List<Ledger.Transaction> transactions = new ArrayList<>();
+
+        pkgName = parser.getAttributeValue(null, XML_ATTR_PACKAGE_NAME);
+        curBalance = parser.getAttributeLong(null, XML_ATTR_CURRENT_BALANCE);
+
+        final boolean isInstalled = validPackages.contains(pkgName);
+        if (!isInstalled) {
+            // Don't return early since we need to go through all the transaction tags and get
+            // to the end of the ledger tag.
+            Slog.w(TAG, "Invalid pkg " + pkgName + " is saved to disk");
+        }
+
+        for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
+                eventType = parser.next()) {
+            final String tagName = parser.getName();
+            if (eventType == XmlPullParser.END_TAG) {
+                if (XML_TAG_LEDGER.equals(tagName)) {
+                    // We've reached the end of the ledger tag.
+                    break;
+                }
+                continue;
+            }
+            if (eventType != XmlPullParser.START_TAG || !XML_TAG_TRANSACTION.equals(tagName)) {
+                // Expecting only "transaction" tags.
+                Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
+                return null;
+            }
+            if (!isInstalled) {
+                continue;
+            }
+            if (DEBUG) {
+                Slog.d(TAG, "Starting ledger tag: " + tagName);
+            }
+            final String tag = parser.getAttributeValue(null, XML_ATTR_TAG);
+            final long startTime = parser.getAttributeLong(null, XML_ATTR_START_TIME);
+            final long endTime = parser.getAttributeLong(null, XML_ATTR_END_TIME);
+            final int eventId = parser.getAttributeInt(null, XML_ATTR_EVENT_ID);
+            final long delta = parser.getAttributeLong(null, XML_ATTR_DELTA);
+            if (endTime <= endTimeCutoff) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Skipping event because it's too old.");
+                }
+                continue;
+            }
+            transactions.add(new Ledger.Transaction(startTime, endTime, eventId, tag, delta));
+        }
+
+        if (!isInstalled) {
+            return null;
+        }
+        return Pair.create(pkgName, new Ledger(curBalance, transactions));
+    }
+
+    /**
+     * @param parser Xml parser at the beginning of a "<user>" tag. The next "parser.next()" call
+     *               will take the parser into the body of the user tag.
+     * @return The earliest valid transaction end time found for the user.
+     */
+    @GuardedBy("mIrs.getLock()")
+    private long readUserFromXmlLocked(TypedXmlPullParser parser,
+            SparseArray<ArraySet<String>> installedPackagesPerUser,
+            long endTimeCutoff) throws XmlPullParserException, IOException {
+        int curUser = parser.getAttributeInt(null, XML_ATTR_USER_ID);
+        final ArraySet<String> installedPackages = installedPackagesPerUser.get(curUser);
+        if (installedPackages == null) {
+            Slog.w(TAG, "Invalid user " + curUser + " is saved to disk");
+            curUser = UserHandle.USER_NULL;
+            // Don't return early since we need to go through all the ledger tags and get to the end
+            // of the user tag.
+        }
+        long earliestEndTime = Long.MAX_VALUE;
+
+        for (int eventType = parser.next(); eventType != XmlPullParser.END_DOCUMENT;
+                eventType = parser.next()) {
+            final String tagName = parser.getName();
+            if (eventType == XmlPullParser.END_TAG) {
+                if (XML_TAG_USER.equals(tagName)) {
+                    // We've reached the end of the user tag.
+                    break;
+                }
+                continue;
+            }
+            if (XML_TAG_LEDGER.equals(tagName)) {
+                if (curUser == UserHandle.USER_NULL) {
+                    continue;
+                }
+                final Pair<String, Ledger> ledgerData =
+                        readLedgerFromXml(parser, installedPackages, endTimeCutoff);
+                if (ledgerData == null) {
+                    continue;
+                }
+                final Ledger ledger = ledgerData.second;
+                if (ledger != null) {
+                    mLedgers.add(curUser, ledgerData.first, ledger);
+                    mNarcsInCirculation += Math.max(0, ledger.getCurrentBalance());
+                    final Ledger.Transaction transaction = ledger.getEarliestTransaction();
+                    if (transaction != null) {
+                        earliestEndTime = Math.min(earliestEndTime, transaction.endTimeMs);
+                    }
+                }
+            } else {
+                Slog.e(TAG, "Unknown tag: " + tagName);
+            }
+        }
+
+        return earliestEndTime;
+    }
+
+    private void scheduleCleanup(long earliestEndTime) {
+        if (earliestEndTime == Long.MAX_VALUE) {
+            return;
+        }
+        // This is just cleanup to manage memory. We don't need to do it too often or at the exact
+        // intended real time, so the delay that comes from using the Handler (and is limited
+        // to uptime) should be fine.
+        final long delayMs = Math.max(HOUR_IN_MILLIS,
+                earliestEndTime + MAX_TRANSACTION_AGE_MS - System.currentTimeMillis());
+        TareHandlerThread.getHandler().postDelayed(mCleanRunnable, delayMs);
+    }
+
+    private void writeState() {
+        synchronized (mIrs.getLock()) {
+            TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable);
+            // Remove mCleanRunnable callbacks since we're going to clean up the ledgers before
+            // writing anyway.
+            TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable);
+            if (!mIrs.isEnabled()) {
+                // If it's no longer enabled, we would have cleared all the data in memory and would
+                // accidentally write an empty file, thus deleting all the history.
+                return;
+            }
+            long earliestStoredEndTime = Long.MAX_VALUE;
+            try (FileOutputStream fos = mStateFile.startWrite()) {
+                TypedXmlSerializer out = Xml.resolveSerializer(fos);
+                out.startDocument(null, true);
+
+                out.startTag(null, XML_TAG_TARE);
+                out.attributeInt(null, XML_ATTR_VERSION, STATE_FILE_VERSION);
+
+                out.startTag(null, XML_TAG_HIGH_LEVEL_STATE);
+                out.attributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME, mLastReclamationTime);
+                out.endTag(null, XML_TAG_HIGH_LEVEL_STATE);
+
+                for (int uIdx = mLedgers.numMaps() - 1; uIdx >= 0; --uIdx) {
+                    final int userId = mLedgers.keyAt(uIdx);
+                    earliestStoredEndTime = Math.min(earliestStoredEndTime,
+                            writeUserLocked(out, userId));
+                }
+
+                out.endTag(null, XML_TAG_TARE);
+
+                out.endDocument();
+                mStateFile.finishWrite(fos);
+            } catch (IOException e) {
+                Slog.e(TAG, "Error writing state to disk", e);
+            }
+            scheduleCleanup(earliestStoredEndTime);
+        }
+    }
+
+    @GuardedBy("mIrs.getLock()")
+    private long writeUserLocked(@NonNull TypedXmlSerializer out, final int userId)
+            throws IOException {
+        final int uIdx = mLedgers.indexOfKey(userId);
+        long earliestStoredEndTime = Long.MAX_VALUE;
+
+        out.startTag(null, XML_TAG_USER);
+        out.attributeInt(null, XML_ATTR_USER_ID, userId);
+        for (int pIdx = mLedgers.numElementsForKey(userId) - 1; pIdx >= 0; --pIdx) {
+            final String pkgName = mLedgers.keyAt(uIdx, pIdx);
+            final Ledger ledger = mLedgers.get(userId, pkgName);
+            // Remove old transactions so we don't waste space storing them.
+            ledger.removeOldTransactions(MAX_TRANSACTION_AGE_MS);
+
+            out.startTag(null, XML_TAG_LEDGER);
+            out.attribute(null, XML_ATTR_PACKAGE_NAME, pkgName);
+            out.attributeLong(null,
+                    XML_ATTR_CURRENT_BALANCE, ledger.getCurrentBalance());
+
+            final List<Ledger.Transaction> transactions = ledger.getTransactions();
+            for (int t = 0; t < transactions.size(); ++t) {
+                Ledger.Transaction transaction = transactions.get(t);
+                if (t == 0) {
+                    earliestStoredEndTime = Math.min(earliestStoredEndTime, transaction.endTimeMs);
+                }
+                writeTransaction(out, transaction);
+            }
+            out.endTag(null, XML_TAG_LEDGER);
+        }
+        out.endTag(null, XML_TAG_USER);
+
+        return earliestStoredEndTime;
+    }
+
+    private static void writeTransaction(@NonNull TypedXmlSerializer out,
+            @NonNull Ledger.Transaction transaction) throws IOException {
+        out.startTag(null, XML_TAG_TRANSACTION);
+        out.attributeLong(null, XML_ATTR_START_TIME, transaction.startTimeMs);
+        out.attributeLong(null, XML_ATTR_END_TIME, transaction.endTimeMs);
+        out.attributeInt(null, XML_ATTR_EVENT_ID, transaction.eventId);
+        if (transaction.tag != null) {
+            out.attribute(null, XML_ATTR_TAG, transaction.tag);
+        }
+        out.attributeLong(null, XML_ATTR_DELTA, transaction.delta);
+        out.endTag(null, XML_TAG_TRANSACTION);
+    }
+
+    @GuardedBy("mIrs.getLock()")
     void dumpLocked(IndentingPrintWriter pw) {
         pw.println("Ledgers:");
         pw.increaseIndent();
diff --git a/apex/media/OWNERS b/apex/media/OWNERS
index 73f02d3..bed3895 100644
--- a/apex/media/OWNERS
+++ b/apex/media/OWNERS
@@ -8,3 +8,6 @@
 
 # go/android-fwk-media-solutions for info on areas of ownership.
 include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
+
+# media reliability team packages/delivers the media mainline builds.
+include platform/frameworks/av:/media/janitors/reliability_mainline_OWNERS
diff --git a/apex/media/framework/java/android/media/MediaSession2.java b/apex/media/framework/java/android/media/MediaSession2.java
index cb6e1a0..7d07eb3 100644
--- a/apex/media/framework/java/android/media/MediaSession2.java
+++ b/apex/media/framework/java/android/media/MediaSession2.java
@@ -302,8 +302,9 @@
             parcel.setDataPosition(0);
             Bundle out = parcel.readBundle(null);
 
-            // Calling Bundle#size() will trigger Bundle#unparcel().
-            out.size();
+            for (String key : out.keySet()) {
+                out.get(key);
+            }
         } catch (BadParcelableException e) {
             Log.d(TAG, "Custom parcelable in bundle.", e);
             return true;
diff --git a/api/Android.bp b/api/Android.bp
index dbb81a6..7590177 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -437,7 +437,7 @@
 }
 
 genrule {
-    name: "services-system-server-current.txt",
+    name: "frameworks-base-api-system-server-current.txt",
     srcs: [
         ":service-media-s{.system-server.api.txt}",
         ":service-permission{.system-server.api.txt}",
@@ -464,7 +464,7 @@
 }
 
 genrule {
-    name: "services-system-server-removed.txt",
+    name: "frameworks-base-api-system-server-removed.txt",
     srcs: [
         ":service-media-s{.system-server.removed-api.txt}",
         ":service-permission{.system-server.removed-api.txt}",
diff --git a/cmds/idmap2/Android.bp b/cmds/idmap2/Android.bp
index cc45589..c134822 100644
--- a/cmds/idmap2/Android.bp
+++ b/cmds/idmap2/Android.bp
@@ -52,6 +52,7 @@
         "-readability-const-return-type",
         "-readability-convert-member-functions-to-static",
         "-readability-else-after-return",
+        "-readability-identifier-length",
         "-readability-named-parameter",
         "-readability-redundant-access-specifiers",
         "-readability-uppercase-literal-suffix",
diff --git a/core/api/current.txt b/core/api/current.txt
index f8891b4..c60ef30 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -114,6 +114,7 @@
     field public static final String MODIFY_PHONE_STATE = "android.permission.MODIFY_PHONE_STATE";
     field public static final String MOUNT_FORMAT_FILESYSTEMS = "android.permission.MOUNT_FORMAT_FILESYSTEMS";
     field public static final String MOUNT_UNMOUNT_FILESYSTEMS = "android.permission.MOUNT_UNMOUNT_FILESYSTEMS";
+    field public static final String NEARBY_WIFI_DEVICES = "android.permission.NEARBY_WIFI_DEVICES";
     field public static final String NFC = "android.permission.NFC";
     field public static final String NFC_PREFERRED_PAYMENT_INFO = "android.permission.NFC_PREFERRED_PAYMENT_INFO";
     field public static final String NFC_TRANSACTION_EVENT = "android.permission.NFC_TRANSACTION_EVENT";
@@ -1303,7 +1304,7 @@
     field public static final int shortcutLongLabel = 16844074; // 0x101052a
     field public static final int shortcutShortLabel = 16844073; // 0x1010529
     field public static final int shouldDisableView = 16843246; // 0x10101ee
-    field public static final int shouldUseDefaultDisplayStateChangeTransition;
+    field public static final int shouldUseDefaultUnfoldTransition;
     field public static final int showAsAction = 16843481; // 0x10102d9
     field public static final int showDefault = 16843258; // 0x10101fa
     field public static final int showDividers = 16843561; // 0x1010329
@@ -6257,6 +6258,7 @@
     method public android.app.NotificationManager.Policy getNotificationPolicy();
     method public boolean isNotificationListenerAccessGranted(android.content.ComponentName);
     method public boolean isNotificationPolicyAccessGranted();
+    method @WorkerThread public boolean matchesCallFilter(@NonNull android.net.Uri);
     method public void notify(int, android.app.Notification);
     method public void notify(String, int, android.app.Notification);
     method public void notifyAsPackage(@NonNull String, @Nullable String, int, @NonNull android.app.Notification);
@@ -6698,13 +6700,13 @@
   }
 
   public class StatusBarManager {
-    method public int requestAddTileService(@NonNull android.content.ComponentName, @NonNull CharSequence, @NonNull android.graphics.drawable.Icon, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
-    field public static final int TILE_ADD_REQUEST_ANSWER_FAILED_BAD_COMPONENT = 3; // 0x3
-    field public static final int TILE_ADD_REQUEST_ANSWER_FAILED_MISMATCHED_PACKAGE = 1; // 0x1
-    field public static final int TILE_ADD_REQUEST_ANSWER_FAILED_NOT_CURRENT_USER = 4; // 0x4
-    field public static final int TILE_ADD_REQUEST_ANSWER_FAILED_REQUEST_IN_PROGRESS = 2; // 0x2
-    field public static final int TILE_ADD_REQUEST_ANSWER_FAILED_UNKNOWN_REASON = 5; // 0x5
-    field public static final int TILE_ADD_REQUEST_ANSWER_SUCCESS = 0; // 0x0
+    method public void requestAddTileService(@NonNull android.content.ComponentName, @NonNull CharSequence, @NonNull android.graphics.drawable.Icon, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+    field public static final int TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND = 1004; // 0x3ec
+    field public static final int TILE_ADD_REQUEST_ERROR_BAD_COMPONENT = 1002; // 0x3ea
+    field public static final int TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE = 1000; // 0x3e8
+    field public static final int TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER = 1003; // 0x3eb
+    field public static final int TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE = 1005; // 0x3ed
+    field public static final int TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS = 1001; // 0x3e9
     field public static final int TILE_ADD_REQUEST_RESULT_TILE_ADDED = 2; // 0x2
     field public static final int TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED = 1; // 0x1
     field public static final int TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED = 0; // 0x0
@@ -6728,6 +6730,7 @@
   }
 
   public class TaskInfo {
+    method public boolean isVisible();
     field @Nullable public android.content.ComponentName baseActivity;
     field @NonNull public android.content.Intent baseIntent;
     field public boolean isRunning;
@@ -6943,7 +6946,7 @@
     method public android.graphics.drawable.Drawable loadIcon(android.content.pm.PackageManager);
     method public CharSequence loadLabel(android.content.pm.PackageManager);
     method public android.graphics.drawable.Drawable loadThumbnail(android.content.pm.PackageManager);
-    method public boolean shouldUseDefaultDisplayStateChangeTransition();
+    method public boolean shouldUseDefaultUnfoldTransition();
     method public boolean supportsMultipleDisplays();
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.app.WallpaperInfo> CREATOR;
@@ -8055,6 +8058,7 @@
     field public static final int STOP_REASON_CONSTRAINT_DEVICE_IDLE = 8; // 0x8
     field public static final int STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW = 9; // 0x9
     field public static final int STOP_REASON_DEVICE_STATE = 4; // 0x4
+    field public static final int STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED = 15; // 0xf
     field public static final int STOP_REASON_PREEMPT = 2; // 0x2
     field public static final int STOP_REASON_QUOTA = 10; // 0xa
     field public static final int STOP_REASON_SYSTEM_PROCESSING = 14; // 0xe
@@ -10003,6 +10007,7 @@
 
   public static final class AttributionSource.Builder {
     ctor public AttributionSource.Builder(int);
+    ctor public AttributionSource.Builder(@NonNull android.content.AttributionSource);
     method @NonNull public android.content.AttributionSource build();
     method @NonNull public android.content.AttributionSource.Builder setAttributionTag(@Nullable String);
     method @NonNull public android.content.AttributionSource.Builder setNext(@Nullable android.content.AttributionSource);
@@ -13298,6 +13303,7 @@
     method public int describeContents();
     method public int diff(android.content.res.Configuration);
     method public boolean equals(android.content.res.Configuration);
+    method @NonNull public static android.content.res.Configuration generateDelta(@NonNull android.content.res.Configuration, @NonNull android.content.res.Configuration);
     method public int getLayoutDirection();
     method @NonNull public android.os.LocaleList getLocales();
     method public boolean isLayoutSizeAtLeast(int);
@@ -18009,6 +18015,7 @@
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> EDGE_AVAILABLE_EDGE_MODES;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Boolean> FLASH_INFO_AVAILABLE;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES;
+    field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.hardware.camera2.params.DeviceStateSensorOrientationMap> INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> INFO_SUPPORTED_HARDWARE_LEVEL;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.String> INFO_VERSION;
     field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Size[]> JPEG_AVAILABLE_THUMBNAIL_SIZES;
@@ -18703,6 +18710,12 @@
     method public android.util.Rational getElement(int, int);
   }
 
+  public final class DeviceStateSensorOrientationMap {
+    method public int getSensorOrientation(long);
+    field public static final long FOLDED = 4L; // 0x4L
+    field public static final long NORMAL = 0L; // 0x0L
+  }
+
   public final class ExtensionSessionConfiguration {
     ctor public ExtensionSessionConfiguration(int, @NonNull java.util.List<android.hardware.camera2.params.OutputConfiguration>, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraExtensionSession.StateCallback);
     method @NonNull public java.util.concurrent.Executor getExecutor();
@@ -22650,6 +22663,7 @@
     field public static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
     field public static final String KEY_MAX_HEIGHT = "max-height";
     field public static final String KEY_MAX_INPUT_SIZE = "max-input-size";
+    field public static final String KEY_MAX_OUTPUT_CHANNEL_COUNT = "max-output-channel_count";
     field public static final String KEY_MAX_PTS_GAP_TO_ENCODER = "max-pts-gap-to-encoder";
     field public static final String KEY_MAX_WIDTH = "max-width";
     field public static final String KEY_MIME = "mime";
@@ -22822,7 +22836,7 @@
 
   public class MediaMetadataRetriever implements java.lang.AutoCloseable {
     ctor public MediaMetadataRetriever();
-    method public void close();
+    method public void close() throws java.io.IOException;
     method @Nullable public String extractMetadata(int);
     method @Nullable public byte[] getEmbeddedPicture();
     method @Nullable public android.graphics.Bitmap getFrameAtIndex(int, @NonNull android.media.MediaMetadataRetriever.BitmapParams);
@@ -22839,7 +22853,7 @@
     method @Nullable public android.graphics.Bitmap getPrimaryImage();
     method @Nullable public android.graphics.Bitmap getScaledFrameAtTime(long, int, @IntRange(from=1) int, @IntRange(from=1) int);
     method @Nullable public android.graphics.Bitmap getScaledFrameAtTime(long, int, @IntRange(from=1) int, @IntRange(from=1) int, @NonNull android.media.MediaMetadataRetriever.BitmapParams);
-    method public void release();
+    method public void release() throws java.io.IOException;
     method public void setDataSource(String) throws java.lang.IllegalArgumentException;
     method public void setDataSource(String, java.util.Map<java.lang.String,java.lang.String>) throws java.lang.IllegalArgumentException;
     method public void setDataSource(java.io.FileDescriptor, long, long) throws java.lang.IllegalArgumentException;
@@ -25650,6 +25664,7 @@
     field public static final String COLUMN_CONTENT_ID = "content_id";
     field public static final String COLUMN_CONTENT_RATING = "content_rating";
     field public static final String COLUMN_DURATION_MILLIS = "duration_millis";
+    field public static final String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
     field public static final String COLUMN_EPISODE_DISPLAY_NUMBER = "episode_display_number";
     field public static final String COLUMN_EPISODE_TITLE = "episode_title";
     field public static final String COLUMN_INTENT_URI = "intent_uri";
@@ -25680,6 +25695,7 @@
     field public static final String COLUMN_SHORT_DESCRIPTION = "short_description";
     field public static final String COLUMN_SPLIT_ID = "split_id";
     field public static final String COLUMN_STARTING_PRICE = "starting_price";
+    field public static final String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis";
     field public static final String COLUMN_THUMBNAIL_ASPECT_RATIO = "poster_thumbnail_aspect_ratio";
     field public static final String COLUMN_THUMBNAIL_URI = "thumbnail_uri";
     field public static final String COLUMN_TITLE = "title";
@@ -25842,6 +25858,7 @@
     field public static final String COLUMN_CONTENT_ID = "content_id";
     field public static final String COLUMN_CONTENT_RATING = "content_rating";
     field public static final String COLUMN_DURATION_MILLIS = "duration_millis";
+    field public static final String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
     field public static final String COLUMN_EPISODE_DISPLAY_NUMBER = "episode_display_number";
     field public static final String COLUMN_EPISODE_TITLE = "episode_title";
     field public static final String COLUMN_INTENT_URI = "intent_uri";
@@ -25873,6 +25890,7 @@
     field public static final String COLUMN_SHORT_DESCRIPTION = "short_description";
     field public static final String COLUMN_SPLIT_ID = "split_id";
     field public static final String COLUMN_STARTING_PRICE = "starting_price";
+    field public static final String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis";
     field public static final String COLUMN_THUMBNAIL_ASPECT_RATIO = "poster_thumbnail_aspect_ratio";
     field public static final String COLUMN_THUMBNAIL_URI = "thumbnail_uri";
     field public static final String COLUMN_TITLE = "title";
@@ -31557,6 +31575,7 @@
     method public int dataSize();
     method public void enforceInterface(@NonNull String);
     method public boolean hasFileDescriptors();
+    method public boolean hasFileDescriptors(int, int);
     method public byte[] marshall();
     method @NonNull public static android.os.Parcel obtain();
     method @NonNull public static android.os.Parcel obtain(@NonNull android.os.IBinder);
@@ -32155,6 +32174,7 @@
     field public static final String DISALLOW_BLUETOOTH = "no_bluetooth";
     field public static final String DISALLOW_BLUETOOTH_SHARING = "no_bluetooth_sharing";
     field public static final String DISALLOW_CAMERA_TOGGLE = "disallow_camera_toggle";
+    field public static final String DISALLOW_CHANGE_WIFI_STATE = "no_change_wifi_state";
     field public static final String DISALLOW_CONFIG_BLUETOOTH = "no_config_bluetooth";
     field public static final String DISALLOW_CONFIG_BRIGHTNESS = "no_config_brightness";
     field public static final String DISALLOW_CONFIG_CELL_BROADCASTS = "no_config_cell_broadcasts";
@@ -34948,6 +34968,7 @@
   }
 
   public static final class ContactsContract.Settings implements android.provider.ContactsContract.SettingsColumns {
+    method @Nullable public static android.accounts.Account getDefaultAccount(@NonNull android.content.ContentResolver);
     field public static final String ACTION_SET_DEFAULT_ACCOUNT = "android.provider.action.SET_DEFAULT_ACCOUNT";
     field public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/setting";
     field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/setting";
@@ -39762,6 +39783,7 @@
     field public static final int CAPABILITY_MANAGE_CONFERENCE = 128; // 0x80
     field public static final int CAPABILITY_MERGE_CONFERENCE = 4; // 0x4
     field public static final int CAPABILITY_MUTE = 64; // 0x40
+    field public static final int CAPABILITY_REMOTE_PARTY_SUPPORTS_RTT = 268435456; // 0x10000000
     field public static final int CAPABILITY_RESPOND_VIA_TEXT = 32; // 0x20
     field public static final int CAPABILITY_SEPARATE_FROM_CONFERENCE = 4096; // 0x1000
     field public static final int CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL = 768; // 0x300
@@ -40048,6 +40070,7 @@
     field public static final int CAPABILITY_MANAGE_CONFERENCE = 128; // 0x80
     field public static final int CAPABILITY_MERGE_CONFERENCE = 4; // 0x4
     field public static final int CAPABILITY_MUTE = 64; // 0x40
+    field public static final int CAPABILITY_REMOTE_PARTY_SUPPORTS_RTT = 536870912; // 0x20000000
     field public static final int CAPABILITY_RESPOND_VIA_TEXT = 32; // 0x20
     field public static final int CAPABILITY_SEPARATE_FROM_CONFERENCE = 4096; // 0x1000
     field public static final int CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL = 768; // 0x300
@@ -45754,7 +45777,7 @@
     method public static final boolean addLinks(@NonNull android.text.Spannable, @NonNull java.util.regex.Pattern, @Nullable String, @Nullable android.text.util.Linkify.MatchFilter, @Nullable android.text.util.Linkify.TransformFilter);
     method public static final boolean addLinks(@NonNull android.text.Spannable, @NonNull java.util.regex.Pattern, @Nullable String, @Nullable String[], @Nullable android.text.util.Linkify.MatchFilter, @Nullable android.text.util.Linkify.TransformFilter);
     method public static final boolean addLinks(@NonNull android.text.Spannable, @NonNull java.util.regex.Pattern, @Nullable String, @Nullable String[], @Nullable android.text.util.Linkify.MatchFilter, @Nullable android.text.util.Linkify.TransformFilter, @Nullable java.util.function.Function<java.lang.String,android.text.style.URLSpan>);
-    field public static final int ALL = 15; // 0xf
+    field @Deprecated public static final int ALL = 15; // 0xf
     field public static final int EMAIL_ADDRESSES = 2; // 0x2
     field @Deprecated public static final int MAP_ADDRESSES = 8; // 0x8
     field public static final int PHONE_NUMBERS = 4; // 0x4
@@ -47020,15 +47043,15 @@
   }
 
   @UiThread public interface AttachedSurfaceControl {
-    method public default void addOnSurfaceTransformHintChangedListener(@NonNull android.view.AttachedSurfaceControl.OnSurfaceTransformHintChangedListener);
+    method public default void addOnBufferTransformHintChangedListener(@NonNull android.view.AttachedSurfaceControl.OnBufferTransformHintChangedListener);
     method public boolean applyTransactionOnDraw(@NonNull android.view.SurfaceControl.Transaction);
     method @Nullable public android.view.SurfaceControl.Transaction buildReparentTransaction(@NonNull android.view.SurfaceControl);
-    method public default int getSurfaceTransformHint();
-    method public default void removeOnSurfaceTransformHintChangedListener(@NonNull android.view.AttachedSurfaceControl.OnSurfaceTransformHintChangedListener);
+    method public default int getBufferTransformHint();
+    method public default void removeOnBufferTransformHintChangedListener(@NonNull android.view.AttachedSurfaceControl.OnBufferTransformHintChangedListener);
   }
 
-  @UiThread public static interface AttachedSurfaceControl.OnSurfaceTransformHintChangedListener {
-    method public void onSurfaceTransformHintChanged(int);
+  @UiThread public static interface AttachedSurfaceControl.OnBufferTransformHintChangedListener {
+    method public void onBufferTransformHintChanged(int);
   }
 
   public final class Choreographer {
@@ -48518,6 +48541,12 @@
     method public void readFromParcel(android.os.Parcel);
     method public void release();
     method public void writeToParcel(android.os.Parcel, int);
+    field public static final int BUFFER_TRANSFORM_IDENTITY = 0; // 0x0
+    field public static final int BUFFER_TRANSFORM_MIRROR_HORIZONTAL = 1; // 0x1
+    field public static final int BUFFER_TRANSFORM_MIRROR_VERTICAL = 2; // 0x2
+    field public static final int BUFFER_TRANSFORM_ROTATE_180 = 3; // 0x3
+    field public static final int BUFFER_TRANSFORM_ROTATE_270 = 7; // 0x7
+    field public static final int BUFFER_TRANSFORM_ROTATE_90 = 4; // 0x4
     field @NonNull public static final android.os.Parcelable.Creator<android.view.SurfaceControl> CREATOR;
   }
 
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 64d4456..368722e 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -55,6 +55,15 @@
 
 }
 
+package android.app.admin {
+
+  public class DevicePolicyManager {
+    method @RequiresPermission(android.Manifest.permission.MANAGE_USERS) public void acknowledgeNewUserDisclaimer();
+    field public static final String ACTION_SHOW_NEW_USER_DISCLAIMER = "android.app.action.SHOW_NEW_USER_DISCLAIMER";
+  }
+
+}
+
 package android.app.usage {
 
   public class NetworkStatsManager {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 9e47e96..68ad40f 100755
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -262,6 +262,7 @@
     field public static final String SERIAL_PORT = "android.permission.SERIAL_PORT";
     field public static final String SET_ACTIVITY_WATCHER = "android.permission.SET_ACTIVITY_WATCHER";
     field public static final String SET_CLIP_SOURCE = "android.permission.SET_CLIP_SOURCE";
+    field public static final String SET_DEFAULT_ACCOUNT_FOR_CONTACTS = "android.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS";
     field public static final String SET_HARMFUL_APP_WARNINGS = "android.permission.SET_HARMFUL_APP_WARNINGS";
     field public static final String SET_MEDIA_KEY_LISTENER = "android.permission.SET_MEDIA_KEY_LISTENER";
     field public static final String SET_ORIENTATION = "android.permission.SET_ORIENTATION";
@@ -2132,6 +2133,7 @@
     field @NonNull public static final android.os.ParcelUuid AVRCP_TARGET;
     field @NonNull public static final android.os.ParcelUuid BASE_UUID;
     field @NonNull public static final android.os.ParcelUuid BNEP;
+    field @NonNull public static final android.os.ParcelUuid CAP;
     field @NonNull public static final android.os.ParcelUuid COORDINATED_SET;
     field @NonNull public static final android.os.ParcelUuid DIP;
     field @NonNull public static final android.os.ParcelUuid GENERIC_MEDIA_CONTROL;
@@ -2425,6 +2427,7 @@
     field @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public static final String ACTION_MANAGE_DEFAULT_APP = "android.intent.action.MANAGE_DEFAULT_APP";
     field public static final String ACTION_MANAGE_PERMISSIONS = "android.intent.action.MANAGE_PERMISSIONS";
     field public static final String ACTION_MANAGE_PERMISSION_APPS = "android.intent.action.MANAGE_PERMISSION_APPS";
+    field @RequiresPermission(android.Manifest.permission.START_VIEW_PERMISSION_USAGE) public static final String ACTION_MANAGE_PERMISSION_USAGE = "android.intent.action.MANAGE_PERMISSION_USAGE";
     field @RequiresPermission(android.Manifest.permission.MANAGE_ROLE_HOLDERS) public static final String ACTION_MANAGE_SPECIAL_APP_ACCESSES = "android.intent.action.MANAGE_SPECIAL_APP_ACCESSES";
     field public static final String ACTION_MASTER_CLEAR_NOTIFICATION = "android.intent.action.MASTER_CLEAR_NOTIFICATION";
     field public static final String ACTION_PACKAGE_NEEDS_INTEGRITY_VERIFICATION = "android.intent.action.PACKAGE_NEEDS_INTEGRITY_VERIFICATION";
@@ -3325,14 +3328,14 @@
 
   public abstract class HdmiClient {
     method public android.hardware.hdmi.HdmiDeviceInfo getActiveSource();
-    method public void selectDevice(int, @NonNull android.hardware.hdmi.HdmiClient.SelectDeviceCallback);
+    method public void selectDevice(int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.hdmi.HdmiClient.OnDeviceSelectedListener);
     method public void sendKeyEvent(int, boolean);
     method public void sendVendorCommand(int, byte[], boolean);
     method public void setVendorCommandListener(@NonNull android.hardware.hdmi.HdmiControlManager.VendorCommandListener);
   }
 
-  public static interface HdmiClient.SelectDeviceCallback {
-    method public void onComplete(@android.hardware.hdmi.HdmiControlManager.ControlCallbackResult int, int);
+  public static interface HdmiClient.OnDeviceSelectedListener {
+    method public void onDeviceSelected(@android.hardware.hdmi.HdmiControlManager.ControlCallbackResult int, int);
   }
 
   public final class HdmiControlManager {
@@ -5446,10 +5449,12 @@
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void addCompatibleAudioDevice(@NonNull android.media.AudioDeviceAttributes);
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void addOnHeadTrackingModeChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnHeadTrackingModeChangedListener);
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void clearOnHeadToSoundstagePoseUpdatedListener();
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void clearOnSpatializerOutputChangedListener();
     method @NonNull @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public java.util.List<android.media.AudioDeviceAttributes> getCompatibleAudioDevices();
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getDesiredHeadTrackingMode();
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void getEffectParameter(int, @NonNull byte[]);
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getHeadTrackingMode();
+    method @IntRange(from=0) @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getOutput();
     method @NonNull @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public java.util.List<java.lang.Integer> getSupportedHeadTrackingModes();
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void recenterHeadTracker();
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void removeCompatibleAudioDevice(@NonNull android.media.AudioDeviceAttributes);
@@ -5459,6 +5464,7 @@
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setEnabled(boolean);
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setGlobalTransform(@NonNull float[]);
     method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setOnHeadToSoundstagePoseUpdatedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnHeadToSoundstagePoseUpdatedListener);
+    method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setOnSpatializerOutputChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnSpatializerOutputChangedListener);
     field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_DISABLED = -1; // 0xffffffff
     field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_OTHER = 0; // 0x0
     field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_RELATIVE_DEVICE = 2; // 0x2
@@ -5475,6 +5481,10 @@
     method public void onHeadTrackingModeChanged(@NonNull android.media.Spatializer, int);
   }
 
+  public static interface Spatializer.OnSpatializerOutputChangedListener {
+    method public void onSpatializerOutputChanged(@NonNull android.media.Spatializer, @IntRange(from=0) int);
+  }
+
 }
 
 package android.media.audiofx {
@@ -9181,6 +9191,10 @@
     field @Deprecated public static final String STATE = "state";
   }
 
+  public static final class ContactsContract.Settings implements android.provider.ContactsContract.SettingsColumns {
+    method @RequiresPermission(android.Manifest.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS) public static void setDefaultAccount(@NonNull android.content.ContentResolver, @Nullable android.accounts.Account);
+  }
+
   public static final class ContactsContract.SimContacts {
     method @RequiresPermission("android.contacts.permission.MANAGE_SIM_ACCOUNTS") public static void addSimAccount(@NonNull android.content.ContentResolver, @NonNull String, @NonNull String, int, int);
     method @RequiresPermission("android.contacts.permission.MANAGE_SIM_ACCOUNTS") public static void removeSimAccounts(@NonNull android.content.ContentResolver, int);
@@ -9211,6 +9225,7 @@
     field public static final String NAMESPACE_BIOMETRICS = "biometrics";
     field public static final String NAMESPACE_BLOBSTORE = "blobstore";
     field public static final String NAMESPACE_BLUETOOTH = "bluetooth";
+    field public static final String NAMESPACE_CAPTIVEPORTALLOGIN = "captive_portal_login";
     field public static final String NAMESPACE_CLIPBOARD = "clipboard";
     field public static final String NAMESPACE_CONNECTIVITY = "connectivity";
     field public static final String NAMESPACE_CONTENT_CAPTURE = "content_capture";
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index b14b6d7..372ad9d 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -26,6 +26,7 @@
     field public static final String MANAGE_ROLLBACKS = "android.permission.MANAGE_ROLLBACKS";
     field public static final String MANAGE_TOAST_RATE_LIMITING = "android.permission.MANAGE_TOAST_RATE_LIMITING";
     field public static final String MODIFY_REFRESH_RATE_SWITCHING_TYPE = "android.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE";
+    field public static final String MODIFY_USER_PREFERRED_DISPLAY_MODE = "android.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE";
     field public static final String NETWORK_SETTINGS = "android.permission.NETWORK_SETTINGS";
     field public static final String NETWORK_STACK = "android.permission.NETWORK_STACK";
     field public static final String OVERRIDE_DISPLAY_MODE_REQUESTS = "android.permission.OVERRIDE_DISPLAY_MODE_REQUESTS";
@@ -304,10 +305,10 @@
 
   public class NotificationManager {
     method public void allowAssistantAdjustment(String);
+    method public void cleanUpCallersAfter(long);
     method public void disallowAssistantAdjustment(String);
     method public android.content.ComponentName getEffectsSuppressor();
     method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String);
-    method public boolean matchesCallFilter(android.os.Bundle);
     method @RequiresPermission(android.Manifest.permission.MANAGE_NOTIFICATION_LISTENERS) public void setNotificationListenerAccessGranted(@NonNull android.content.ComponentName, boolean, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_TOAST_RATE_LIMITING) public void setToastRateLimitingEnabled(boolean);
     method public void updateNotificationChannel(@NonNull String, int, @NonNull android.app.NotificationChannel);
@@ -3220,13 +3221,6 @@
     method @Nullable public android.view.View getBrandingView();
   }
 
-  public final class StartingWindowInfo implements android.os.Parcelable {
-    ctor public StartingWindowInfo();
-    method public int describeContents();
-    method public void writeToParcel(@NonNull android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.window.StartingWindowInfo> CREATOR;
-  }
-
   public final class TaskAppearedInfo implements android.os.Parcelable {
     ctor public TaskAppearedInfo(@NonNull android.app.ActivityManager.RunningTaskInfo, @NonNull android.view.SurfaceControl);
     method public int describeContents();
@@ -3293,7 +3287,6 @@
 
   public class TaskOrganizer extends android.window.WindowOrganizer {
     ctor public TaskOrganizer();
-    method @BinderThread public void addStartingWindow(@NonNull android.window.StartingWindowInfo, @NonNull android.os.IBinder);
     method @BinderThread public void copySplashScreenView(int);
     method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void createRootTask(int, int, @Nullable android.os.IBinder);
     method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public boolean deleteRootTask(@NonNull android.window.WindowContainerToken);
@@ -3306,7 +3299,6 @@
     method @BinderThread public void onTaskInfoChanged(@NonNull android.app.ActivityManager.RunningTaskInfo);
     method @BinderThread public void onTaskVanished(@NonNull android.app.ActivityManager.RunningTaskInfo);
     method @CallSuper @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public java.util.List<android.window.TaskAppearedInfo> registerOrganizer();
-    method @BinderThread public void removeStartingWindow(int, @Nullable android.view.SurfaceControl, @Nullable android.graphics.Rect, boolean);
     method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void setInterceptBackPressedOnTaskRoot(@NonNull android.window.WindowContainerToken, boolean);
     method @CallSuper @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void unregisterOrganizer();
   }
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index a28a34b..11a2ede 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -76,7 +76,6 @@
 import android.os.Looper;
 import android.os.Parcelable;
 import android.os.PersistableBundle;
-import android.os.PowerManager;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager.ServiceNotFoundException;
@@ -138,7 +137,6 @@
 import android.widget.Toast;
 import android.widget.Toolbar;
 import android.window.SplashScreen;
-import android.window.SplashScreenView;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -955,7 +953,6 @@
     private UiTranslationController mUiTranslationController;
 
     private SplashScreen mSplashScreen;
-    private SplashScreenView mSplashScreenView;
 
     private final WindowControllerCallback mWindowControllerCallback =
             new WindowControllerCallback() {
@@ -1615,16 +1612,6 @@
         }
     }
 
-    /** @hide */
-    public void setSplashScreenView(SplashScreenView v) {
-        mSplashScreenView = v;
-    }
-
-    /** @hide */
-    SplashScreenView getSplashScreenView() {
-        return mSplashScreenView;
-    }
-
     /**
      * Same as {@link #onCreate(android.os.Bundle)} but called for those activities created with
      * the attribute {@link android.R.attr#persistableMode} set to
@@ -8486,9 +8473,7 @@
      * the activity is visible after the screen is turned on when the lockscreen is up. In addition,
      * if this flag is set and the activity calls {@link
      * KeyguardManager#requestDismissKeyguard(Activity, KeyguardManager.KeyguardDismissCallback)}
-     * the screen will turn on. If the screen is off and device is not secured, this flag can turn
-     * screen on and dismiss keyguard to make this activity visible and resume, which can be used to
-     * replace {@link PowerManager#ACQUIRE_CAUSES_WAKEUP}
+     * the screen will turn on.
      *
      * @param turnScreenOn {@code true} to turn on the screen; {@code false} otherwise.
      *
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 6ebcd81..19223f9 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -166,6 +166,7 @@
 import android.view.Display;
 import android.view.DisplayAdjustments;
 import android.view.DisplayAdjustments.FixedRotationAdjustments;
+import android.view.SurfaceControl;
 import android.view.ThreadedRenderer;
 import android.view.View;
 import android.view.ViewDebug;
@@ -235,7 +236,6 @@
 import java.util.Objects;
 import java.util.TimeZone;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 
 /**
@@ -3893,7 +3893,6 @@
                         Intent intent = new Intent(activityIntent);
                         intent.setFlags(intent.getFlags() & ~(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                                 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION));
-                        intent.removeUnsafeExtras();
                         content.setDefaultIntent(intent);
                     }
                 } else {
@@ -4074,10 +4073,11 @@
 
     @Override
     public void handleAttachSplashScreenView(@NonNull ActivityClientRecord r,
-            @Nullable SplashScreenView.SplashScreenViewParcelable parcelable) {
+            @Nullable SplashScreenView.SplashScreenViewParcelable parcelable,
+            @NonNull SurfaceControl startingWindowLeash) {
         final DecorView decorView = (DecorView) r.window.peekDecorView();
         if (parcelable != null && decorView != null) {
-            createSplashScreen(r, decorView, parcelable);
+            createSplashScreen(r, decorView, parcelable, startingWindowLeash);
         } else {
             // shouldn't happen!
             Slog.e(TAG, "handleAttachSplashScreenView failed, unable to attach");
@@ -4085,61 +4085,50 @@
     }
 
     private void createSplashScreen(ActivityClientRecord r, DecorView decorView,
-            SplashScreenView.SplashScreenViewParcelable parcelable) {
+            SplashScreenView.SplashScreenViewParcelable parcelable,
+            @NonNull SurfaceControl startingWindowLeash) {
         final SplashScreenView.Builder builder = new SplashScreenView.Builder(r.activity);
         final SplashScreenView view = builder.createFromParcel(parcelable).build();
         decorView.addView(view);
         view.attachHostActivityAndSetSystemUIColors(r.activity, r.window);
         view.requestLayout();
-        // Ensure splash screen view is shown before remove the splash screen window.
-        final ViewRootImpl impl = decorView.getViewRootImpl();
-        final boolean hardwareEnabled = impl != null && impl.isHardwareEnabled();
-        final AtomicBoolean notified = new AtomicBoolean();
-        if (hardwareEnabled) {
-            final Runnable frameCommit = new Runnable() {
-                        @Override
-                        public void run() {
-                            view.post(() -> {
-                                if (!notified.get()) {
-                                    view.getViewTreeObserver().unregisterFrameCommitCallback(this);
-                                    ActivityClient.getInstance().reportSplashScreenAttached(
-                                            r.token);
-                                    notified.set(true);
-                                }
-                            });
-                        }
-                    };
-            view.getViewTreeObserver().registerFrameCommitCallback(frameCommit);
-        } else {
-            final ViewTreeObserver.OnDrawListener onDrawListener =
-                    new ViewTreeObserver.OnDrawListener() {
-                        @Override
-                        public void onDraw() {
-                            view.post(() -> {
-                                if (!notified.get()) {
-                                    view.getViewTreeObserver().removeOnDrawListener(this);
-                                    ActivityClient.getInstance().reportSplashScreenAttached(
-                                            r.token);
-                                    notified.set(true);
-                                }
-                            });
-                        }
-                    };
-            view.getViewTreeObserver().addOnDrawListener(onDrawListener);
+
+        view.getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
+            @Override
+            public void onDraw() {
+                // Transfer the splash screen view from shell to client.
+                // Call syncTransferSplashscreenViewTransaction at the first onDraw so we can ensure
+                // the client view is ready to show and we can use applyTransactionOnDraw to make
+                // all transitions happen at the same frame.
+                syncTransferSplashscreenViewTransaction(
+                        view, r.token, decorView, startingWindowLeash);
+                view.postOnAnimation(() -> view.getViewTreeObserver().removeOnDrawListener(this));
+            }
+        });
+    }
+
+    private void reportSplashscreenViewShown(IBinder token, SplashScreenView view) {
+        ActivityClient.getInstance().reportSplashScreenAttached(token);
+        synchronized (this) {
+            if (mSplashScreenGlobal != null) {
+                mSplashScreenGlobal.handOverSplashScreenView(token, view);
+            }
         }
     }
 
-    @Override
-    public void handOverSplashScreenView(@NonNull ActivityClientRecord r) {
-        final SplashScreenView v = r.activity.getSplashScreenView();
-        if (v == null) {
-            return;
-        }
-        synchronized (this) {
-            if (mSplashScreenGlobal != null) {
-                mSplashScreenGlobal.handOverSplashScreenView(r.token, v);
-            }
-        }
+    private void syncTransferSplashscreenViewTransaction(SplashScreenView view, IBinder token,
+            View decorView, @NonNull SurfaceControl startingWindowLeash) {
+        // Ensure splash screen view is shown before remove the splash screen window.
+        // Once the copied splash screen view is onDrawn on decor view, use applyTransactionOnDraw
+        // to ensure the transfer of surface view and hide starting window are happen at the same
+        // frame.
+        final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+        transaction.hide(startingWindowLeash);
+
+        decorView.getViewRootImpl().applyTransactionOnDraw(transaction);
+        view.syncTransferSurfaceOnDraw();
+        // Tell server we can remove the starting window
+        decorView.postOnAnimation(() -> reportSplashscreenViewShown(token, view));
     }
 
     /**
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index a17a89b..773694c 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1292,6 +1292,9 @@
     /** @hide */
     public static final int OP_UWB_RANGING = AppProtoEnums.APP_OP_UWB_RANGING;
 
+    /** @hide */
+    public static final int OP_NEARBY_WIFI_DEVICES = AppProtoEnums.APP_OP_NEARBY_WIFI_DEVICES;
+
     /**
      * Activity recognition being accessed by an activity recognition source, which
      * is a component that already has access since it is the one that detects
@@ -1312,7 +1315,7 @@
 
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 116;
+    public static final int _NUM_OP = 117;
 
     /** Access to coarse location information. */
     public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -1731,6 +1734,8 @@
     public static final String OPSTR_MANAGE_MEDIA = "android:manage_media";
     /** @hide */
     public static final String OPSTR_UWB_RANGING = "android:uwb_ranging";
+    /** @hide */
+    public static final String OPSTR_NEARBY_WIFI_DEVICES = "android:nearby_wifi_devices";
 
     /**
      * Activity recognition being accessed by an activity recognition source, which
@@ -1819,6 +1824,7 @@
             OP_BLUETOOTH_CONNECT,
             OP_BLUETOOTH_ADVERTISE,
             OP_UWB_RANGING,
+            OP_NEARBY_WIFI_DEVICES,
             // Notifications
             OP_POST_NOTIFICATION,
 
@@ -1965,6 +1971,7 @@
             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
     };
 
     /**
@@ -2087,6 +2094,7 @@
             OPSTR_ACTIVITY_RECOGNITION_SOURCE,
             OPSTR_BLUETOOTH_ADVERTISE,
             OPSTR_RECORD_INCOMING_PHONE_AUDIO,
+            OPSTR_NEARBY_WIFI_DEVICES,
     };
 
     /**
@@ -2210,6 +2218,7 @@
             "ACTIVITY_RECOGNITION_SOURCE",
             "BLUETOOTH_ADVERTISE",
             "RECORD_INCOMING_PHONE_AUDIO",
+            "NEARBY_WIFI_DEVICES"
     };
 
     /**
@@ -2334,6 +2343,7 @@
             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,
     };
 
     /**
@@ -2458,6 +2468,7 @@
             null, // ACTIVITY_RECOGNITION_SOURCE
             null, // BLUETOOTH_ADVERTISE
             null, // RECORD_INCOMING_PHONE_AUDIO
+            null, // NEARBY_WIFI_DEVICES
     };
 
     /**
@@ -2581,6 +2592,7 @@
             null, // ACTIVITY_RECOGNITION_SOURCE
             null, // BLUETOOTH_ADVERTISE
             null, // RECORD_INCOMING_PHONE_AUDIO
+            null, // NEARBY_WIFI_DEVICES
     };
 
     /**
@@ -2703,6 +2715,7 @@
             AppOpsManager.MODE_ALLOWED, // ACTIVITY_RECOGNITION_SOURCE
             AppOpsManager.MODE_ALLOWED, // BLUETOOTH_ADVERTISE
             AppOpsManager.MODE_ALLOWED, // RECORD_INCOMING_PHONE_AUDIO
+            AppOpsManager.MODE_ALLOWED, // NEARBY_WIFI_DEVICES
     };
 
     /**
@@ -2829,6 +2842,7 @@
             false, // ACTIVITY_RECOGNITION_SOURCE
             false, // BLUETOOTH_ADVERTISE
             false, // RECORD_INCOMING_PHONE_AUDIO
+            false, // NEARBY_WIFI_DEVICES
     };
 
     /**
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index 115101c..c743f65 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -28,6 +28,7 @@
 import android.os.IBinder;
 import android.util.MergedConfiguration;
 import android.view.DisplayAdjustments.FixedRotationAdjustments;
+import android.view.SurfaceControl;
 import android.window.SplashScreenView.SplashScreenViewParcelable;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -165,10 +166,8 @@
 
     /** Attach a splash screen window view to the top of the activity */
     public abstract void handleAttachSplashScreenView(@NonNull ActivityClientRecord r,
-            @NonNull SplashScreenViewParcelable parcelable);
-
-    /** Hand over the splash screen window view to the activity */
-    public abstract void handOverSplashScreenView(@NonNull ActivityClientRecord r);
+            @NonNull SplashScreenViewParcelable parcelable,
+            @NonNull SurfaceControl startingWindowLeash);
 
     /** Perform activity launch. */
     public abstract Activity handleLaunchActivity(@NonNull ActivityClientRecord r,
diff --git a/core/java/android/app/ILocaleManager.aidl b/core/java/android/app/ILocaleManager.aidl
new file mode 100644
index 0000000..348cb2d
--- /dev/null
+++ b/core/java/android/app/ILocaleManager.aidl
@@ -0,0 +1,43 @@
+
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.os.LocaleList;
+
+/**
+ * Internal interface used to control app-specific locales.
+ *
+ * <p>Use the {@link android.app.LocaleManager} class rather than going through
+ * this Binder interface directly. See {@link android.app.LocaleManager} for
+ * more complete documentation.
+ *
+ * @hide
+ */
+ interface ILocaleManager {
+
+     /**
+      * Sets a specified app’s app-specific UI locales.
+      */
+     void setApplicationLocales(String packageName, int userId, in LocaleList locales);
+
+     /**
+      * Returns the specified app's app-specific locales.
+      */
+     LocaleList getApplicationLocales(String packageName, int userId);
+
+ }
\ No newline at end of file
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 098492c..01885b2 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -175,6 +175,7 @@
 
     ComponentName getEffectsSuppressor();
     boolean matchesCallFilter(in Bundle extras);
+    void cleanUpCallersAfter(long timeThreshold);
     boolean isSystemConditionProviderEnabled(String path);
 
     boolean isNotificationListenerAccessGranted(in ComponentName listener);
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index ccf1edb..9be4adc 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -25,6 +25,7 @@
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
+import android.annotation.WorkerThread;
 import android.app.Notification.Builder;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
@@ -1079,7 +1080,6 @@
     /**
      * @hide
      */
-    @TestApi
     public boolean matchesCallFilter(Bundle extras) {
         INotificationManager service = getService();
         try {
@@ -1092,6 +1092,19 @@
     /**
      * @hide
      */
+    @TestApi
+    public void cleanUpCallersAfter(long timeThreshold) {
+        INotificationManager service = getService();
+        try {
+            service.cleanUpCallersAfter(timeThreshold);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     */
     public boolean isSystemConditionProviderEnabled(String path) {
         INotificationManager service = getService();
         try {
@@ -2544,6 +2557,46 @@
         }
     }
 
+    /**
+     * Returns whether a call from the provided URI is permitted to notify the user.
+     * <p>
+     * A true return value indicates one of the following: Do Not Disturb is not currently active;
+     * or the caller is a repeat caller and the current policy allows interruptions from repeat
+     * callers; or the caller is in the user's set of contacts whose calls are allowed to interrupt
+     * Do Not Disturb.
+     * </p>
+     * <p>
+     * If Do Not Disturb is enabled and either no interruptions or only alarms are allowed, this
+     * method will return false regardless of input.
+     * </p>
+     * <p>
+     * The provided URI must meet the requirements for a URI associated with a
+     * {@link Person}: it may be the {@code String} representation of a
+     * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}, or a
+     * <code>mailto:</code> or <code>tel:</code> schema URI matching an entry in the
+     * Contacts database. See also {@link Person.Builder#setUri} and
+     * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}
+     * for more information.
+     * </p>
+     * <p>
+     * NOTE: This method calls into Contacts, which may take some time, and should not be called
+     * on the main thread.
+     * </p>
+     *
+     * @param uri A URI representing a caller. Must not be null.
+     * @return A boolean indicating whether a call from the URI provided would be allowed to
+     *         interrupt the user given the current filter.
+     */
+    @WorkerThread
+    public boolean matchesCallFilter(@NonNull Uri uri) {
+        Bundle extras = new Bundle();
+        ArrayList<Person> pList = new ArrayList<>();
+        pList.add(new Person.Builder().setUri(uri.toString()).build());
+        extras.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, pList);
+
+        return matchesCallFilter(extras);
+    }
+
     /** @hide */
     public static int zenModeToInterruptionFilter(int zen) {
         switch (zen) {
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index ee50634..64f89bc 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -228,55 +228,59 @@
     /** @hide */
     public static final int TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED = 3;
 
-    /** @hide */
-    @IntDef(prefix = {"TILE_ADD_REQUEST_RESULT_"}, value = {
-            TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED,
-            TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED,
-            TILE_ADD_REQUEST_RESULT_TILE_ADDED,
-            TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface RequestResult {}
-
     /**
-     * Indicates that the request was sent successfully. Does not indicate that the user
-     * has accepted the request.
+     * Values greater or equal to this value indicate an error in the request.
      */
-    public static final int TILE_ADD_REQUEST_ANSWER_SUCCESS = 0;
+    private static final int TILE_ADD_REQUEST_FIRST_ERROR_CODE = 1000;
+
     /**
      * Indicates that this package does not match that of the
      * {@link android.service.quicksettings.TileService}.
      */
-    public static final int TILE_ADD_REQUEST_ANSWER_FAILED_MISMATCHED_PACKAGE = 1;
+    public static final int TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE =
+            TILE_ADD_REQUEST_FIRST_ERROR_CODE;
     /**
      * Indicates that there's a request in progress for this package.
      */
-    public static final int TILE_ADD_REQUEST_ANSWER_FAILED_REQUEST_IN_PROGRESS = 2;
+    public static final int TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS =
+            TILE_ADD_REQUEST_FIRST_ERROR_CODE + 1;
     /**
-     * Indicates that the component does not match an enabled
+     * Indicates that the component does not match an enabled exported
      * {@link android.service.quicksettings.TileService} for the current user.
      */
-    public static final int TILE_ADD_REQUEST_ANSWER_FAILED_BAD_COMPONENT = 3;
+    public static final int TILE_ADD_REQUEST_ERROR_BAD_COMPONENT =
+            TILE_ADD_REQUEST_FIRST_ERROR_CODE + 2;
     /**
      * Indicates that the user is not the current user.
      */
-    public static final int TILE_ADD_REQUEST_ANSWER_FAILED_NOT_CURRENT_USER = 4;
+    public static final int TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER =
+            TILE_ADD_REQUEST_FIRST_ERROR_CODE + 3;
     /**
-     * The request could not be processed due to an unkonwn reason.
+     * Indicates that the requesting application is not in the foreground.
      */
-    public static final int TILE_ADD_REQUEST_ANSWER_FAILED_UNKNOWN_REASON = 5;
+    public static final int TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND =
+            TILE_ADD_REQUEST_FIRST_ERROR_CODE + 4;
+    /**
+     * The request could not be processed because no fulfilling service was found. This could be
+     * a temporary issue (for example, SystemUI has crashed).
+     */
+    public static final int TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE =
+            TILE_ADD_REQUEST_FIRST_ERROR_CODE + 5;
 
     /** @hide */
-    @IntDef(prefix = {"TILE_ADD_REQUEST_ANSWER_"}, value = {
-            TILE_ADD_REQUEST_ANSWER_SUCCESS,
-            TILE_ADD_REQUEST_ANSWER_FAILED_MISMATCHED_PACKAGE,
-            TILE_ADD_REQUEST_ANSWER_FAILED_REQUEST_IN_PROGRESS,
-            TILE_ADD_REQUEST_ANSWER_FAILED_BAD_COMPONENT,
-            TILE_ADD_REQUEST_ANSWER_FAILED_NOT_CURRENT_USER,
-            TILE_ADD_REQUEST_ANSWER_FAILED_UNKNOWN_REASON
+    @IntDef(prefix = {"TILE_ADD_REQUEST"}, value = {
+            TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED,
+            TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED,
+            TILE_ADD_REQUEST_RESULT_TILE_ADDED,
+            TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE,
+            TILE_ADD_REQUEST_ERROR_REQUEST_IN_PROGRESS,
+            TILE_ADD_REQUEST_ERROR_BAD_COMPONENT,
+            TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER,
+            TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND,
+            TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface RequestAnswer {}
+    public @interface RequestResult {}
 
     @UnsupportedAppUsage
     private Context mContext;
@@ -618,7 +622,9 @@
      * </ul>
      * <p>
      * The user for which this will be added is determined from the {@link Context} used to retrieve
-     * this service, and must match the current user.
+     * this service, and must match the current user. The requesting application must be in the
+     * foreground ({@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_FOREGROUND}
+     * and the {@link android.service.quicksettings.TileService} must be exported.
      *
      * @param tileServiceComponentName {@link ComponentName} of the
      *        {@link android.service.quicksettings.TileService} for the request.
@@ -626,12 +632,10 @@
      * @param icon icon to use in the tile shown to the user.
      * @param resultExecutor an executor to run the callback on
      * @param resultCallback callback to indicate the {@link RequestResult}.
-     * @return whether the request was successfully sent.
      *
      * @see android.service.quicksettings.TileService
      */
-    @RequestAnswer
-    public int requestAddTileService(
+    public void requestAddTileService(
             @NonNull ComponentName tileServiceComponentName,
             @NonNull CharSequence tileLabel,
             @NonNull Icon icon,
@@ -644,14 +648,16 @@
         Objects.requireNonNull(resultExecutor);
         Objects.requireNonNull(resultCallback);
         if (!tileServiceComponentName.getPackageName().equals(mContext.getPackageName())) {
-            return TILE_ADD_REQUEST_ANSWER_FAILED_MISMATCHED_PACKAGE;
+            resultExecutor.execute(
+                    () -> resultCallback.accept(TILE_ADD_REQUEST_ERROR_MISMATCHED_PACKAGE));
+            return;
         }
         int userId = mContext.getUserId();
         RequestResultCallback callbackProxy = new RequestResultCallback(resultExecutor,
                 resultCallback);
         IStatusBarService svc = getService();
         try {
-            return svc.requestAddTile(
+            svc.requestAddTile(
                     tileServiceComponentName,
                     tileLabel,
                     icon,
@@ -661,7 +667,6 @@
         } catch (RemoteException ex) {
             ex.rethrowFromSystemServer();
         }
-        return TILE_ADD_REQUEST_ANSWER_FAILED_UNKNOWN_REASON;
     }
 
     /** @hide */
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 8ac8229..8ea1f83 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -26,6 +26,8 @@
 import android.app.admin.IDevicePolicyManager;
 import android.app.appsearch.AppSearchManagerFrameworkInitializer;
 import android.app.blob.BlobStoreManagerFrameworkInitializer;
+import android.app.communal.CommunalManager;
+import android.app.communal.ICommunalManager;
 import android.app.contentsuggestions.ContentSuggestionsManager;
 import android.app.contentsuggestions.IContentSuggestionsManager;
 import android.app.job.JobSchedulerFrameworkInitializer;
@@ -1460,7 +1462,23 @@
                     @Override
                     public DisplayHashManager createService(ContextImpl ctx) {
                         return new DisplayHashManager();
-                    }});
+                    }
+                });
+
+        registerService(Context.COMMUNAL_MANAGER_SERVICE, CommunalManager.class,
+                new CachedServiceFetcher<CommunalManager>() {
+                    @Override
+                    public CommunalManager createService(ContextImpl ctx) {
+                        if (!ctx.getPackageManager().hasSystemFeature(
+                                PackageManager.FEATURE_COMMUNAL_MODE)) {
+                            return null;
+                        }
+                        IBinder iBinder =
+                                ServiceManager.getService(Context.COMMUNAL_MANAGER_SERVICE);
+                        return iBinder != null ? new CommunalManager(
+                                ICommunalManager.Stub.asInterface(iBinder)) : null;
+                    }
+                });
 
         sInitializing = true;
         try {
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index cac7639..bd9b6e9 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -265,6 +265,13 @@
     }
 
     /**
+     * Whether this task is visible.
+     */
+    public boolean isVisible() {
+        return isVisible;
+    }
+
+    /**
      * @param isLowResolution
      * @return
      * @hide
diff --git a/core/java/android/app/WallpaperInfo.java b/core/java/android/app/WallpaperInfo.java
index a969b10..99d4064 100644
--- a/core/java/android/app/WallpaperInfo.java
+++ b/core/java/android/app/WallpaperInfo.java
@@ -81,7 +81,7 @@
     final int mContextDescriptionResource;
     final boolean mShowMetadataInPreview;
     final boolean mSupportsAmbientMode;
-    final boolean mShouldUseDefaultDisplayStateChangeTransition;
+    final boolean mShouldUseDefaultUnfoldTransition;
     final String mSettingsSliceUri;
     final boolean mSupportMultipleDisplays;
 
@@ -146,9 +146,9 @@
             mSupportsAmbientMode = sa.getBoolean(
                     com.android.internal.R.styleable.Wallpaper_supportsAmbientMode,
                     false);
-            mShouldUseDefaultDisplayStateChangeTransition = sa.getBoolean(
+            mShouldUseDefaultUnfoldTransition = sa.getBoolean(
                     com.android.internal.R.styleable
-                            .Wallpaper_shouldUseDefaultDisplayStateChangeTransition, true);
+                            .Wallpaper_shouldUseDefaultUnfoldTransition, true);
             mSettingsSliceUri = sa.getString(
                     com.android.internal.R.styleable.Wallpaper_settingsSliceUri);
             mSupportMultipleDisplays = sa.getBoolean(
@@ -175,7 +175,7 @@
         mSupportsAmbientMode = source.readInt() != 0;
         mSettingsSliceUri = source.readString();
         mSupportMultipleDisplays = source.readInt() != 0;
-        mShouldUseDefaultDisplayStateChangeTransition = source.readInt() != 0;
+        mShouldUseDefaultUnfoldTransition = source.readInt() != 0;
         mService = ResolveInfo.CREATOR.createFromParcel(source);
     }
     
@@ -400,24 +400,24 @@
 
     /**
      * Returns whether this wallpaper should receive default zooming updates when the device
-     * changes its display state (e.g. when folding or unfolding a foldable device).
+     * changes its state (e.g. when folding or unfolding a foldable device).
      * If set to false the wallpaper will not receive zoom events when changing the device state,
      * so it can implement its own transition instead.
      * <p>
      * This corresponds to the value {@link
-     * android.R.styleable#Wallpaper_shouldUseDefaultDisplayStateChangeTransition} in the
+     * android.R.styleable#Wallpaper_shouldUseDefaultUnfoldTransition} in the
      * XML description of the wallpaper.
      * <p>
      * The default value is {@code true}.
      *
-     * @see android.R.styleable#Wallpaper_shouldUseDefaultDisplayStateChangeTransition
+     * @see android.R.styleable#Wallpaper_shouldUseDefaultUnfoldTransition
      * @return {@code true} if wallpaper should receive default device state change
      * transition updates
      *
-     * @attr ref android.R.styleable#Wallpaper_shouldUseDefaultDisplayStateChangeTransition
+     * @attr ref android.R.styleable#Wallpaper_shouldUseDefaultUnfoldTransition
      */
-    public boolean shouldUseDefaultDisplayStateChangeTransition() {
-        return mShouldUseDefaultDisplayStateChangeTransition;
+    public boolean shouldUseDefaultUnfoldTransition() {
+        return mShouldUseDefaultUnfoldTransition;
     }
 
     public void dump(Printer pw, String prefix) {
@@ -450,7 +450,7 @@
         dest.writeInt(mSupportsAmbientMode ? 1 : 0);
         dest.writeString(mSettingsSliceUri);
         dest.writeInt(mSupportMultipleDisplays ? 1 : 0);
-        dest.writeInt(mShouldUseDefaultDisplayStateChangeTransition ? 1 : 0);
+        dest.writeInt(mShouldUseDefaultUnfoldTransition ? 1 : 0);
         mService.writeToParcel(dest, flags);
     }
 
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 6cea2a4..84bb9c6 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -3116,11 +3116,19 @@
         }
     }
 
-    /** @hide */
-    public void resetNewUserDisclaimer() {
+    /**
+     * Acknoledges that the new managed user disclaimer was viewed by the (human) user
+     * so that {@link #ACTION_SHOW_NEW_USER_DISCLAIMER broadcast} is not sent again the next time
+     * this user is switched to.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MANAGE_USERS)
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+    public void acknowledgeNewUserDisclaimer() {
         if (mService != null) {
             try {
-                mService.resetNewUserDisclaimer();
+                mService.acknowledgeNewUserDisclaimer();
             } catch (RemoteException e) {
                 throw e.rethrowFromSystemServer();
             }
@@ -5651,9 +5659,10 @@
      *
      * @hide
      */
+    @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
     public static final String ACTION_SHOW_NEW_USER_DISCLAIMER =
-            "android.app.action.ACTION_SHOW_NEW_USER_DISCLAIMER";
+            "android.app.action.SHOW_NEW_USER_DISCLAIMER";
 
     /**
      * Widgets are enabled in keyguard
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index ade1190..e4c3386 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -262,7 +262,7 @@
     int stopUser(in ComponentName who, in UserHandle userHandle);
     int logoutUser(in ComponentName who);
     List<UserHandle> getSecondaryUsers(in ComponentName who);
-    void resetNewUserDisclaimer();
+    void acknowledgeNewUserDisclaimer();
 
     void enableSystemApp(in ComponentName admin, in String callerPackage, in String packageName);
     int enableSystemAppWithIntent(in ComponentName admin, in String callerPackage, in Intent intent);
diff --git a/core/java/android/app/communal/CommunalManager.java b/core/java/android/app/communal/CommunalManager.java
new file mode 100644
index 0000000..375a9af
--- /dev/null
+++ b/core/java/android/app/communal/CommunalManager.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.communal;
+
+import android.Manifest;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+
+/**
+ * System private class for talking with the
+ * {@link com.android.server.communal.CommunalManagerService} that handles communal mode state.
+ *
+ * @hide
+ */
+@SystemService(Context.COMMUNAL_MANAGER_SERVICE)
+public final class CommunalManager {
+    private final ICommunalManager mService;
+
+    public CommunalManager(ICommunalManager service) {
+        mService = service;
+    }
+
+    /**
+     * Updates whether or not the communal view is currently showing over the lockscreen.
+     *
+     * @param isShowing Whether communal view is showing.
+     */
+    @RequiresPermission(Manifest.permission.WRITE_COMMUNAL_STATE)
+    public void setCommunalViewShowing(boolean isShowing) {
+        try {
+            mService.setCommunalViewShowing(isShowing);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/core/java/android/app/communal/ICommunalManager.aidl b/core/java/android/app/communal/ICommunalManager.aidl
new file mode 100644
index 0000000..02e8a65
--- /dev/null
+++ b/core/java/android/app/communal/ICommunalManager.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app.communal;
+
+/**
+ * System private API for talking with the communal manager service that handles communal mode
+ * state.
+ *
+ * @hide
+ */
+oneway interface ICommunalManager {
+    void setCommunalViewShowing(boolean isShowing);
+}
\ No newline at end of file
diff --git a/core/java/android/app/servertransaction/TransferSplashScreenViewStateItem.java b/core/java/android/app/servertransaction/TransferSplashScreenViewStateItem.java
index 5374984..767fd28 100644
--- a/core/java/android/app/servertransaction/TransferSplashScreenViewStateItem.java
+++ b/core/java/android/app/servertransaction/TransferSplashScreenViewStateItem.java
@@ -16,17 +16,14 @@
 
 package android.app.servertransaction;
 
-import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityThread;
 import android.app.ClientTransactionHandler;
 import android.os.Parcel;
+import android.view.SurfaceControl;
 import android.window.SplashScreenView.SplashScreenViewParcelable;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
 /**
  * Transfer a splash screen view to an Activity.
  * @hide
@@ -34,31 +31,13 @@
 public class TransferSplashScreenViewStateItem extends ActivityTransactionItem {
 
     private SplashScreenViewParcelable mSplashScreenViewParcelable;
-    private @TransferRequest int mRequest;
-
-    @IntDef(value = {
-            ATTACH_TO,
-            HANDOVER_TO
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface TransferRequest {}
-    // request client to attach the view on it.
-    public static final int ATTACH_TO = 0;
-    // tell client that you can handle the splash screen view.
-    public static final int HANDOVER_TO = 1;
+    private SurfaceControl mStartingWindowLeash;
 
     @Override
     public void execute(@NonNull ClientTransactionHandler client,
             @NonNull ActivityThread.ActivityClientRecord r,
             PendingTransactionActions pendingActions) {
-        switch (mRequest) {
-            case ATTACH_TO:
-                client.handleAttachSplashScreenView(r, mSplashScreenViewParcelable);
-                break;
-            case HANDOVER_TO:
-                client.handOverSplashScreenView(r);
-                break;
-        }
+        client.handleAttachSplashScreenView(r, mSplashScreenViewParcelable, mStartingWindowLeash);
     }
 
     @Override
@@ -68,26 +47,27 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeInt(mRequest);
         dest.writeTypedObject(mSplashScreenViewParcelable, flags);
+        dest.writeTypedObject(mStartingWindowLeash, flags);
     }
 
     private TransferSplashScreenViewStateItem() {}
     private TransferSplashScreenViewStateItem(Parcel in) {
-        mRequest = in.readInt();
         mSplashScreenViewParcelable = in.readTypedObject(SplashScreenViewParcelable.CREATOR);
+        mStartingWindowLeash = in.readTypedObject(SurfaceControl.CREATOR);
     }
 
     /** Obtain an instance initialized with provided params. */
-    public static TransferSplashScreenViewStateItem obtain(@TransferRequest int state,
-            @Nullable SplashScreenViewParcelable parcelable) {
+    public static TransferSplashScreenViewStateItem obtain(
+            @Nullable SplashScreenViewParcelable parcelable,
+            @Nullable SurfaceControl startingWindowLeash) {
         TransferSplashScreenViewStateItem instance =
                 ObjectPool.obtain(TransferSplashScreenViewStateItem.class);
         if (instance == null) {
             instance = new TransferSplashScreenViewStateItem();
         }
-        instance.mRequest = state;
         instance.mSplashScreenViewParcelable = parcelable;
+        instance.mStartingWindowLeash = startingWindowLeash;
 
         return instance;
     }
diff --git a/core/java/android/app/time/OWNERS b/core/java/android/app/time/OWNERS
index 8f80897..ef357e5 100644
--- a/core/java/android/app/time/OWNERS
+++ b/core/java/android/app/time/OWNERS
@@ -1,3 +1,4 @@
 # Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+# The app-facing APIs related to both time and time zone detection.
+include /services/core/java/com/android/server/timedetector/OWNERS
+include /services/core/java/com/android/server/timezonedetector/OWNERS
diff --git a/core/java/android/app/timedetector/OWNERS b/core/java/android/app/timedetector/OWNERS
index 941eed8..e9dbe4a 100644
--- a/core/java/android/app/timedetector/OWNERS
+++ b/core/java/android/app/timedetector/OWNERS
@@ -1,4 +1,3 @@
 # Bug component: 847766
-mingaleev@google.com
-narayan@google.com
-nfuller@google.com
+# Internal APIs related to time detection. SDK APIs are in android.app.time.
+include /services/core/java/com/android/server/timedetector/OWNERS
diff --git a/core/java/android/app/timezone/OWNERS b/core/java/android/app/timezone/OWNERS
index 8f80897..04d78f2 100644
--- a/core/java/android/app/timezone/OWNERS
+++ b/core/java/android/app/timezone/OWNERS
@@ -1,3 +1,4 @@
-# Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+# Bug component: 24949
+# Internal APIs related to APK-based time zone rule updates.
+# Deprecated, deletion tracked by b/148144561
+include /services/core/java/com/android/server/timezone/OWNERS
diff --git a/core/java/android/app/timezonedetector/OWNERS b/core/java/android/app/timezonedetector/OWNERS
index 8f80897..fa03f1e 100644
--- a/core/java/android/app/timezonedetector/OWNERS
+++ b/core/java/android/app/timezonedetector/OWNERS
@@ -1,3 +1,3 @@
 # Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+# Internal APIs related to time zone detection. SDK APIs are in android.app.time.
+include /services/core/java/com/android/server/timezonedetector/OWNERS
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java
index 3b744a7..06ce053 100644
--- a/core/java/android/bluetooth/BluetoothAdapter.java
+++ b/core/java/android/bluetooth/BluetoothAdapter.java
@@ -2099,7 +2099,7 @@
         try {
             return mManagerService.isBleScanAlwaysAvailable();
         } catch (RemoteException e) {
-            Log.e(TAG, "remote expection when calling isBleScanAlwaysAvailable", e);
+            Log.e(TAG, "remote exception when calling isBleScanAlwaysAvailable", e);
             return false;
         }
     }
@@ -2307,7 +2307,7 @@
         try {
             return mManagerService.isHearingAidProfileSupported();
         } catch (RemoteException e) {
-            Log.e(TAG, "remote expection when calling isHearingAidProfileSupported", e);
+            Log.e(TAG, "remote exception when calling isHearingAidProfileSupported", e);
             return false;
         }
     }
diff --git a/core/java/android/bluetooth/BluetoothLeAudio.java b/core/java/android/bluetooth/BluetoothLeAudio.java
index 3ea865b..d7940eb 100644
--- a/core/java/android/bluetooth/BluetoothLeAudio.java
+++ b/core/java/android/bluetooth/BluetoothLeAudio.java
@@ -156,6 +156,12 @@
             "android.bluetooth.action.LE_AUDIO_CONF_CHANGED";
 
     /**
+     * Indicates unspecified audio content.
+     * @hide
+     */
+    public static final int CONTEXT_TYPE_UNSPECIFIED = 0x0001;
+
+    /**
      * Indicates conversation between humans as, for example, in telephony or video calls.
      * @hide
      */
@@ -168,6 +174,66 @@
     public static final int CONTEXT_TYPE_MEDIA = 0x0004;
 
     /**
+     * Indicates instructional audio as, for example, in navigation, traffic announcements
+     * or user guidance.
+     * @hide
+     */
+    public static final int CONTEXT_TYPE_INSTRUCTIONAL = 0x0008;
+
+    /**
+     * Indicates attention seeking audio as, for example, in beeps signalling arrival of a message
+     * or keyboard clicks.
+     * @hide
+     */
+    public static final int CONTEXT_TYPE_ATTENTION_SEEKING = 0x0010;
+
+    /**
+     * Indicates immediate alerts as, for example, in a low battery alarm, timer expiry or alarm
+     * clock.
+     * @hide
+     */
+    public static final int CONTEXT_TYPE_IMMEDIATE_ALERT = 0x0020;
+
+    /**
+     * Indicates man machine communication as, for example, with voice recognition or virtual
+     * assistant.
+     * @hide
+     */
+    public static final int CONTEXT_TYPE_MAN_MACHINE = 0x0040;
+
+    /**
+     * Indicates emergency alerts as, for example, with fire alarms or other urgent alerts.
+     * @hide
+     */
+    public static final int CONTEXT_TYPE_EMERGENCY_ALERT = 0x0080;
+
+    /**
+     * Indicates ringtone as in a call alert.
+     * @hide
+     */
+    public static final int CONTEXT_TYPE_RINGTONE = 0x0100;
+
+    /**
+     * Indicates audio associated with a television program and/or with metadata conforming to the
+     * Bluetooth Broadcast TV profile.
+     * @hide
+     */
+    public static final int CONTEXT_TYPE_TV = 0x0200;
+
+    /**
+     * Indicates audio associated with a low latency live audio stream.
+     *
+     * @hide
+     */
+    public static final int CONTEXT_TYPE_LIVE = 0x0400;
+
+    /**
+     * Indicates audio associated with a video game stream.
+     * @hide
+     */
+    public static final int CONTEXT_TYPE_GAME = 0x0800;
+
+    /**
      * This represents an invalid group ID.
      *
      * @hide
@@ -199,11 +265,8 @@
      *
      * <p>
      * <ul>
-     * <li> {@link #GROUP_STATUS_IDLE} </li>
-     * <li> {@link #GROUP_STATUS_STREAMING} </li>
-     * <li> {@link #GROUP_STATUS_SUSPENDED} </li>
-     * <li> {@link #GROUP_STATUS_RECONFIGURED} </li>
-     * <li> {@link #GROUP_STATUS_DESTROYED} </li>
+     * <li> {@link #GROUP_STATUS_ACTIVE} </li>
+     * <li> {@link #GROUP_STATUS_INACTIVE} </li>
      * </ul>
      * <p>
      * @hide
@@ -241,6 +304,30 @@
 
     private final BluetoothAdapter mAdapter;
     private final AttributionSource mAttributionSource;
+    /**
+     * Indicating that group is Active ( Audio device is available )
+     * @hide
+     */
+    public static final int GROUP_STATUS_ACTIVE = IBluetoothLeAudio.GROUP_STATUS_ACTIVE;
+
+    /**
+     * Indicating that group is Inactive ( Audio device is not available )
+     * @hide
+     */
+    public static final int GROUP_STATUS_INACTIVE = IBluetoothLeAudio.GROUP_STATUS_INACTIVE;
+
+    /**
+     * Indicating that node has been added to the group.
+     * @hide
+     */
+    public static final int GROUP_NODE_ADDED = IBluetoothLeAudio.GROUP_NODE_ADDED;
+
+    /**
+     * Indicating that node has been removed from the group.
+     * @hide
+     */
+    public static final int GROUP_NODE_REMOVED = IBluetoothLeAudio.GROUP_NODE_REMOVED;
+
     private final BluetoothProfileConnector<IBluetoothLeAudio> mProfileConnector =
             new BluetoothProfileConnector(this, BluetoothProfile.LE_AUDIO, "BluetoothLeAudio",
                     IBluetoothLeAudio.class.getName()) {
@@ -433,7 +520,7 @@
      * <p> This API returns false in scenarios like the profile on the
      * device is not connected or Bluetooth is not turned on.
      * When this API returns true, it is guaranteed that the
-     * {@link #ACTION_LEAUDIO_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
+     * {@link #ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
      * with the active device.
      *
      *
@@ -512,6 +599,85 @@
     }
 
     /**
+     * Set volume for the streaming devices
+     *
+     * @param volume volume to set
+     * @hide
+     */
+    @RequiresBluetoothConnectPermission
+    @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED})
+    public void setVolume(int volume) {
+        if (VDBG) log("setVolume(vol: " + volume + " )");
+        try {
+            final IBluetoothLeAudio service = getService();
+            if (service != null && mAdapter.isEnabled()) {
+                service.setVolume(volume, mAttributionSource);
+                return;
+            }
+            if (service == null) Log.w(TAG, "Proxy not attached to service");
+            return;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+            return;
+        }
+    }
+
+    /**
+     * Add device to the given group.
+     * @param group_id group ID the device is being added to
+     * @param device the active device
+     * @return true on success, otherwise false
+     * @hide
+     */
+    @RequiresBluetoothConnectPermission
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.BLUETOOTH_CONNECT,
+            android.Manifest.permission.BLUETOOTH_PRIVILEGED
+    })
+    public boolean groupAddNode(int group_id, @NonNull BluetoothDevice device) {
+        if (VDBG) log("groupAddNode()");
+        final IBluetoothLeAudio service = getService();
+        try {
+            if (service != null && mAdapter.isEnabled()) {
+                return service.groupAddNode(group_id, device, mAttributionSource);
+            }
+            if (service == null) Log.w(TAG, "Proxy not attached to service");
+            return false;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+            return false;
+        }
+    }
+
+    /**
+     * Remove device from a given group.
+     * @param group_id group ID the device is being removed from
+     * @param device the active device
+     * @return true on success, otherwise false
+     *
+     * @hide
+     */
+    @RequiresBluetoothConnectPermission
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.BLUETOOTH_CONNECT,
+            android.Manifest.permission.BLUETOOTH_PRIVILEGED
+    })
+    public boolean groupRemoveNode(int group_id, @NonNull BluetoothDevice device) {
+        if (VDBG) log("groupRemoveNode()");
+        final IBluetoothLeAudio service = getService();
+        try {
+            if (service != null && mAdapter.isEnabled()) {
+                return service.groupRemoveNode(group_id, device, mAttributionSource);
+            }
+            if (service == null) Log.w(TAG, "Proxy not attached to service");
+            return false;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+            return false;
+        }
+    }
+
+    /**
      * Set connection policy of the profile
      *
      * <p> The device should already be paired.
diff --git a/core/java/android/bluetooth/BluetoothManager.java b/core/java/android/bluetooth/BluetoothManager.java
index c21362c..20152f3 100644
--- a/core/java/android/bluetooth/BluetoothManager.java
+++ b/core/java/android/bluetooth/BluetoothManager.java
@@ -88,8 +88,9 @@
                 uid = android.os.Process.SYSTEM_UID;
             }
             try {
-                res = new AttributionSource(uid,
-                        AppGlobals.getPackageManager().getPackagesForUid(uid)[0], null);
+                res = new AttributionSource.Builder(uid)
+                    .setPackageName(AppGlobals.getPackageManager().getPackagesForUid(uid)[0])
+                    .build();
             } catch (RemoteException ignored) {
             }
         }
diff --git a/core/java/android/bluetooth/BluetoothUuid.java b/core/java/android/bluetooth/BluetoothUuid.java
index 67b7252..858819e 100644
--- a/core/java/android/bluetooth/BluetoothUuid.java
+++ b/core/java/android/bluetooth/BluetoothUuid.java
@@ -155,12 +155,11 @@
     @SystemApi
     public static final ParcelUuid HEARING_AID =
             ParcelUuid.fromString("0000FDF0-0000-1000-8000-00805f9b34fb");
-   /** Placeholder until specification is released
-     * @hide */
+    /** @hide */
     @NonNull
     @SystemApi
     public static final ParcelUuid LE_AUDIO =
-            ParcelUuid.fromString("EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE");
+            ParcelUuid.fromString("0000184E-0000-1000-8000-00805F9B34FB");
     /** @hide */
     @NonNull
     @SystemApi
@@ -189,6 +188,11 @@
     /** @hide */
     @NonNull
     @SystemApi
+    public static final ParcelUuid CAP =
+        ParcelUuid.fromString("EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE");
+    /** @hide */
+    @NonNull
+    @SystemApi
     public static final ParcelUuid BASE_UUID =
             ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB");
 
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index bdb7900..6ae2bb5 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -474,6 +474,18 @@
             mAttributionSourceState.uid = uid;
         }
 
+        public Builder(@NonNull AttributionSource current) {
+            if (current == null) {
+                throw new IllegalArgumentException("current AttributionSource can not be null");
+            }
+            mAttributionSourceState.uid = current.getUid();
+            mAttributionSourceState.packageName = current.getPackageName();
+            mAttributionSourceState.attributionTag = current.getAttributionTag();
+            mAttributionSourceState.token = current.getToken();
+            mAttributionSourceState.renouncedPermissions =
+                current.mAttributionSourceState.renouncedPermissions;
+        }
+
         /**
          * The package that is accessing the permission protected data.
          */
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 3ab070f..dc37882 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3001,7 +3001,7 @@
      * @param receiver The BroadcastReceiver to handle the broadcast.
      * @param filter Selects the Intent broadcasts to be received.
      * @param flags Additional options for the receiver. As of
-     * {@link android.os.Build.VERSION_CODES#TIRAMISU}, either {@link #RECEIVER_EXPORTED} or
+     * Android T, either {@link #RECEIVER_EXPORTED} or
      * {@link #RECEIVER_NOT_EXPORTED} must be specified if the receiver isn't being registered
      *            for protected broadcasts, and may additionally specify
      *            {@link #RECEIVER_VISIBLE_TO_INSTANT_APPS} if {@link #RECEIVER_EXPORTED} is
@@ -3078,7 +3078,7 @@
      * @param scheduler Handler identifying the thread that will receive
      *      the Intent.  If null, the main thread of the process will be used.
      * @param flags Additional options for the receiver. As of
-     * {@link android.os.Build.VERSION_CODES#TIRAMISU}, either {@link #RECEIVER_EXPORTED} or
+     * Android T, either {@link #RECEIVER_EXPORTED} or
      * {@link #RECEIVER_NOT_EXPORTED} must be specified if the receiver isn't being registered
      *            for protected broadcasts, and may additionally specify
      *            {@link #RECEIVER_VISIBLE_TO_INSTANT_APPS} if {@link #RECEIVER_EXPORTED} is
@@ -3142,7 +3142,7 @@
      * @param scheduler Handler identifying the thread that will receive
      *      the Intent. If {@code null}, the main thread of the process will be used.
      * @param flags Additional options for the receiver. As of
-     *      {@link android.os.Build.VERSION_CODES#TIRAMISU}, either {@link #RECEIVER_EXPORTED} or
+     *      Android T, either {@link #RECEIVER_EXPORTED} or
      *      {@link #RECEIVER_NOT_EXPORTED} must be specified if the receiver isn't being
      *      registered for protected broadcasts
      *
@@ -3209,7 +3209,7 @@
      * @param scheduler Handler identifying the thread that will receive
      *      the Intent.  If null, the main thread of the process will be used.
      * @param flags Additional options for the receiver. As of
-     *      {@link android.os.Build.VERSION_CODES#TIRAMISU}, either {@link #RECEIVER_EXPORTED} or
+     *      Android T, either {@link #RECEIVER_EXPORTED} or
      *      {@link #RECEIVER_NOT_EXPORTED} must be specified if the receiver isn't being
      *      registered for protected broadcasts
      *
@@ -3801,6 +3801,7 @@
             //@hide: TIME_ZONE_DETECTOR_SERVICE,
             PERMISSION_SERVICE,
             LIGHTS_SERVICE,
+            LOCALE_SERVICE,
             //@hide: PEOPLE_SERVICE,
             //@hide: DEVICE_STATE_SERVICE,
             //@hide: SPEECH_RECOGNITION_SERVICE,
@@ -5784,6 +5785,25 @@
     public static final String DISPLAY_HASH_SERVICE = "display_hash";
 
     /**
+     * Use with {@link #getSystemService(String)} to retrieve a
+     * {@link android.app.CommunalManager} for interacting with the global system state.
+     *
+     * @see #getSystemService(String)
+     * @see android.app.CommunalManager
+     * @hide
+     */
+    public static final String COMMUNAL_MANAGER_SERVICE = "communal_manager";
+
+    /**
+     * Use with {@link #getSystemService(String)} to retrieve a
+     * {@link android.app.LocaleManager}.
+     *
+     * @see #getSystemService(String)
+     * @hide
+     */
+    public static final String LOCALE_SERVICE = "locale";
+
+    /**
      * Determine whether the given permission is allowed for a particular
      * process and user ID running in the system.
      *
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 896bf9c..ed65af3 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2306,6 +2306,47 @@
     public static final String ACTION_REVIEW_ACCESSIBILITY_SERVICES =
             "android.intent.action.REVIEW_ACCESSIBILITY_SERVICES";
 
+    /**
+     * Activity action: Launch UI to manage the usage of a given permission group.
+     * This action would be handled by apps that want to show controls about the features
+     * which use the permission group.
+     *
+     * <p>
+     * Input: {@link #EXTRA_PERMISSION_GROUP_NAME} specifies the permission group for
+     * which the launched UI would be targeted.
+     * Input: {@link #EXTRA_ATTRIBUTION_TAGS} specifies the attribution tags for the usage entry.
+     * Input: {@link #EXTRA_START_TIME} specifies the start time of the period (epoch time in
+     * millis). If both start time and end time are present, start time must be <= end time.
+     * Input: {@link #EXTRA_END_TIME} specifies the end time of the period (epoch time in
+     * millis). If the end time is empty, that implies that the permission usage is still in use.
+     * If both start time and end time are present, start time must be <= end time.
+     * Input: {@link #EXTRA_SHOWING_ATTRIBUTION} specifies whether the subattribution was shown
+     * in the UI.
+     * </p>
+     * <p>
+     * Output: Nothing.
+     * </p>
+     * <p class="note">
+     * You must protect the activity that handles this action with the
+     * {@link android.Manifest.permission#START_VIEW_PERMISSION_USAGE} permission to ensure that
+     * only the system can launch this activity. The system will not launch activities
+     * that are not properly protected.
+     * </p>
+     *
+     * @see #EXTRA_PERMISSION_GROUP_NAME
+     * @see #EXTRA_ATTRIBUTION_TAGS
+     * @see #EXTRA_START_TIME
+     * @see #EXTRA_END_TIME
+     * @see #EXTRA_SHOWING_ATTRIBUTION
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.START_VIEW_PERMISSION_USAGE)
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_MANAGE_PERMISSION_USAGE =
+            "android.intent.action.MANAGE_PERMISSION_USAGE";
+
     // ---------------------------------------------------------------------
     // ---------------------------------------------------------------------
     // Standard intent broadcast actions (see action variable).
@@ -8971,16 +9012,6 @@
     }
 
     /**
-     * Filter extras to only basic types.
-     * @hide
-     */
-    public void removeUnsafeExtras() {
-        if (mExtras != null) {
-            mExtras = mExtras.filterValues();
-        }
-    }
-
-    /**
      * @return Whether {@link #maybeStripForHistory} will return an lightened intent or
      * return itself as-is.
      * @hide
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index b66f048..8ebb8ec 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -1736,35 +1736,9 @@
      * object and the given one.  Does not change the values of either.  Any
      * undefined fields in <var>delta</var> are ignored.
      * @return Returns a bit mask indicating which configuration
-     * values has changed, containing any combination of
-     * {@link android.content.pm.ActivityInfo#CONFIG_FONT_SCALE
-     * PackageManager.ActivityInfo.CONFIG_FONT_SCALE},
-     * {@link android.content.pm.ActivityInfo#CONFIG_MCC
-     * PackageManager.ActivityInfo.CONFIG_MCC},
-     * {@link android.content.pm.ActivityInfo#CONFIG_MNC
-     * PackageManager.ActivityInfo.CONFIG_MNC},
-     * {@link android.content.pm.ActivityInfo#CONFIG_LOCALE
-     * PackageManager.ActivityInfo.CONFIG_LOCALE},
-     * {@link android.content.pm.ActivityInfo#CONFIG_TOUCHSCREEN
-     * PackageManager.ActivityInfo.CONFIG_TOUCHSCREEN},
-     * {@link android.content.pm.ActivityInfo#CONFIG_KEYBOARD
-     * PackageManager.ActivityInfo.CONFIG_KEYBOARD},
-     * {@link android.content.pm.ActivityInfo#CONFIG_NAVIGATION
-     * PackageManager.ActivityInfo.CONFIG_NAVIGATION},
-     * {@link android.content.pm.ActivityInfo#CONFIG_ORIENTATION
-     * PackageManager.ActivityInfo.CONFIG_ORIENTATION},
-     * {@link android.content.pm.ActivityInfo#CONFIG_SCREEN_LAYOUT
-     * PackageManager.ActivityInfo.CONFIG_SCREEN_LAYOUT}, or
-     * {@link android.content.pm.ActivityInfo#CONFIG_SCREEN_SIZE
-     * PackageManager.ActivityInfo.CONFIG_SCREEN_SIZE}, or
-     * {@link android.content.pm.ActivityInfo#CONFIG_SMALLEST_SCREEN_SIZE
-     * PackageManager.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE}.
-     * {@link android.content.pm.ActivityInfo#CONFIG_LAYOUT_DIRECTION
-     * PackageManager.ActivityInfo.CONFIG_LAYOUT_DIRECTION}.
-     * {@link android.content.pm.ActivityInfo#CONFIG_FONT_WEIGHT_ADJUSTMENT
-     *  PackageManager.ActivityInfo.CONFIG_FONT_WEIGHT_ADJUSTMENT.
+     * values have changed.
      */
-    public int diff(Configuration delta) {
+    public @Config int diff(Configuration delta) {
         return diff(delta, false /* compareUndefined */, false /* publicOnly */);
     }
 
@@ -2620,10 +2594,10 @@
      * {@link #updateFrom(Configuration)} will treat it as a no-op and not update that member.
      *
      * This is fine for device configurations as no member is ever undefined.
-     * {@hide}
      */
-    @UnsupportedAppUsage
-    public static Configuration generateDelta(Configuration base, Configuration change) {
+    @NonNull
+    public static Configuration generateDelta(
+            @NonNull Configuration base, @NonNull Configuration change) {
         final Configuration delta = new Configuration();
         if (base.fontScale != change.fontScale) {
             delta.fontScale = change.fontScale;
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index e0138c5..9f77a7e 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -22,15 +22,18 @@
 import android.hardware.camera2.impl.CameraMetadataNative;
 import android.hardware.camera2.impl.PublicKey;
 import android.hardware.camera2.impl.SyntheticKey;
+import android.hardware.camera2.params.DeviceStateSensorOrientationMap;
 import android.hardware.camera2.params.RecommendedStreamConfigurationMap;
 import android.hardware.camera2.params.SessionConfiguration;
 import android.hardware.camera2.utils.TypeReference;
 import android.os.Build;
+import android.util.Log;
 import android.util.Rational;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -202,8 +205,25 @@
     private List<CaptureResult.Key<?>> mAvailableResultKeys;
     private ArrayList<RecommendedStreamConfigurationMap> mRecommendedConfigurations;
 
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private boolean mFoldedDeviceState;
+
+    private final CameraManager.DeviceStateListener mFoldStateListener =
+            new CameraManager.DeviceStateListener() {
+                @Override
+                public final void onDeviceStateChanged(boolean folded) {
+                    synchronized (mLock) {
+                        mFoldedDeviceState = folded;
+                    }
+                }};
+
+    private static final String TAG = "CameraCharacteristics";
+
     /**
      * Takes ownership of the passed-in properties object
+     *
+     * @param properties Camera properties.
      * @hide
      */
     public CameraCharacteristics(CameraMetadataNative properties) {
@@ -220,6 +240,42 @@
     }
 
     /**
+     * Return the device state listener for this Camera characteristics instance
+     */
+    CameraManager.DeviceStateListener getDeviceStateListener() { return mFoldStateListener; }
+
+    /**
+     * Overrides the property value
+     *
+     * <p>Check whether a given property value needs to be overridden in some specific
+     * case.</p>
+     *
+     * @param key The characteristics field to override.
+     * @return The value of overridden property, or {@code null} if the property doesn't need an
+     * override.
+     */
+    @Nullable
+    private <T> T overrideProperty(Key<T> key) {
+        if (CameraCharacteristics.SENSOR_ORIENTATION.equals(key) && (mFoldStateListener != null) &&
+                (mProperties.get(CameraCharacteristics.INFO_DEVICE_STATE_ORIENTATIONS) != null)) {
+            DeviceStateSensorOrientationMap deviceStateSensorOrientationMap =
+                    mProperties.get(CameraCharacteristics.INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP);
+            synchronized (mLock) {
+                Integer ret = deviceStateSensorOrientationMap.getSensorOrientation(
+                        mFoldedDeviceState ? DeviceStateSensorOrientationMap.FOLDED :
+                                DeviceStateSensorOrientationMap.NORMAL);
+                if (ret >= 0) {
+                    return (T) ret;
+                } else {
+                    Log.w(TAG, "No valid device state to orientation mapping! Using default!");
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
      * Get a camera characteristics field value.
      *
      * <p>The field definitions can be
@@ -235,7 +291,8 @@
      */
     @Nullable
     public <T> T get(Key<T> key) {
-        return mProperties.get(key);
+        T propertyOverride = overrideProperty(key);
+        return (propertyOverride != null) ? propertyOverride : mProperties.get(key);
     }
 
     /**
@@ -3993,11 +4050,26 @@
      * upright on the device screen in its native orientation.</p>
      * <p>Also defines the direction of rolling shutter readout, which is from top to bottom in
      * the sensor's coordinate system.</p>
+     * <p>Starting with Android API level 32, camera clients that query the orientation via
+     * {@link android.hardware.camera2.CameraCharacteristics#get } on foldable devices which
+     * include logical cameras can receive a value that can dynamically change depending on the
+     * device/fold state.
+     * Clients are advised to not cache or store the orientation value of such logical sensors.
+     * In case repeated queries to CameraCharacteristics are not preferred, then clients can
+     * also access the entire mapping from device state to sensor orientation in
+     * {@link android.hardware.camera2.params.DeviceStateSensorOrientationMap }.
+     * Do note that a dynamically changing sensor orientation value in camera characteristics
+     * will not be the best way to establish the orientation per frame. Clients that want to
+     * know the sensor orientation of a particular captured frame should query the
+     * {@link CaptureResult#LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_ID android.logicalMultiCamera.activePhysicalId} from the corresponding capture result and
+     * check the respective physical camera orientation.</p>
      * <p><b>Units</b>: Degrees of clockwise rotation; always a multiple of
      * 90</p>
      * <p><b>Range of valid values:</b><br>
      * 0, 90, 180, 270</p>
      * <p>This key is available on all devices.</p>
+     *
+     * @see CaptureResult#LOGICAL_MULTI_CAMERA_ACTIVE_PHYSICAL_ID
      */
     @PublicKey
     @NonNull
@@ -4307,6 +4379,46 @@
             new Key<String>("android.info.version", String.class);
 
     /**
+     * <p>This lists the mapping between a device folding state and
+     * specific camera sensor orientation for logical cameras on a foldable device.</p>
+     * <p>Logical cameras on foldable devices can support sensors with different orientation
+     * values. The orientation value may need to change depending on the specific folding
+     * state. Information about the mapping between the device folding state and the
+     * sensor orientation can be obtained in
+     * {@link android.hardware.camera2.params.DeviceStateSensorOrientationMap }.
+     * Device state orientation maps are optional and maybe present on devices that support
+     * {@link CaptureRequest#SCALER_ROTATE_AND_CROP android.scaler.rotateAndCrop}.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @see CaptureRequest#SCALER_ROTATE_AND_CROP
+     */
+    @PublicKey
+    @NonNull
+    @SyntheticKey
+    public static final Key<android.hardware.camera2.params.DeviceStateSensorOrientationMap> INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP =
+            new Key<android.hardware.camera2.params.DeviceStateSensorOrientationMap>("android.info.deviceStateSensorOrientationMap", android.hardware.camera2.params.DeviceStateSensorOrientationMap.class);
+
+    /**
+     * <p>HAL must populate the array with
+     * (hardware::camera::provider::V2_5::DeviceState, sensorOrientation) pairs for each
+     * supported device state bitwise combination.</p>
+     * <p><b>Units</b>: (device fold state, sensor orientation) x n</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     * <p><b>Limited capability</b> -
+     * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+     * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+     *
+     * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+     * @hide
+     */
+    public static final Key<long[]> INFO_DEVICE_STATE_ORIENTATIONS =
+            new Key<long[]>("android.info.deviceStateOrientations", long[].class);
+
+    /**
      * <p>The maximum number of frames that can occur after a request
      * (different than the previous) has been submitted, and before the
      * result's state becomes synchronized.</p>
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index f5306b8..93f1d61 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -36,10 +36,13 @@
 import android.hardware.camera2.params.StreamConfiguration;
 import android.hardware.camera2.utils.CameraIdAndSessionConfiguration;
 import android.hardware.camera2.utils.ConcurrentCameraIdCombination;
+import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.display.DisplayManager;
 import android.os.Binder;
 import android.os.DeadObjectException;
 import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -51,6 +54,10 @@
 import android.util.Size;
 import android.view.Display;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ArrayUtils;
+
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
@@ -105,6 +112,90 @@
                     mContext.checkSelfPermission(CAMERA_OPEN_CLOSE_LISTENER_PERMISSION) ==
                     PackageManager.PERMISSION_GRANTED;
         }
+
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mFoldStateListener = new FoldStateListener(context);
+        try {
+            context.getSystemService(DeviceStateManager.class)
+                    .registerCallback(new HandlerExecutor(mHandler), mFoldStateListener);
+        } catch (IllegalStateException e) {
+            Log.v(TAG, "Failed to register device state listener!");
+            Log.v(TAG, "Device state dependent characteristics updates will not be functional!");
+            mHandlerThread.quitSafely();
+            mHandler = null;
+            mFoldStateListener = null;
+        }
+    }
+
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private FoldStateListener mFoldStateListener;
+    @GuardedBy("mLock")
+    private ArrayList<WeakReference<DeviceStateListener>> mDeviceStateListeners = new ArrayList<>();
+    private boolean mFoldedDeviceState;
+
+    /**
+     * @hide
+     */
+    public interface DeviceStateListener {
+        void onDeviceStateChanged(boolean folded);
+    }
+
+    private final class FoldStateListener implements DeviceStateManager.DeviceStateCallback {
+        private final int[] mFoldedDeviceStates;
+
+        public FoldStateListener(Context context) {
+            mFoldedDeviceStates = context.getResources().getIntArray(
+                    com.android.internal.R.array.config_foldedDeviceStates);
+        }
+
+        private void handleStateChange(int state) {
+            boolean folded = ArrayUtils.contains(mFoldedDeviceStates, state);
+            synchronized (mLock) {
+                mFoldedDeviceState = folded;
+                ArrayList<WeakReference<DeviceStateListener>> invalidListeners = new ArrayList<>();
+                for (WeakReference<DeviceStateListener> listener : mDeviceStateListeners) {
+                    DeviceStateListener callback = listener.get();
+                    if (callback != null) {
+                        callback.onDeviceStateChanged(folded);
+                    } else {
+                        invalidListeners.add(listener);
+                    }
+                }
+                if (!invalidListeners.isEmpty()) {
+                    mDeviceStateListeners.removeAll(invalidListeners);
+                }
+            }
+        }
+
+        @Override
+        public final void onBaseStateChanged(int state) {
+            handleStateChange(state);
+        }
+
+        @Override
+        public final void onStateChanged(int state) {
+            handleStateChange(state);
+        }
+    }
+
+    /**
+     * Register a {@link CameraCharacteristics} device state listener
+     *
+     * @param chars Camera characteristics that need to receive device state updates
+     *
+     * @hide
+     */
+    public void registerDeviceStateListener(@NonNull CameraCharacteristics chars) {
+        synchronized (mLock) {
+            DeviceStateListener listener = chars.getDeviceStateListener();
+            listener.onDeviceStateChanged(mFoldedDeviceState);
+            if (mFoldStateListener != null) {
+                mDeviceStateListeners.add(new WeakReference<>(listener));
+            }
+        }
     }
 
     /**
@@ -513,6 +604,7 @@
                         "Camera service is currently unavailable", e);
             }
         }
+        registerDeviceStateListener(characteristics);
         return characteristics;
     }
 
@@ -1339,8 +1431,7 @@
         private boolean mHasOpenCloseListenerPermission = false;
 
         // Singleton, don't allow construction
-        private CameraManagerGlobal() {
-        }
+        private CameraManagerGlobal() { }
 
         public static final boolean sCameraServiceDisabled =
                 SystemProperties.getBoolean("config.disable_cameraservice", false);
diff --git a/core/java/android/hardware/camera2/CaptureFailure.java b/core/java/android/hardware/camera2/CaptureFailure.java
index 20ca4a3..032ed7e 100644
--- a/core/java/android/hardware/camera2/CaptureFailure.java
+++ b/core/java/android/hardware/camera2/CaptureFailure.java
@@ -59,7 +59,7 @@
 
     private final CaptureRequest mRequest;
     private final int mReason;
-    private final boolean mDropped;
+    private final boolean mWasImageCaptured;
     private final int mSequenceId;
     private final long mFrameNumber;
     private final String mErrorPhysicalCameraId;
@@ -68,10 +68,11 @@
      * @hide
      */
     public CaptureFailure(CaptureRequest request, int reason,
-            boolean dropped, int sequenceId, long frameNumber, String errorPhysicalCameraId) {
+            boolean wasImageCaptured, int sequenceId, long frameNumber,
+            String errorPhysicalCameraId) {
         mRequest = request;
         mReason = reason;
-        mDropped = dropped;
+        mWasImageCaptured = wasImageCaptured;
         mSequenceId = sequenceId;
         mFrameNumber = frameNumber;
         mErrorPhysicalCameraId = errorPhysicalCameraId;
@@ -141,7 +142,7 @@
      * @return boolean True if the image was captured, false otherwise.
      */
     public boolean wasImageCaptured() {
-        return !mDropped;
+        return mWasImageCaptured;
     }
 
     /**
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index 8da6551..b8443fb 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -873,21 +873,19 @@
         @Override
         public int submitBurst(List<Request> requests, IRequestCallback callback) {
             int seqId = -1;
-            synchronized (mInterfaceLock) {
-                try {
-                    CaptureCallbackHandler captureCallback = new CaptureCallbackHandler(callback);
-                    ArrayList<CaptureRequest> captureRequests = new ArrayList<>();
-                    for (Request request : requests) {
-                        captureRequests.add(initializeCaptureRequest(mCameraDevice, request,
-                                mCameraConfigMap));
-                    }
-                    seqId = mCaptureSession.captureBurstRequests(captureRequests,
-                            new CameraExtensionUtils.HandlerExecutor(mHandler), captureCallback);
-                } catch (CameraAccessException e) {
-                    Log.e(TAG, "Failed to submit capture requests!");
-                } catch (IllegalStateException e) {
-                    Log.e(TAG, "Capture session closed!");
+            try {
+                CaptureCallbackHandler captureCallback = new CaptureCallbackHandler(callback);
+                ArrayList<CaptureRequest> captureRequests = new ArrayList<>();
+                for (Request request : requests) {
+                    captureRequests.add(initializeCaptureRequest(mCameraDevice, request,
+                            mCameraConfigMap));
                 }
+                seqId = mCaptureSession.captureBurstRequests(captureRequests,
+                        new CameraExtensionUtils.HandlerExecutor(mHandler), captureCallback);
+            } catch (CameraAccessException e) {
+                Log.e(TAG, "Failed to submit capture requests!");
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Capture session closed!");
             }
 
             return seqId;
@@ -896,18 +894,16 @@
         @Override
         public int setRepeating(Request request, IRequestCallback callback) {
             int seqId = -1;
-            synchronized (mInterfaceLock) {
-                try {
-                    CaptureRequest repeatingRequest = initializeCaptureRequest(mCameraDevice,
-                                request, mCameraConfigMap);
-                    CaptureCallbackHandler captureCallback = new CaptureCallbackHandler(callback);
-                    seqId = mCaptureSession.setSingleRepeatingRequest(repeatingRequest,
-                            new CameraExtensionUtils.HandlerExecutor(mHandler), captureCallback);
-                } catch (CameraAccessException e) {
-                    Log.e(TAG, "Failed to enable repeating request!");
-                } catch (IllegalStateException e) {
-                    Log.e(TAG, "Capture session closed!");
-                }
+            try {
+                CaptureRequest repeatingRequest = initializeCaptureRequest(mCameraDevice,
+                            request, mCameraConfigMap);
+                CaptureCallbackHandler captureCallback = new CaptureCallbackHandler(callback);
+                seqId = mCaptureSession.setSingleRepeatingRequest(repeatingRequest,
+                        new CameraExtensionUtils.HandlerExecutor(mHandler), captureCallback);
+            } catch (CameraAccessException e) {
+                Log.e(TAG, "Failed to enable repeating request!");
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Capture session closed!");
             }
 
             return seqId;
@@ -915,27 +911,23 @@
 
         @Override
         public void abortCaptures() {
-            synchronized (mInterfaceLock) {
-                try {
-                    mCaptureSession.abortCaptures();
-                } catch (CameraAccessException e) {
-                    Log.e(TAG, "Failed during capture abort!");
-                } catch (IllegalStateException e) {
-                    Log.e(TAG, "Capture session closed!");
-                }
+            try {
+                mCaptureSession.abortCaptures();
+            } catch (CameraAccessException e) {
+                Log.e(TAG, "Failed during capture abort!");
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Capture session closed!");
             }
         }
 
         @Override
         public void stopRepeating() {
-            synchronized (mInterfaceLock) {
-                try {
-                    mCaptureSession.stopRepeating();
-                } catch (CameraAccessException e) {
-                    Log.e(TAG, "Failed during repeating capture stop!");
-                } catch (IllegalStateException e) {
-                    Log.e(TAG, "Capture session closed!");
-                }
+            try {
+                mCaptureSession.stopRepeating();
+            } catch (CameraAccessException e) {
+                Log.e(TAG, "Failed during repeating capture stop!");
+            } catch (IllegalStateException e) {
+                Log.e(TAG, "Capture session closed!");
             }
         }
     }
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index 4708f3e..8864939 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -1867,7 +1867,7 @@
             final CaptureFailure failure = new CaptureFailure(
                 request,
                 reason,
-                /*dropped*/ mayHaveBuffers,
+                mayHaveBuffers,
                 requestId,
                 frameNumber,
                 errorPhysicalCameraId);
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index 196134b..e393a66 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -50,10 +50,10 @@
 import android.hardware.camera2.marshal.impl.MarshalQueryableStreamConfigurationDuration;
 import android.hardware.camera2.marshal.impl.MarshalQueryableString;
 import android.hardware.camera2.params.Capability;
+import android.hardware.camera2.params.DeviceStateSensorOrientationMap;
 import android.hardware.camera2.params.Face;
 import android.hardware.camera2.params.HighSpeedVideoConfiguration;
 import android.hardware.camera2.params.LensShadingMap;
-import android.hardware.camera2.params.MeteringRectangle;
 import android.hardware.camera2.params.MandatoryStreamCombination;
 import android.hardware.camera2.params.MultiResolutionStreamConfigurationMap;
 import android.hardware.camera2.params.OisSample;
@@ -754,7 +754,7 @@
                 });
         sGetCommandMap.put(
                 CaptureResult.STATISTICS_LENS_SHADING_CORRECTION_MAP.getNativeKey(),
-                        new GetCommand() {
+                new GetCommand() {
                     @Override
                     @SuppressWarnings("unchecked")
                     public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
@@ -762,6 +762,15 @@
                     }
                 });
         sGetCommandMap.put(
+                CameraCharacteristics.INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP.getNativeKey(),
+                        new GetCommand() {
+                    @Override
+                    @SuppressWarnings("unchecked")
+                    public <T> T getValue(CameraMetadataNative metadata, Key<T> key) {
+                        return (T) metadata.getDeviceStateOrientationMap();
+                    }
+                });
+        sGetCommandMap.put(
                 CaptureResult.STATISTICS_OIS_SAMPLES.getNativeKey(),
                         new GetCommand() {
                     @Override
@@ -994,6 +1003,18 @@
         return map;
     }
 
+    private DeviceStateSensorOrientationMap getDeviceStateOrientationMap() {
+        long[] mapArray = getBase(CameraCharacteristics.INFO_DEVICE_STATE_ORIENTATIONS);
+
+        // Do not warn if map is null while s is not. This is valid.
+        if (mapArray == null) {
+            return null;
+        }
+
+        DeviceStateSensorOrientationMap map = new DeviceStateSensorOrientationMap(mapArray);
+        return map;
+    }
+
     private Location getGpsLocation() {
         String processingMethod = get(CaptureResult.JPEG_GPS_PROCESSING_METHOD);
         double[] coords = get(CaptureResult.JPEG_GPS_COORDINATES);
diff --git a/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java b/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java
new file mode 100644
index 0000000..200409e
--- /dev/null
+++ b/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.camera2.params;
+
+import android.annotation.LongDef;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.utils.HashCodeHelpers;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Objects;
+
+/**
+ * Immutable class that maps the device fold state to sensor orientation.
+ *
+ * <p>Some {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA logical}
+ * cameras on foldables can include physical sensors with different sensor orientation
+ * values. As a result, the values of the logical camera device can potentially change depending
+ * on the device fold state.</p>
+ *
+ * <p>The device fold state to sensor orientation map will contain information about the
+ * respective logical camera sensor orientation given a device state. Clients
+ * can query the mapping for all possible supported folded states.
+ *
+ * @see CameraCharacteristics#SENSOR_ORIENTATION
+ */
+public final class DeviceStateSensorOrientationMap {
+    /**
+     *  Needs to be kept in sync with the HIDL/AIDL DeviceState
+     */
+
+    /**
+     * The device is in its normal physical configuration. This is the default if the
+     * device does not support multiple different states.
+     */
+    public static final long NORMAL = 0;
+
+    /**
+     * The device is folded.  If not set, the device is unfolded or does not
+     * support folding.
+     *
+     * The exact point when this status change happens during the folding
+     * operation is device-specific.
+     */
+    public static final long FOLDED = 1 << 2;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @LongDef(prefix = {"DEVICE_STATE"}, value =
+            {NORMAL,
+             FOLDED })
+    public @interface DeviceState {};
+
+    private final HashMap<Long, Integer> mDeviceStateOrientationMap = new HashMap<>();
+
+    /**
+     * Create a new immutable DeviceStateOrientationMap instance.
+     *
+     * <p>This constructor takes over the array; do not write to the array afterwards.</p>
+     *
+     * @param elements
+     *          An array of elements describing the map
+     *
+     * @throws IllegalArgumentException
+     *            if the {@code elements} array length is invalid, not divisible by 2 or contains
+     *            invalid element values
+     * @throws NullPointerException
+     *            if {@code elements} is {@code null}
+     *
+     * @hide
+     */
+    public DeviceStateSensorOrientationMap(final long[] elements) {
+        mElements = Objects.requireNonNull(elements, "elements must not be null");
+        if ((elements.length % 2) != 0) {
+            throw new IllegalArgumentException("Device state sensor orientation map length " +
+                    elements.length + " is not even!");
+        }
+
+        for (int i = 0; i < elements.length; i += 2) {
+            if ((elements[i+1] % 90) != 0) {
+                throw new IllegalArgumentException("Sensor orientation not divisible by 90: " +
+                        elements[i+1]);
+            }
+
+            mDeviceStateOrientationMap.put(elements[i], Math.toIntExact(elements[i + 1]));
+        }
+    }
+
+    /**
+     * Return the logical camera sensor orientation given a specific device fold state.
+     *
+     * @param deviceState Device fold state
+     *
+     * @return Valid {@link android.hardware.camera2.CameraCharacteristics#SENSOR_ORIENTATION} for
+     *         any supported device fold state
+     *
+     * @throws IllegalArgumentException if the given device state is invalid
+     */
+    public int getSensorOrientation(@DeviceState long deviceState) {
+        if (!mDeviceStateOrientationMap.containsKey(deviceState)) {
+            throw new IllegalArgumentException("Invalid device state: " + deviceState);
+        }
+
+        return mDeviceStateOrientationMap.get(deviceState);
+    }
+
+    /**
+     * Check if this DeviceStateSensorOrientationMap is equal to another
+     * DeviceStateSensorOrientationMap.
+     *
+     * <p>Two device state orientation maps are equal if and only if all of their elements are
+     * {@link Object#equals equal}.</p>
+     *
+     * @return {@code true} if the objects were equal, {@code false} otherwise
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof DeviceStateSensorOrientationMap) {
+            final DeviceStateSensorOrientationMap other = (DeviceStateSensorOrientationMap) obj;
+            return Arrays.equals(mElements, other.mElements);
+        }
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return HashCodeHelpers.hashCodeGeneric(mElements);
+    }
+
+    private final long[] mElements;
+}
diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
index 518b22b..f5b2ac5 100644
--- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java
+++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
@@ -24,6 +24,8 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import java.util.Arrays;
+
 import com.android.internal.R;
 
 /**
@@ -258,10 +260,11 @@
             String defaultValue,
             int posture) {
         String sensorType = defaultValue;
-        if (posture < postureMapping.length) {
+        if (postureMapping != null && posture < postureMapping.length) {
             sensorType = postureMapping[posture];
         } else {
-            Log.e(TAG, "Unsupported doze posture " + posture);
+            Log.e(TAG, "Unsupported doze posture " + posture
+                  + " postureMapping=" + Arrays.toString(postureMapping));
         }
 
         return TextUtils.isEmpty(sensorType) ? defaultValue : sensorType;
diff --git a/core/java/android/hardware/display/BrightnessInfo.java b/core/java/android/hardware/display/BrightnessInfo.java
index c5d37c2..0dc8f92 100644
--- a/core/java/android/hardware/display/BrightnessInfo.java
+++ b/core/java/android/hardware/display/BrightnessInfo.java
@@ -60,12 +60,18 @@
     /** Brightness */
     public final float brightness;
 
+    /** Brightness after {@link DisplayPowerController} adjustments */
+    public final float adjustedBrightness;
+
     /** Current minimum supported brightness. */
     public final float brightnessMinimum;
 
     /** Current maximum supported brightness. */
     public final float brightnessMaximum;
 
+    /** Brightness values greater than this point are only used in High Brightness Mode. */
+    public final float highBrightnessTransitionPoint;
+
     /**
      * Current state of high brightness mode.
      * Can be any of HIGH_BRIGHTNESS_MODE_* values.
@@ -73,11 +79,20 @@
     public final int highBrightnessMode;
 
     public BrightnessInfo(float brightness, float brightnessMinimum, float brightnessMaximum,
-            @HighBrightnessMode int highBrightnessMode) {
+            @HighBrightnessMode int highBrightnessMode, float highBrightnessTransitionPoint) {
+        this(brightness, brightness, brightnessMinimum, brightnessMaximum, highBrightnessMode,
+                highBrightnessTransitionPoint);
+    }
+
+    public BrightnessInfo(float brightness, float adjustedBrightness, float brightnessMinimum,
+            float brightnessMaximum, @HighBrightnessMode int highBrightnessMode,
+            float highBrightnessTransitionPoint) {
         this.brightness = brightness;
+        this.adjustedBrightness = adjustedBrightness;
         this.brightnessMinimum = brightnessMinimum;
         this.brightnessMaximum = brightnessMaximum;
         this.highBrightnessMode = highBrightnessMode;
+        this.highBrightnessTransitionPoint = highBrightnessTransitionPoint;
     }
 
     /**
@@ -103,9 +118,11 @@
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeFloat(brightness);
+        dest.writeFloat(adjustedBrightness);
         dest.writeFloat(brightnessMinimum);
         dest.writeFloat(brightnessMaximum);
         dest.writeInt(highBrightnessMode);
+        dest.writeFloat(highBrightnessTransitionPoint);
     }
 
     public static final @android.annotation.NonNull Creator<BrightnessInfo> CREATOR =
@@ -123,9 +140,11 @@
 
     private BrightnessInfo(Parcel source) {
         brightness = source.readFloat();
+        adjustedBrightness = source.readFloat();
         brightnessMinimum = source.readFloat();
         brightnessMaximum = source.readFloat();
         highBrightnessMode = source.readInt();
+        highBrightnessTransitionPoint = source.readFloat();
     }
 
 }
diff --git a/core/java/android/hardware/hdmi/HdmiClient.java b/core/java/android/hardware/hdmi/HdmiClient.java
index c94a507..1a52f11 100644
--- a/core/java/android/hardware/hdmi/HdmiClient.java
+++ b/core/java/android/hardware/hdmi/HdmiClient.java
@@ -1,12 +1,16 @@
 package android.hardware.hdmi;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.hardware.hdmi.HdmiControlManager.VendorCommandListener;
+import android.os.Binder;
 import android.os.RemoteException;
 import android.util.Log;
 
+import java.util.concurrent.Executor;
+
 /**
  * Parent for classes of various HDMI-CEC device type used to access
  * the HDMI control system service. Contains methods and data used in common.
@@ -28,16 +32,17 @@
     }
 
     /**
-     * Callback interface used to get the result of {@link #selectDevice}.
+     * Listener interface used to get the result of {@link #selectDevice}.
      */
-    public interface SelectDeviceCallback {
+    public interface OnDeviceSelectedListener {
         /**
          * Called when the operation is finished.
          * @param result the result value of {@link #selectDevice} and can have the values mentioned
          *               in {@link HdmiControlShellCommand#getResultString}
          * @param logicalAddress logical address of the selected device
          */
-        void onComplete(@HdmiControlManager.ControlCallbackResult int result, int logicalAddress);
+        void onDeviceSelected(@HdmiControlManager.ControlCallbackResult int result,
+                int logicalAddress);
     }
 
     /**
@@ -48,15 +53,19 @@
      * containing the result of that call only.
      *
      * @param logicalAddress logical address of the device to select
-     * @param callback callback to get the result with
-     * @throws {@link IllegalArgumentException} if the {@code callback} is null
+     * @param listener listener to get the result with
+     * @throws {@link IllegalArgumentException} if the {@code listener} is null
      */
-    public void selectDevice(int logicalAddress, @NonNull SelectDeviceCallback callback) {
-        if (callback == null) {
-            throw new IllegalArgumentException("callback must not be null.");
+    public void selectDevice(
+            int logicalAddress,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnDeviceSelectedListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null.");
         }
         try {
-            mService.deviceSelect(logicalAddress, getCallbackWrapper(callback, logicalAddress));
+            mService.deviceSelect(logicalAddress,
+                    getCallbackWrapper(logicalAddress, executor, listener));
         } catch (RemoteException e) {
             Log.e(TAG, "failed to select device: ", e);
         }
@@ -65,12 +74,14 @@
     /**
      * @hide
      */
-    private static IHdmiControlCallback getCallbackWrapper(final SelectDeviceCallback callback,
-            int logicalAddress) {
+    private static IHdmiControlCallback getCallbackWrapper(int logicalAddress,
+            final Executor executor, final OnDeviceSelectedListener listener) {
         return new IHdmiControlCallback.Stub() {
             @Override
             public void onComplete(int result) {
-                callback.onComplete(result, logicalAddress);
+                Binder.withCleanCallingIdentity(
+                        () -> executor.execute(() -> listener.onDeviceSelected(result,
+                                logicalAddress)));
             }
         };
     }
diff --git a/core/java/android/hardware/hdmi/HdmiSwitchClient.java b/core/java/android/hardware/hdmi/HdmiSwitchClient.java
index 79846cd..0fb5894 100644
--- a/core/java/android/hardware/hdmi/HdmiSwitchClient.java
+++ b/core/java/android/hardware/hdmi/HdmiSwitchClient.java
@@ -85,7 +85,6 @@
      * @see {@link android.media.tv.TvInputHardwareInfo#getHdmiPortId()}
      *     to get portId of a specific TV Input.
      * @param listener listener to get the result with
-     *
      * @hide
      */
     @SystemApi
@@ -104,8 +103,10 @@
      *
      * @param logicalAddress logical address of the device to select
      * @param listener       listener to get the result with
+     * @deprecated Please use {@link HdmiClient#selectDevice} instead.
      * @hide
      */
+    @Deprecated
     public void selectDevice(
             int logicalAddress,
             @NonNull @CallbackExecutor Executor executor,
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index c6a365e..aa3c361 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -590,10 +590,6 @@
         @Override
         public final void initializeInternal(@NonNull IBinder token,
                 IInputMethodPrivilegedOperations privilegedOperations, int configChanges) {
-            if (InputMethodPrivilegedOperationsRegistry.isRegistered(token)) {
-                Log.w(TAG, "The token has already registered, ignore this initialization.");
-                return;
-            }
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMS.initializeInternal");
             mConfigTracker.onInitialize(configChanges);
             mPrivOps.set(privilegedOperations);
@@ -789,11 +785,6 @@
                 return;
             }
 
-            if (Trace.isEnabled()) {
-                Binder.enableTracing();
-            } else {
-                Binder.disableTracing();
-            }
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMS.showSoftInput");
             ImeTracing.getInstance().triggerServiceDump(
                     "InputMethodService.InputMethodImpl#showSoftInput", mDumper,
diff --git a/core/java/android/net/OWNERS b/core/java/android/net/OWNERS
index 4ea8a54..f55bcd3 100644
--- a/core/java/android/net/OWNERS
+++ b/core/java/android/net/OWNERS
@@ -2,4 +2,5 @@
 
 include platform/frameworks/base:/services/core/java/com/android/server/net/OWNERS
 
-per-file SSL*, Uri*, Url* = prb@google.com, dauletz@google.com, narayan@google.com, ngeoffray@google.com
+per-file SSL*,Uri*,Url* = prb@google.com,oth@google.com,narayan@google.com,ngeoffray@google.com
+per-file SntpClient* = file:/services/core/java/com/android/server/timedetector/OWNERS
diff --git a/core/java/android/net/SntpClient.java b/core/java/android/net/SntpClient.java
index f6852e6..0eb4cf3 100644
--- a/core/java/android/net/SntpClient.java
+++ b/core/java/android/net/SntpClient.java
@@ -17,16 +17,26 @@
 package android.net;
 
 import android.compat.annotation.UnsupportedAppUsage;
+import android.net.sntp.Duration64;
+import android.net.sntp.Timestamp64;
 import android.os.SystemClock;
 import android.util.Log;
+import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.TrafficStatsConstants;
 
 import java.net.DatagramPacket;
 import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
-import java.util.Arrays;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Random;
+import java.util.function.Supplier;
 
 /**
  * {@hide}
@@ -60,17 +70,21 @@
     private static final int NTP_STRATUM_DEATH = 0;
     private static final int NTP_STRATUM_MAX = 15;
 
-    // Number of seconds between Jan 1, 1900 and Jan 1, 1970
-    // 70 years plus 17 leap days
-    private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L;
+    // The source of the current system clock time, replaceable for testing.
+    private final Supplier<Instant> mSystemTimeSupplier;
 
-    // system time computed from NTP server response
+    private final Random mRandom;
+
+    // The last offset calculated from an NTP server response
+    private long mClockOffset;
+
+    // The last system time computed from an NTP server response
     private long mNtpTime;
 
-    // value of SystemClock.elapsedRealtime() corresponding to mNtpTime
+    // The value of SystemClock.elapsedRealtime() corresponding to mNtpTime / mClockOffset
     private long mNtpTimeReference;
 
-    // round trip time in milliseconds
+    // The round trip (network) time in milliseconds
     private long mRoundTripTime;
 
     private static class InvalidServerReplyException extends Exception {
@@ -81,6 +95,13 @@
 
     @UnsupportedAppUsage
     public SntpClient() {
+        this(Instant::now, defaultRandom());
+    }
+
+    @VisibleForTesting
+    public SntpClient(Supplier<Instant> systemTimeSupplier, Random random) {
+        mSystemTimeSupplier = Objects.requireNonNull(systemTimeSupplier);
+        mRandom = Objects.requireNonNull(random);
     }
 
     /**
@@ -126,9 +147,13 @@
             buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3);
 
             // get current time and write it to the request packet
-            final long requestTime = System.currentTimeMillis();
+            final Instant requestTime = mSystemTimeSupplier.get();
+            final Timestamp64 requestTimestamp = Timestamp64.fromInstant(requestTime);
+
+            final Timestamp64 randomizedRequestTimestamp =
+                    requestTimestamp.randomizeSubMillis(mRandom);
             final long requestTicks = SystemClock.elapsedRealtime();
-            writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, requestTime);
+            writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, randomizedRequestTimestamp);
 
             socket.send(request);
 
@@ -136,42 +161,44 @@
             DatagramPacket response = new DatagramPacket(buffer, buffer.length);
             socket.receive(response);
             final long responseTicks = SystemClock.elapsedRealtime();
-            final long responseTime = requestTime + (responseTicks - requestTicks);
+            final Instant responseTime = requestTime.plusMillis(responseTicks - requestTicks);
+            final Timestamp64 responseTimestamp = Timestamp64.fromInstant(responseTime);
 
             // extract the results
             final byte leap = (byte) ((buffer[0] >> 6) & 0x3);
             final byte mode = (byte) (buffer[0] & 0x7);
             final int stratum = (int) (buffer[1] & 0xff);
-            final long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET);
-            final long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET);
-            final long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET);
-            final long referenceTime = readTimeStamp(buffer, REFERENCE_TIME_OFFSET);
+            final Timestamp64 referenceTimestamp = readTimeStamp(buffer, REFERENCE_TIME_OFFSET);
+            final Timestamp64 originateTimestamp = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET);
+            final Timestamp64 receiveTimestamp = readTimeStamp(buffer, RECEIVE_TIME_OFFSET);
+            final Timestamp64 transmitTimestamp = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET);
 
             /* Do validation according to RFC */
-            // TODO: validate originateTime == requestTime.
-            checkValidServerReply(leap, mode, stratum, transmitTime, referenceTime);
+            checkValidServerReply(leap, mode, stratum, transmitTimestamp, referenceTimestamp,
+                    randomizedRequestTimestamp, originateTimestamp);
 
-            long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime);
-            // receiveTime = originateTime + transit + skew
-            // responseTime = transmitTime + transit - skew
-            // clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2
-            //             = ((originateTime + transit + skew - originateTime) +
-            //                (transmitTime - (transmitTime + transit - skew)))/2
-            //             = ((transit + skew) + (transmitTime - transmitTime - transit + skew))/2
-            //             = (transit + skew - transit + skew)/2
-            //             = (2 * skew)/2 = skew
-            long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2;
-            EventLogTags.writeNtpSuccess(address.toString(), roundTripTime, clockOffset);
+            long totalTransactionDurationMillis = responseTicks - requestTicks;
+            long serverDurationMillis =
+                    Duration64.between(receiveTimestamp, transmitTimestamp).toDuration().toMillis();
+            long roundTripTimeMillis = totalTransactionDurationMillis - serverDurationMillis;
+
+            Duration clockOffsetDuration = calculateClockOffset(requestTimestamp,
+                    receiveTimestamp, transmitTimestamp, responseTimestamp);
+            long clockOffsetMillis = clockOffsetDuration.toMillis();
+
+            EventLogTags.writeNtpSuccess(
+                    address.toString(), roundTripTimeMillis, clockOffsetMillis);
             if (DBG) {
-                Log.d(TAG, "round trip: " + roundTripTime + "ms, " +
-                        "clock offset: " + clockOffset + "ms");
+                Log.d(TAG, "round trip: " + roundTripTimeMillis + "ms, "
+                        + "clock offset: " + clockOffsetMillis + "ms");
             }
 
             // save our results - use the times on this side of the network latency
             // (response rather than request time)
-            mNtpTime = responseTime + clockOffset;
+            mClockOffset = clockOffsetMillis;
+            mNtpTime = responseTime.plus(clockOffsetDuration).toEpochMilli();
             mNtpTimeReference = responseTicks;
-            mRoundTripTime = roundTripTime;
+            mRoundTripTime = roundTripTimeMillis;
         } catch (Exception e) {
             EventLogTags.writeNtpFailure(address.toString(), e.toString());
             if (DBG) Log.d(TAG, "request time failed: " + e);
@@ -186,6 +213,28 @@
         return true;
     }
 
+    /** Performs the NTP clock offset calculation. */
+    @VisibleForTesting
+    public static Duration calculateClockOffset(Timestamp64 clientRequestTimestamp,
+            Timestamp64 serverReceiveTimestamp, Timestamp64 serverTransmitTimestamp,
+            Timestamp64 clientResponseTimestamp) {
+        // According to RFC4330:
+        // t is the system clock offset (the adjustment we are trying to find)
+        // t = ((T2 - T1) + (T3 - T4)) / 2
+        //
+        // Which is:
+        // t = (([server]receiveTimestamp - [client]requestTimestamp)
+        //       + ([server]transmitTimestamp - [client]responseTimestamp)) / 2
+        //
+        // See the NTP spec and tests: the numeric types used are deliberate:
+        // + Duration64.between() uses 64-bit arithmetic (32-bit for the seconds).
+        // + plus() / dividedBy() use Duration, which isn't the double precision floating point
+        //   used in NTPv4, but is good enough.
+        return Duration64.between(clientRequestTimestamp, serverReceiveTimestamp)
+                .plus(Duration64.between(clientResponseTimestamp, serverTransmitTimestamp))
+                .dividedBy(2);
+    }
+
     @Deprecated
     @UnsupportedAppUsage
     public boolean requestTime(String host, int timeout) {
@@ -194,6 +243,14 @@
     }
 
     /**
+     * Returns the offset calculated to apply to the client clock to arrive at {@link #getNtpTime()}
+     */
+    @VisibleForTesting
+    public long getClockOffset() {
+        return mClockOffset;
+    }
+
+    /**
      * Returns the time computed from the NTP transaction.
      *
      * @return time value computed from NTP server response.
@@ -225,8 +282,9 @@
     }
 
     private static void checkValidServerReply(
-            byte leap, byte mode, int stratum, long transmitTime, long referenceTime)
-            throws InvalidServerReplyException {
+            byte leap, byte mode, int stratum, Timestamp64 transmitTimestamp,
+            Timestamp64 referenceTimestamp, Timestamp64 randomizedRequestTimestamp,
+            Timestamp64 originateTimestamp) throws InvalidServerReplyException {
         if (leap == NTP_LEAP_NOSYNC) {
             throw new InvalidServerReplyException("unsynchronized server");
         }
@@ -236,73 +294,68 @@
         if ((stratum == NTP_STRATUM_DEATH) || (stratum > NTP_STRATUM_MAX)) {
             throw new InvalidServerReplyException("untrusted stratum: " + stratum);
         }
-        if (transmitTime == 0) {
-            throw new InvalidServerReplyException("zero transmitTime");
+        if (!randomizedRequestTimestamp.equals(originateTimestamp)) {
+            throw new InvalidServerReplyException(
+                    "originateTimestamp != randomizedRequestTimestamp");
         }
-        if (referenceTime == 0) {
-            throw new InvalidServerReplyException("zero reference timestamp");
+        if (transmitTimestamp.equals(Timestamp64.ZERO)) {
+            throw new InvalidServerReplyException("zero transmitTimestamp");
+        }
+        if (referenceTimestamp.equals(Timestamp64.ZERO)) {
+            throw new InvalidServerReplyException("zero referenceTimestamp");
         }
     }
 
     /**
      * Reads an unsigned 32 bit big endian number from the given offset in the buffer.
      */
-    private long read32(byte[] buffer, int offset) {
-        byte b0 = buffer[offset];
-        byte b1 = buffer[offset+1];
-        byte b2 = buffer[offset+2];
-        byte b3 = buffer[offset+3];
+    private long readUnsigned32(byte[] buffer, int offset) {
+        int i0 = buffer[offset++] & 0xFF;
+        int i1 = buffer[offset++] & 0xFF;
+        int i2 = buffer[offset++] & 0xFF;
+        int i3 = buffer[offset] & 0xFF;
 
-        // convert signed bytes to unsigned values
-        int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0);
-        int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1);
-        int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2);
-        int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3);
-
-        return ((long)i0 << 24) + ((long)i1 << 16) + ((long)i2 << 8) + (long)i3;
+        int bits = (i0 << 24) | (i1 << 16) | (i2 << 8) | i3;
+        return bits & 0xFFFF_FFFFL;
     }
 
     /**
-     * Reads the NTP time stamp at the given offset in the buffer and returns
-     * it as a system time (milliseconds since January 1, 1970).
+     * Reads the NTP time stamp from the given offset in the buffer.
      */
-    private long readTimeStamp(byte[] buffer, int offset) {
-        long seconds = read32(buffer, offset);
-        long fraction = read32(buffer, offset + 4);
-        // Special case: zero means zero.
-        if (seconds == 0 && fraction == 0) {
-            return 0;
-        }
-        return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L);
+    private Timestamp64 readTimeStamp(byte[] buffer, int offset) {
+        long seconds = readUnsigned32(buffer, offset);
+        int fractionBits = (int) readUnsigned32(buffer, offset + 4);
+        return Timestamp64.fromComponents(seconds, fractionBits);
     }
 
     /**
-     * Writes system time (milliseconds since January 1, 1970) as an NTP time stamp
-     * at the given offset in the buffer.
+     * Writes the NTP time stamp at the given offset in the buffer.
      */
-    private void writeTimeStamp(byte[] buffer, int offset, long time) {
-        // Special case: zero means zero.
-        if (time == 0) {
-            Arrays.fill(buffer, offset, offset + 8, (byte) 0x00);
-            return;
-        }
-
-        long seconds = time / 1000L;
-        long milliseconds = time - seconds * 1000L;
-        seconds += OFFSET_1900_TO_1970;
-
+    private void writeTimeStamp(byte[] buffer, int offset, Timestamp64 timestamp) {
+        long seconds = timestamp.getEraSeconds();
         // write seconds in big endian format
-        buffer[offset++] = (byte)(seconds >> 24);
-        buffer[offset++] = (byte)(seconds >> 16);
-        buffer[offset++] = (byte)(seconds >> 8);
-        buffer[offset++] = (byte)(seconds >> 0);
+        buffer[offset++] = (byte) (seconds >>> 24);
+        buffer[offset++] = (byte) (seconds >>> 16);
+        buffer[offset++] = (byte) (seconds >>> 8);
+        buffer[offset++] = (byte) (seconds);
 
-        long fraction = milliseconds * 0x100000000L / 1000L;
+        int fractionBits = timestamp.getFractionBits();
         // write fraction in big endian format
-        buffer[offset++] = (byte)(fraction >> 24);
-        buffer[offset++] = (byte)(fraction >> 16);
-        buffer[offset++] = (byte)(fraction >> 8);
-        // low order bits should be random data
-        buffer[offset++] = (byte)(Math.random() * 255.0);
+        buffer[offset++] = (byte) (fractionBits >>> 24);
+        buffer[offset++] = (byte) (fractionBits >>> 16);
+        buffer[offset++] = (byte) (fractionBits >>> 8);
+        buffer[offset] = (byte) (fractionBits);
+    }
+
+    private static Random defaultRandom() {
+        Random random;
+        try {
+            random = SecureRandom.getInstanceStrong();
+        } catch (NoSuchAlgorithmException e) {
+            // This should never happen.
+            Slog.wtf(TAG, "Unable to access SecureRandom", e);
+            random = new Random(System.currentTimeMillis());
+        }
+        return random;
     }
 }
diff --git a/core/java/android/net/sntp/Duration64.java b/core/java/android/net/sntp/Duration64.java
new file mode 100644
index 0000000..939b289
--- /dev/null
+++ b/core/java/android/net/sntp/Duration64.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.sntp;
+
+import java.time.Duration;
+
+/**
+ * A type similar to {@link Timestamp64} but used when calculating the difference between two
+ * timestamps. As such, it is a signed type, but still uses 64-bits in total and so can only
+ * represent half the magnitude of {@link Timestamp64}.
+ *
+ * <p>See <a href="https://www.eecis.udel.edu/~mills/time.html">4. Time Difference Calculations</a>.
+ *
+ * @hide
+ */
+public class Duration64 {
+
+    public static final Duration64 ZERO = new Duration64(0);
+    private final long mBits;
+
+    private Duration64(long bits) {
+        this.mBits = bits;
+    }
+
+    /**
+     * Returns the difference between two 64-bit NTP timestamps as a {@link Duration64}, as
+     * described in the NTP spec. The times represented by the timestamps have to be within {@link
+     * Timestamp64#MAX_SECONDS_IN_ERA} (~68 years) of each other for the calculation to produce a
+     * correct answer.
+     */
+    public static Duration64 between(Timestamp64 startInclusive, Timestamp64 endExclusive) {
+        long oneBits = (startInclusive.getEraSeconds() << 32)
+                | (startInclusive.getFractionBits() & 0xFFFF_FFFFL);
+        long twoBits = (endExclusive.getEraSeconds() << 32)
+                | (endExclusive.getFractionBits() & 0xFFFF_FFFFL);
+        long resultBits = twoBits - oneBits;
+        return new Duration64(resultBits);
+    }
+
+    /**
+     * Add two {@link Duration64} instances together. This performs the calculation in {@link
+     * Duration} and returns a {@link Duration} to increase the magnitude of accepted arguments,
+     * since {@link Duration64} only supports signed 32-bit seconds. The use of {@link Duration}
+     * limits precision to nanoseconds.
+     */
+    public Duration plus(Duration64 other) {
+        // From https://www.eecis.udel.edu/~mills/time.html:
+        // "The offset and delay calculations require sums and differences of these raw timestamp
+        // differences that can span no more than from 34 years in the future to 34 years in the
+        // past without overflow. This is a fundamental limitation in 64-bit integer calculations.
+        //
+        // In the NTPv4 reference implementation, all calculations involving offset and delay values
+        // use 64-bit floating double arithmetic, with the exception of raw timestamp subtraction,
+        // as mentioned above. The raw timestamp differences are then converted to 64-bit floating
+        // double format without loss of precision or chance of overflow in subsequent
+        // calculations."
+        //
+        // Here, we use Duration instead, which provides sufficient range, but loses precision below
+        // nanos.
+        return this.toDuration().plus(other.toDuration());
+    }
+
+    /**
+     * Returns a {@link Duration64} equivalent of the supplied duration, if the magnitude can be
+     * represented. Because {@link Duration64} uses a fixed point type for sub-second values it
+     * cannot represent all nanosecond values precisely and so the conversion can be lossy.
+     *
+     * @throws IllegalArgumentException if the supplied duration is too big to be represented
+     */
+    public static Duration64 fromDuration(Duration duration) {
+        long seconds = duration.getSeconds();
+        if (seconds < Integer.MIN_VALUE || seconds > Integer.MAX_VALUE) {
+            throw new IllegalArgumentException();
+        }
+        long bits = (seconds << 32)
+                | (Timestamp64.nanosToFractionBits(duration.getNano()) & 0xFFFF_FFFFL);
+        return new Duration64(bits);
+    }
+
+    /**
+     * Returns a {@link Duration} equivalent of this duration. Because {@link Duration64} uses a
+     * fixed point type for sub-second values it can values smaller than nanosecond precision and so
+     * the conversion can be lossy.
+     */
+    public Duration toDuration() {
+        int seconds = getSeconds();
+        int nanos = getNanos();
+        return Duration.ofSeconds(seconds, nanos);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        Duration64 that = (Duration64) o;
+        return mBits == that.mBits;
+    }
+
+    @Override
+    public int hashCode() {
+        return java.util.Objects.hash(mBits);
+    }
+
+    @Override
+    public String toString() {
+        Duration duration = toDuration();
+        return Long.toHexString(mBits)
+                + "(" + duration.getSeconds() + "s " + duration.getNano() + "ns)";
+    }
+
+    /**
+     * Returns the <em>signed</em> seconds in this duration.
+     */
+    public int getSeconds() {
+        return (int) (mBits >> 32);
+    }
+
+    /**
+     * Returns the <em>unsigned</em> nanoseconds in this duration (truncated).
+     */
+    public int getNanos() {
+        return Timestamp64.fractionBitsToNanos((int) (mBits & 0xFFFF_FFFFL));
+    }
+}
diff --git a/core/java/android/net/sntp/OWNERS b/core/java/android/net/sntp/OWNERS
new file mode 100644
index 0000000..9a3e264
--- /dev/null
+++ b/core/java/android/net/sntp/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/timedetector/OWNERS
diff --git a/core/java/android/net/sntp/Timestamp64.java b/core/java/android/net/sntp/Timestamp64.java
new file mode 100644
index 0000000..81a3310
--- /dev/null
+++ b/core/java/android/net/sntp/Timestamp64.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.sntp;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Random;
+
+/**
+ * The 64-bit type ("timestamp") that NTP uses to represent a point in time. It only holds the
+ * lowest 32-bits of the number of seconds since 1900-01-01 00:00:00. Consequently, to turn an
+ * instance into an unambiguous point in time the era number must be known. Era zero runs from
+ * 1900-01-01 00:00:00 to a date in 2036.
+ *
+ * It stores sub-second values using a 32-bit fixed point type, so it can resolve values smaller
+ * than a nanosecond, but is imprecise (i.e. it truncates).
+ *
+ * See also <a href=https://www.eecis.udel.edu/~mills/y2k.html>NTP docs</a>.
+ *
+ * @hide
+ */
+public final class Timestamp64 {
+
+    public static final Timestamp64 ZERO = fromComponents(0, 0);
+    static final int SUB_MILLIS_BITS_TO_RANDOMIZE = 32 - 10;
+
+    // Number of seconds between Jan 1, 1900 and Jan 1, 1970
+    // 70 years plus 17 leap days
+    static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L;
+    static final long MAX_SECONDS_IN_ERA = 0xFFFF_FFFFL;
+    static final long SECONDS_IN_ERA = MAX_SECONDS_IN_ERA + 1;
+
+    static final int NANOS_PER_SECOND = 1_000_000_000;
+
+    /** Creates a {@link Timestamp64} from the seconds and fraction components. */
+    public static Timestamp64 fromComponents(long eraSeconds, int fractionBits) {
+        return new Timestamp64(eraSeconds, fractionBits);
+    }
+
+    /** Creates a {@link Timestamp64} by decoding a string in the form "e4dc720c.4d4fc9eb". */
+    public static Timestamp64 fromString(String string) {
+        final int requiredLength = 17;
+        if (string.length() != requiredLength || string.charAt(8) != '.') {
+            throw new IllegalArgumentException(string);
+        }
+        String eraSecondsString = string.substring(0, 8);
+        String fractionString = string.substring(9);
+        long eraSeconds = Long.parseLong(eraSecondsString, 16);
+
+        // Use parseLong() because the type is unsigned. Integer.parseInt() will reject 0x70000000
+        // or above as being out of range.
+        long fractionBitsAsLong = Long.parseLong(fractionString, 16);
+        if (fractionBitsAsLong < 0 || fractionBitsAsLong > 0xFFFFFFFFL) {
+            throw new IllegalArgumentException("Invalid fractionBits:" + fractionString);
+        }
+        return new Timestamp64(eraSeconds, (int) fractionBitsAsLong);
+    }
+
+    /**
+     * Converts an {@link Instant} into a {@link Timestamp64}. This is lossy: Timestamp64 only
+     * contains the number of seconds in a given era, but the era is not stored. Also, sub-second
+     * values are not stored precisely.
+     */
+    public static Timestamp64 fromInstant(Instant instant) {
+        long ntpEraSeconds = instant.getEpochSecond() + OFFSET_1900_TO_1970;
+        if (ntpEraSeconds < 0) {
+            ntpEraSeconds = SECONDS_IN_ERA - (-ntpEraSeconds % SECONDS_IN_ERA);
+        }
+        ntpEraSeconds %= SECONDS_IN_ERA;
+
+        long nanos = instant.getNano();
+        int fractionBits = nanosToFractionBits(nanos);
+
+        return new Timestamp64(ntpEraSeconds, fractionBits);
+    }
+
+    private final long mEraSeconds;
+    private final int mFractionBits;
+
+    private Timestamp64(long eraSeconds, int fractionBits) {
+        if (eraSeconds < 0 || eraSeconds > MAX_SECONDS_IN_ERA) {
+            throw new IllegalArgumentException(
+                    "Invalid parameters. seconds=" + eraSeconds + ", fraction=" + fractionBits);
+        }
+        this.mEraSeconds = eraSeconds;
+        this.mFractionBits = fractionBits;
+    }
+
+    /** Returns the number of seconds in the NTP era. */
+    public long getEraSeconds() {
+        return mEraSeconds;
+    }
+
+    /** Returns the fraction of a second as 32-bit, unsigned fixed-point bits. */
+    public int getFractionBits() {
+        return mFractionBits;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%08x.%08x", mEraSeconds, mFractionBits);
+    }
+
+    /** Returns the instant represented by this value in the specified NTP era. */
+    public Instant toInstant(int ntpEra) {
+        long secondsSinceEpoch = mEraSeconds - OFFSET_1900_TO_1970;
+        secondsSinceEpoch += ntpEra * SECONDS_IN_ERA;
+
+        int nanos = fractionBitsToNanos(mFractionBits);
+        return Instant.ofEpochSecond(secondsSinceEpoch, nanos);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        Timestamp64 that = (Timestamp64) o;
+        return mEraSeconds == that.mEraSeconds && mFractionBits == that.mFractionBits;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mEraSeconds, mFractionBits);
+    }
+
+    static int fractionBitsToNanos(int fractionBits) {
+        long fractionBitsLong = fractionBits & 0xFFFF_FFFFL;
+        return (int) ((fractionBitsLong * NANOS_PER_SECOND) >>> 32);
+    }
+
+    static int nanosToFractionBits(long nanos) {
+        if (nanos > NANOS_PER_SECOND) {
+            throw new IllegalArgumentException();
+        }
+        return (int) ((nanos << 32) / NANOS_PER_SECOND);
+    }
+
+    /**
+     * Randomizes the fraction bits that represent sub-millisecond values. i.e. the randomization
+     * won't change the number of milliseconds represented after truncation. This is used to
+     * implement the part of the NTP spec that calls for clients with millisecond accuracy clocks
+     * to send randomized LSB values rather than zeros.
+     */
+    public Timestamp64 randomizeSubMillis(Random random) {
+        int randomizedFractionBits =
+                randomizeLowestBits(random, this.mFractionBits, SUB_MILLIS_BITS_TO_RANDOMIZE);
+        return new Timestamp64(mEraSeconds, randomizedFractionBits);
+    }
+
+    /**
+     * Randomizes the specified number of LSBs in {@code value} by using replacement bits from
+     * {@code Random.getNextInt()}.
+     */
+    @VisibleForTesting
+    public static int randomizeLowestBits(Random random, int value, int bitsToRandomize) {
+        if (bitsToRandomize < 1 || bitsToRandomize >= Integer.SIZE) {
+            // There's no point in randomizing all bits or none of the bits.
+            throw new IllegalArgumentException(Integer.toString(bitsToRandomize));
+        }
+
+        int upperBitMask = 0xFFFF_FFFF << bitsToRandomize;
+        int lowerBitMask = ~upperBitMask;
+
+        int randomValue = random.nextInt();
+        return (value & upperBitMask) | (randomValue & lowerBitMask);
+    }
+}
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index 7ce8d72..2a344f6 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -43,18 +43,22 @@
     protected static final String TAG = "Bundle";
     static final boolean DEBUG = false;
 
-    // Keep them in sync with frameworks/native/libs/binder/PersistableBundle.cpp.
-    private static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L'
+    /**
+     * Keep them in sync with frameworks/native/libs/binder/PersistableBundle.cpp.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L'
     private static final int BUNDLE_MAGIC_NATIVE = 0x4C444E44; // 'B' 'N' 'D' 'N'
 
     /**
-     * Flag indicating that this Bundle is okay to "defuse." That is, it's okay
-     * for system processes to ignore any {@link BadParcelableException}
-     * encountered when unparceling it, leaving an empty bundle in its place.
+     * Flag indicating that this Bundle is okay to "defuse", see {@link #setShouldDefuse(boolean)}
+     * for more details.
      * <p>
-     * This should <em>only</em> be set when the Bundle reaches its final
-     * destination, otherwise a system process may clobber contents that were
-     * destined for an app that could have unparceled them.
+     * This should <em>only</em> be set when the Bundle reaches its final destination, otherwise a
+     * system process may clobber contents that were destined for an app that could have unparceled
+     * them.
      */
     static final int FLAG_DEFUSABLE = 1 << 0;
 
@@ -63,10 +67,15 @@
     private static volatile boolean sShouldDefuse = false;
 
     /**
-     * Set global variable indicating that any Bundles parsed in this process
-     * should be "defused." That is, any {@link BadParcelableException}
-     * encountered will be suppressed and logged, leaving an empty Bundle
-     * instead of crashing.
+     * Set global variable indicating that any Bundles parsed in this process should be "defused".
+     * That is, any {@link BadParcelableException} encountered will be suppressed and logged. Also:
+     * <ul>
+     *   <li>If it was the deserialization of a custom item (eg. {@link Parcelable}) that caused the
+     *   exception, {@code null} will be returned but the item will be held in the map in its
+     *   serialized form (lazy value).
+     *   <li>If the exception happened during partial deserialization, that is, during the read of
+     *   the map and its basic types (while skipping custom types), the map will be left empty.
+     * </ul>
      *
      * @hide
      */
@@ -249,6 +258,12 @@
                 }
             }
             if (itemwise) {
+                if (LOG_DEFUSABLE && sShouldDefuse && (mFlags & FLAG_DEFUSABLE) == 0) {
+                    Slog.wtf(TAG,
+                            "Attempting to unparcel all items in a Bundle while in transit; this "
+                                    + "may remove elements intended for the final desitination.",
+                            new Throwable());
+                }
                 for (int i = 0, n = mMap.size(); i < n; i++) {
                     // Triggers deserialization of i-th item, if needed
                     getValueAt(i);
@@ -281,7 +296,16 @@
     final Object getValueAt(int i) {
         Object object = mMap.valueAt(i);
         if (object instanceof Supplier<?>) {
-            object = ((Supplier<?>) object).get();
+            try {
+                object = ((Supplier<?>) object).get();
+            } catch (BadParcelableException e) {
+                if (sShouldDefuse) {
+                    Log.w(TAG, "Failed to parse item " + mMap.keyAt(i) + ", returning null.", e);
+                    return null;
+                } else {
+                    throw e;
+                }
+            }
             mMap.setValueAt(i, object);
         }
         return object;
@@ -289,11 +313,6 @@
 
     private void initializeFromParcelLocked(@NonNull Parcel parcelledData, boolean recycleParcel,
             boolean parcelledByNative) {
-        if (LOG_DEFUSABLE && sShouldDefuse && (mFlags & FLAG_DEFUSABLE) == 0) {
-            Slog.wtf(TAG, "Attempting to unparcel a Bundle while in transit; this may "
-                    + "clobber all data inside!", new Throwable());
-        }
-
         if (isEmptyParcel(parcelledData)) {
             if (DEBUG) {
                 Log.d(TAG, "unparcel "
@@ -376,8 +395,16 @@
         }
     }
 
-    /** @hide */
-    ArrayMap<String, Object> getMap() {
+    /**
+     * Returns the backing map of this bundle after deserializing every item.
+     *
+     * <p><b>Warning:</b> This method will deserialize every item on the bundle, including custom
+     * types such as {@link Parcelable} and {@link Serializable}, so only use this when you trust
+     * the source. Specifically don't use this method on app-provided bundles.
+     *
+     * @hide
+     */
+    ArrayMap<String, Object> getItemwiseMap() {
         unparcel(/* itemwise */ true);
         return mMap;
     }
@@ -500,7 +527,7 @@
                     final int N = fromMap.size();
                     mMap = new ArrayMap<>(N);
                     for (int i = 0; i < N; i++) {
-                        mMap.append(fromMap.keyAt(i), deepCopyValue(from.getValueAt(i)));
+                        mMap.append(fromMap.keyAt(i), deepCopyValue(fromMap.valueAt(i)));
                     }
                 }
             } else {
@@ -1772,7 +1799,7 @@
             pw.println("[null]");
             return;
         }
-        final ArrayMap<String, Object> map = bundle.getMap();
+        final ArrayMap<String, Object> map = bundle.getItemwiseMap();
         for (int i = 0; i < map.size(); i++) {
             dumpStats(pw, map.keyAt(i), map.valueAt(i));
         }
diff --git a/core/java/android/os/BatteryConsumer.java b/core/java/android/os/BatteryConsumer.java
index 34f079a..1c1fc2c 100644
--- a/core/java/android/os/BatteryConsumer.java
+++ b/core/java/android/os/BatteryConsumer.java
@@ -26,6 +26,7 @@
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 
 /**
  * Interface for objects containing battery attribution data.
@@ -43,6 +44,7 @@
      * @hide
      */
     @IntDef(prefix = {"POWER_COMPONENT_"}, value = {
+            POWER_COMPONENT_ANY,
             POWER_COMPONENT_SCREEN,
             POWER_COMPONENT_CPU,
             POWER_COMPONENT_BLUETOOTH,
@@ -65,6 +67,7 @@
     public static @interface PowerComponent {
     }
 
+    public static final int POWER_COMPONENT_ANY = -1;
     public static final int POWER_COMPONENT_SCREEN = OsProtoEnums.POWER_COMPONENT_SCREEN; // 0
     public static final int POWER_COMPONENT_CPU = OsProtoEnums.POWER_COMPONENT_CPU; // 1
     public static final int POWER_COMPONENT_BLUETOOTH = OsProtoEnums.POWER_COMPONENT_BLUETOOTH; // 2
@@ -151,9 +154,145 @@
      */
     public static final int POWER_MODEL_MEASURED_ENERGY = 2;
 
+    /**
+     * Identifiers of consumed power aggregations.
+     *
+     * @hide
+     */
+    @IntDef(prefix = {"PROCESS_STATE_"}, value = {
+            PROCESS_STATE_ANY,
+            PROCESS_STATE_FOREGROUND,
+            PROCESS_STATE_BACKGROUND,
+            PROCESS_STATE_FOREGROUND_SERVICE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ProcessState {
+    }
+
+    public static final int PROCESS_STATE_ANY = 0;
+    public static final int PROCESS_STATE_FOREGROUND = 1;
+    public static final int PROCESS_STATE_BACKGROUND = 2;
+    public static final int PROCESS_STATE_FOREGROUND_SERVICE = 3;
+
+    static final int PROCESS_STATE_COUNT = 4;
+
+    private static final String[] sProcessStateNames = new String[PROCESS_STATE_COUNT];
+
+    static {
+        // Assign individually to avoid future mismatch
+        sProcessStateNames[PROCESS_STATE_ANY] = "any";
+        sProcessStateNames[PROCESS_STATE_FOREGROUND] = "fg";
+        sProcessStateNames[PROCESS_STATE_BACKGROUND] = "bg";
+        sProcessStateNames[PROCESS_STATE_FOREGROUND_SERVICE] = "fgs";
+    }
+
+    private static final int[] SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE = {
+            POWER_COMPONENT_CPU,
+    };
+
     static final int COLUMN_INDEX_BATTERY_CONSUMER_TYPE = 0;
     static final int COLUMN_COUNT = 1;
 
+    /**
+     * Identifies power attribution dimensions that a caller is interested in.
+     */
+    public static final class Dimensions {
+        public final @PowerComponent int powerComponent;
+        public final @ProcessState int processState;
+
+        public Dimensions(int powerComponent, int processState) {
+            this.powerComponent = powerComponent;
+            this.processState = processState;
+        }
+
+        @Override
+        public String toString() {
+            boolean dimensionSpecified = false;
+            StringBuilder sb = new StringBuilder();
+            if (powerComponent != POWER_COMPONENT_ANY) {
+                sb.append("powerComponent=").append(sPowerComponentNames[powerComponent]);
+                dimensionSpecified = true;
+            }
+            if (processState != PROCESS_STATE_ANY) {
+                if (dimensionSpecified) {
+                    sb.append(", ");
+                }
+                sb.append("processState=").append(sProcessStateNames[processState]);
+                dimensionSpecified = true;
+            }
+            if (!dimensionSpecified) {
+                sb.append("any components and process states");
+            }
+            return sb.toString();
+        }
+    }
+
+    public static final Dimensions UNSPECIFIED_DIMENSIONS =
+            new Dimensions(POWER_COMPONENT_ANY, PROCESS_STATE_ANY);
+
+    /**
+     * Identifies power attribution dimensions that are captured by an data element of
+     * a BatteryConsumer. These Keys are used to access those values and to set them using
+     * Builders.  See for example {@link #getConsumedPower(Key)}.
+     *
+     * Keys cannot be allocated by the client - they can only be obtained by calling
+     * {@link #getKeys} or {@link #getKey}.  All BatteryConsumers that are part of the
+     * same BatteryUsageStats share the same set of keys, therefore it is safe to obtain
+     * the keys from one BatteryConsumer and apply them to other BatteryConsumers
+     * in the same BatteryUsageStats.
+     */
+    public static final class Key {
+        public final @PowerComponent int powerComponent;
+        public final @ProcessState int processState;
+
+        final int mPowerModelColumnIndex;
+        final int mPowerColumnIndex;
+        final int mDurationColumnIndex;
+        private String mShortString;
+
+        private Key(int powerComponent, int processState, int powerModelColumnIndex,
+                int powerColumnIndex, int durationColumnIndex) {
+            this.powerComponent = powerComponent;
+            this.processState = processState;
+
+            mPowerModelColumnIndex = powerModelColumnIndex;
+            mPowerColumnIndex = powerColumnIndex;
+            mDurationColumnIndex = durationColumnIndex;
+        }
+
+        @SuppressWarnings("EqualsUnsafeCast")
+        @Override
+        public boolean equals(Object o) {
+            // Skipping null and class check for performance
+            final Key key = (Key) o;
+            return powerComponent == key.powerComponent
+                && processState == key.processState;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = powerComponent;
+            result = 31 * result + processState;
+            return result;
+        }
+
+        /**
+         * Returns a string suitable for use in dumpsys.
+         */
+        public String toShortString() {
+            if (mShortString == null) {
+                StringBuilder sb = new StringBuilder();
+                sb.append(powerComponentIdToString(powerComponent));
+                if (processState != PROCESS_STATE_ANY) {
+                    sb.append(':');
+                    sb.append(processStateToString(processState));
+                }
+                mShortString = sb.toString();
+            }
+            return mShortString;
+        }
+    }
+
     protected final BatteryConsumerData mData;
     protected final PowerComponents mPowerComponents;
 
@@ -171,7 +310,37 @@
      * Total power consumed by this consumer, in mAh.
      */
     public double getConsumedPower() {
-        return mPowerComponents.getConsumedPower();
+        return mPowerComponents.getConsumedPower(UNSPECIFIED_DIMENSIONS);
+    }
+
+    /**
+     * Returns power consumed aggregated over the specified dimensions, in mAh.
+     */
+    public double getConsumedPower(Dimensions dimensions) {
+        return mPowerComponents.getConsumedPower(dimensions);
+    }
+
+    /**
+     * Returns keys for various power values attributed to the specified component
+     * held by this BatteryUsageStats object.
+     */
+    public Key[] getKeys(@PowerComponent int componentId) {
+        return mData.getKeys(componentId);
+    }
+
+    /**
+     * Returns the key for the power attributed to the specified component,
+     * for all values of other dimensions such as process state.
+     */
+    public Key getKey(@PowerComponent int componentId) {
+        return mData.getKey(componentId, PROCESS_STATE_ANY);
+    }
+
+    /**
+     * Returns the key for the power attributed to the specified component and process state.
+     */
+    public Key getKey(@PowerComponent int componentId, @ProcessState int processState) {
+        return mData.getKey(componentId, processState);
     }
 
     /**
@@ -182,7 +351,19 @@
      * @return Amount of consumed power in mAh.
      */
     public double getConsumedPower(@PowerComponent int componentId) {
-        return mPowerComponents.getConsumedPower(componentId);
+        return mPowerComponents.getConsumedPower(
+                mData.getKeyOrThrow(componentId, PROCESS_STATE_ANY));
+    }
+
+    /**
+     * Returns the amount of drain attributed to the specified drain type, e.g. CPU, WiFi etc.
+     *
+     * @param key The key of the power component, obtained by calling {@link #getKey} or
+     *            {@link #getKeys} method.
+     * @return Amount of consumed power in mAh.
+     */
+    public double getConsumedPower(@NonNull Key key) {
+        return mPowerComponents.getConsumedPower(key);
     }
 
     /**
@@ -192,7 +373,18 @@
      *                    {@link BatteryConsumer#POWER_COMPONENT_CPU}.
      */
     public @PowerModel int getPowerModel(@BatteryConsumer.PowerComponent int componentId) {
-        return mPowerComponents.getPowerModel(componentId);
+        return mPowerComponents.getPowerModel(
+                mData.getKeyOrThrow(componentId, PROCESS_STATE_ANY));
+    }
+
+    /**
+     * Returns the ID of the model that was used for power estimation.
+     *
+     * @param key The key of the power component, obtained by calling {@link #getKey} or
+     *            {@link #getKeys} method.
+     */
+    public @PowerModel int getPowerModel(@NonNull BatteryConsumer.Key key) {
+        return mPowerComponents.getPowerModel(key);
     }
 
     /**
@@ -227,7 +419,20 @@
      * @return Amount of time in milliseconds.
      */
     public long getUsageDurationMillis(@PowerComponent int componentId) {
-        return mPowerComponents.getUsageDurationMillis(componentId);
+        return mPowerComponents.getUsageDurationMillis(getKey(componentId));
+    }
+
+    /**
+     * Returns the amount of time since BatteryStats reset used by the specified component, e.g.
+     * CPU, WiFi etc.
+     *
+     *
+     * @param key The key of the power component, obtained by calling {@link #getKey} or
+     *            {@link #getKeys} method.
+     * @return Amount of time in milliseconds.
+     */
+    public long getUsageDurationMillis(@NonNull Key key) {
+        return mPowerComponents.getUsageDurationMillis(key);
     }
 
     /**
@@ -245,6 +450,9 @@
      * Returns the name of the specified component.  Intended for logging and debugging.
      */
     public static String powerComponentIdToString(@BatteryConsumer.PowerComponent int componentId) {
+        if (componentId == POWER_COMPONENT_ANY) {
+            return "all";
+        }
         return sPowerComponentNames[componentId];
     }
 
@@ -263,6 +471,13 @@
     }
 
     /**
+     * Returns the name of the specified process state.  Intended for logging and debugging.
+     */
+    public static String processStateToString(@BatteryConsumer.ProcessState int processState) {
+        return sProcessStateNames[processState];
+    }
+
+    /**
      * Prints the stats in a human-readable format.
      */
     public void dump(PrintWriter pw) {
@@ -347,6 +562,44 @@
             return new BatteryConsumerData(cursorWindow, cursorRow, layout);
         }
 
+        public Key[] getKeys(int componentId) {
+            return layout.keys[componentId];
+        }
+
+        Key getKeyOrThrow(int componentId, int processState) {
+            Key key = getKey(componentId, processState);
+            if (key == null) {
+                if (processState == PROCESS_STATE_ANY) {
+                    throw new IllegalArgumentException(
+                            "Unsupported power component ID: " + componentId);
+                } else {
+                    throw new IllegalArgumentException(
+                            "Unsupported power component ID: " + componentId
+                                    + " process state: " + processState);
+                }
+            }
+            return key;
+        }
+
+        Key getKey(int componentId, int processState) {
+            if (componentId >= POWER_COMPONENT_COUNT) {
+                return null;
+            }
+
+            if (processState == PROCESS_STATE_ANY) {
+                // The 0-th key for each component corresponds to the roll-up,
+                // across all dimensions. We might as well skip the iteration over the array.
+                return layout.keys[componentId][0];
+            } else {
+                for (Key key : layout.keys[componentId]) {
+                    if (key.processState == processState) {
+                        return key;
+                    }
+                }
+            }
+            return null;
+        }
+
         void putInt(int columnIndex, int value) {
             if (mCursorRow == -1) {
                 return;
@@ -405,59 +658,91 @@
     }
 
     static class BatteryConsumerDataLayout {
+        private static final Key[] KEY_ARRAY = new Key[0];
         public final String[] customPowerComponentNames;
         public final int customPowerComponentCount;
         public final boolean powerModelsIncluded;
-
-        public final int consumedPowerColumn;
-        public final int firstConsumedPowerColumn;
+        public final boolean processStateDataIncluded;
+        public final Key[][] keys;
+        public final int totalConsumedPowerColumnIndex;
         public final int firstCustomConsumedPowerColumn;
-        public final int firstUsageDurationColumn;
         public final int firstCustomUsageDurationColumn;
-        public final int firstPowerModelColumn;
         public final int columnCount;
 
         private BatteryConsumerDataLayout(int firstColumn, String[] customPowerComponentNames,
-                boolean powerModelsIncluded) {
+                boolean powerModelsIncluded, boolean includeProcessStateData) {
             this.customPowerComponentNames = customPowerComponentNames;
             this.customPowerComponentCount = customPowerComponentNames.length;
             this.powerModelsIncluded = powerModelsIncluded;
+            this.processStateDataIncluded = includeProcessStateData;
 
             int columnIndex = firstColumn;
-            consumedPowerColumn = columnIndex++;
 
-            firstConsumedPowerColumn = columnIndex;
-            columnIndex += BatteryConsumer.POWER_COMPONENT_COUNT;
+            totalConsumedPowerColumnIndex = columnIndex++;
+
+            keys = new Key[POWER_COMPONENT_COUNT][];
+
+            ArrayList<Key> perComponentKeys = new ArrayList<>();
+            for (int componentId = 0; componentId < POWER_COMPONENT_COUNT; componentId++) {
+                perComponentKeys.clear();
+
+                // Declare the Key for the power component, ignoring other dimensions.
+                perComponentKeys.add(
+                        new Key(componentId, PROCESS_STATE_ANY,
+                                powerModelsIncluded ? columnIndex++ : -1,  // power model
+                                columnIndex++,      // power
+                                columnIndex++       // usage duration
+                        ));
+
+                // Declare Keys for all process states, if needed
+                if (includeProcessStateData) {
+                    boolean isSupported = false;
+                    for (int id : SUPPORTED_POWER_COMPONENTS_PER_PROCESS_STATE) {
+                        if (id == componentId) {
+                            isSupported = true;
+                            break;
+                        }
+                    }
+                    if (isSupported) {
+                        for (int processState = 0; processState < PROCESS_STATE_COUNT;
+                                processState++) {
+                            if (processState == PROCESS_STATE_ANY) {
+                                continue;
+                            }
+
+                            perComponentKeys.add(
+                                    new Key(componentId, processState,
+                                            powerModelsIncluded ? columnIndex++ : -1, // power model
+                                            columnIndex++,      // power
+                                            columnIndex++       // usage duration
+                                    ));
+                        }
+                    }
+                }
+
+                keys[componentId] = perComponentKeys.toArray(KEY_ARRAY);
+            }
 
             firstCustomConsumedPowerColumn = columnIndex;
             columnIndex += customPowerComponentCount;
 
-            firstUsageDurationColumn = columnIndex;
-            columnIndex += BatteryConsumer.POWER_COMPONENT_COUNT;
-
             firstCustomUsageDurationColumn = columnIndex;
             columnIndex += customPowerComponentCount;
 
-            if (powerModelsIncluded) {
-                firstPowerModelColumn = columnIndex;
-                columnIndex += BatteryConsumer.POWER_COMPONENT_COUNT;
-            } else {
-                firstPowerModelColumn = -1;
-            }
-
             columnCount = columnIndex;
         }
     }
 
     static BatteryConsumerDataLayout createBatteryConsumerDataLayout(
-            String[] customPowerComponentNames, boolean includePowerModels) {
+            String[] customPowerComponentNames, boolean includePowerModels,
+            boolean includeProcessStateData) {
         int columnCount = BatteryConsumer.COLUMN_COUNT;
         columnCount = Math.max(columnCount, AggregateBatteryConsumer.COLUMN_COUNT);
         columnCount = Math.max(columnCount, UidBatteryConsumer.COLUMN_COUNT);
         columnCount = Math.max(columnCount, UserBatteryConsumer.COLUMN_COUNT);
 
         return new BatteryConsumerDataLayout(columnCount, customPowerComponentNames,
-                includePowerModels);
+                includePowerModels, includeProcessStateData);
     }
 
     protected abstract static class BaseBuilder<T extends BaseBuilder<?>> {
@@ -471,6 +756,11 @@
             mPowerComponentsBuilder = new PowerComponents.Builder(data);
         }
 
+        @Nullable
+        public Key getKey(@PowerComponent int componentId, @ProcessState int processState) {
+            return mData.getKey(componentId, processState);
+        }
+
         /**
          * Sets the amount of drain attributed to the specified drain type, e.g. CPU, WiFi etc.
          *
@@ -494,7 +784,15 @@
         @NonNull
         public T setConsumedPower(@PowerComponent int componentId, double componentPower,
                 @PowerModel int powerModel) {
-            mPowerComponentsBuilder.setConsumedPower(componentId, componentPower, powerModel);
+            mPowerComponentsBuilder.setConsumedPower(getKey(componentId, PROCESS_STATE_ANY),
+                    componentPower, powerModel);
+            return (T) this;
+        }
+
+        @SuppressWarnings("unchecked")
+        @NonNull
+        public T setConsumedPower(Key key, double componentPower, @PowerModel int powerModel) {
+            mPowerComponentsBuilder.setConsumedPower(key, componentPower, powerModel);
             return (T) this;
         }
 
@@ -522,7 +820,16 @@
         @NonNull
         public T setUsageDurationMillis(@UidBatteryConsumer.PowerComponent int componentId,
                 long componentUsageTimeMillis) {
-            mPowerComponentsBuilder.setUsageDurationMillis(componentId, componentUsageTimeMillis);
+            mPowerComponentsBuilder.setUsageDurationMillis(getKey(componentId, PROCESS_STATE_ANY),
+                    componentUsageTimeMillis);
+            return (T) this;
+        }
+
+
+        @SuppressWarnings("unchecked")
+        @NonNull
+        public T setUsageDurationMillis(Key key, long componentUsageTimeMillis) {
+            mPowerComponentsBuilder.setUsageDurationMillis(key, componentUsageTimeMillis);
             return (T) this;
         }
 
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index fb99118..30613b9 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -654,6 +654,11 @@
     }
 
     /**
+     * Returns true if battery consumption is tracked on a per-process-state basis.
+     */
+    public abstract boolean isProcessStateDataAvailable();
+
+    /**
      * The statistics associated with a particular uid.
      */
     public static abstract class Uid {
@@ -1908,6 +1913,10 @@
         public final HistoryTag localWakeReasonTag = new HistoryTag();
         public final HistoryTag localEventTag = new HistoryTag();
 
+        // Includes a tag's first occurrence in the parcel, so the value of the tag is written
+        // rather than just its index in the history tag pool.
+        public boolean tagsFirstOccurrence;
+
         @UnsupportedAppUsage
         public HistoryItem() {
         }
@@ -2014,6 +2023,7 @@
             wakeReasonTag = null;
             eventCode = EVENT_NONE;
             eventTag = null;
+            tagsFirstOccurrence = false;
         }
 
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
@@ -2061,6 +2071,7 @@
             } else {
                 eventTag = null;
             }
+            tagsFirstOccurrence = o.tagsFirstOccurrence;
             currentTime = o.currentTime;
         }
 
@@ -2303,6 +2314,38 @@
     public abstract Timer getScreenBrightnessTimer(int brightnessBin);
 
     /**
+     * Returns the number of physical displays on the device.
+     *
+     * {@hide}
+     */
+    public abstract int getDisplayCount();
+
+    /**
+     * Returns the time in microseconds that the screen has been on for a display while the
+     * device was running on battery.
+     *
+     * {@hide}
+     */
+    public abstract long getDisplayScreenOnTime(int display, long elapsedRealtimeUs);
+
+    /**
+     * Returns the time in microseconds that a display has been dozing while the device was
+     * running on battery.
+     *
+     * {@hide}
+     */
+    public abstract long getDisplayScreenDozeTime(int display, long elapsedRealtimeUs);
+
+    /**
+     * Returns the time in microseconds that a display has been on with the given brightness
+     * level while the device was running on battery.
+     *
+     * {@hide}
+     */
+    public abstract long getDisplayScreenBrightnessTime(int display, int brightnessBin,
+            long elapsedRealtimeUs);
+
+    /**
      * Returns the time in microseconds that power save mode has been enabled while the device was
      * running on battery.
      *
@@ -5038,6 +5081,71 @@
             pw.println(sb.toString());
         }
 
+        final int numDisplays = getDisplayCount();
+        if (numDisplays > 1) {
+            pw.println("");
+            pw.print(prefix);
+            sb.setLength(0);
+            sb.append(prefix);
+            sb.append("  MULTI-DISPLAY POWER SUMMARY START");
+            pw.println(sb.toString());
+
+            for (int display = 0; display < numDisplays; display++) {
+                sb.setLength(0);
+                sb.append(prefix);
+                sb.append("  Display ");
+                sb.append(display);
+                sb.append(" Statistics:");
+                pw.println(sb.toString());
+
+                final long displayScreenOnTime = getDisplayScreenOnTime(display, rawRealtime);
+                sb.setLength(0);
+                sb.append(prefix);
+                sb.append("    Screen on: ");
+                formatTimeMs(sb, displayScreenOnTime / 1000);
+                sb.append("(");
+                sb.append(formatRatioLocked(displayScreenOnTime, whichBatteryRealtime));
+                sb.append(") ");
+                pw.println(sb.toString());
+
+                sb.setLength(0);
+                sb.append("    Screen brightness levels:");
+                didOne = false;
+                for (int bin = 0; bin < NUM_SCREEN_BRIGHTNESS_BINS; bin++) {
+                    final long timeUs = getDisplayScreenBrightnessTime(display, bin, rawRealtime);
+                    if (timeUs == 0) {
+                        continue;
+                    }
+                    didOne = true;
+                    sb.append("\n      ");
+                    sb.append(prefix);
+                    sb.append(SCREEN_BRIGHTNESS_NAMES[bin]);
+                    sb.append(" ");
+                    formatTimeMs(sb, timeUs / 1000);
+                    sb.append("(");
+                    sb.append(formatRatioLocked(timeUs, displayScreenOnTime));
+                    sb.append(")");
+                }
+                if (!didOne) sb.append(" (no activity)");
+                pw.println(sb.toString());
+
+                final long displayScreenDozeTimeUs = getDisplayScreenDozeTime(display, rawRealtime);
+                sb.setLength(0);
+                sb.append(prefix);
+                sb.append("    Screen Doze: ");
+                formatTimeMs(sb, displayScreenDozeTimeUs / 1000);
+                sb.append("(");
+                sb.append(formatRatioLocked(displayScreenDozeTimeUs, whichBatteryRealtime));
+                sb.append(") ");
+                pw.println(sb.toString());
+            }
+            pw.print(prefix);
+            sb.setLength(0);
+            sb.append(prefix);
+            sb.append("  MULTI-DISPLAY POWER SUMMARY END");
+            pw.println(sb.toString());
+        }
+
         pw.println("");
         pw.print(prefix);
         sb.setLength(0);
@@ -5303,6 +5411,7 @@
                 new BatteryUsageStatsQuery.Builder()
                         .setMaxStatsAgeMs(0)
                         .includePowerModels()
+                        .includeProcessStateData()
                         .build());
         stats.dump(pw, prefix);
 
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 591b665..ed44fb6 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -98,8 +98,10 @@
     static final String XML_ATTR_USER_ID = "user_id";
     static final String XML_ATTR_SCOPE = "scope";
     static final String XML_ATTR_PREFIX_CUSTOM_COMPONENT = "custom_component_";
+    static final String XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA = "includes_proc_state_data";
     static final String XML_ATTR_START_TIMESTAMP = "start_timestamp";
     static final String XML_ATTR_END_TIMESTAMP = "end_timestamp";
+    static final String XML_ATTR_PROCESS_STATE = "process_state";
     static final String XML_ATTR_POWER = "power";
     static final String XML_ATTR_DURATION = "duration";
     static final String XML_ATTR_MODEL = "model";
@@ -129,11 +131,11 @@
     private final long mChargeTimeRemainingMs;
     private final String[] mCustomPowerComponentNames;
     private final boolean mIncludesPowerModels;
+    private final boolean mIncludesProcessStateData;
     private final List<UidBatteryConsumer> mUidBatteryConsumers;
     private final List<UserBatteryConsumer> mUserBatteryConsumers;
     private final AggregateBatteryConsumer[] mAggregateBatteryConsumers;
     private final Parcel mHistoryBuffer;
-    private final List<BatteryStats.HistoryTag> mHistoryTagPool;
     private CursorWindow mBatteryConsumersCursorWindow;
 
     private BatteryUsageStats(@NonNull Builder builder) {
@@ -145,11 +147,11 @@
         mDischargedPowerLowerBound = builder.mDischargedPowerLowerBoundMah;
         mDischargedPowerUpperBound = builder.mDischargedPowerUpperBoundMah;
         mHistoryBuffer = builder.mHistoryBuffer;
-        mHistoryTagPool = builder.mHistoryTagPool;
         mBatteryTimeRemainingMs = builder.mBatteryTimeRemainingMs;
         mChargeTimeRemainingMs = builder.mChargeTimeRemainingMs;
         mCustomPowerComponentNames = builder.mCustomPowerComponentNames;
         mIncludesPowerModels = builder.mIncludePowerModels;
+        mIncludesProcessStateData = builder.mIncludesProcessStateData;
         mBatteryConsumersCursorWindow = builder.mBatteryConsumersCursorWindow;
 
         double totalPowerMah = 0;
@@ -283,13 +285,17 @@
     /**
      * Returns the names of custom power components in order, so the first name in the array
      * corresponds to the custom componentId
-     * {@link BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID}.
+     * {@link BatteryConsumer#FIRST_CUSTOM_POWER_COMPONENT_ID}.
      */
     @NonNull
     public String[] getCustomPowerComponentNames() {
         return mCustomPowerComponentNames;
     }
 
+    public boolean isProcessStateDataIncluded() {
+        return mIncludesProcessStateData;
+    }
+
     /**
      * Returns an iterator for {@link android.os.BatteryStats.HistoryItem}'s.
      */
@@ -299,8 +305,7 @@
             throw new IllegalStateException(
                     "Battery history was not requested in the BatteryUsageStatsQuery");
         }
-        return new BatteryStatsHistoryIterator(new BatteryStatsHistory(mHistoryBuffer),
-                mHistoryTagPool);
+        return new BatteryStatsHistoryIterator(new BatteryStatsHistory(mHistoryBuffer));
     }
 
     @Override
@@ -320,11 +325,12 @@
         mChargeTimeRemainingMs = source.readLong();
         mCustomPowerComponentNames = source.readStringArray();
         mIncludesPowerModels = source.readBoolean();
+        mIncludesProcessStateData = source.readBoolean();
 
         mBatteryConsumersCursorWindow = CursorWindow.newFromParcel(source);
         BatteryConsumer.BatteryConsumerDataLayout dataLayout =
                 BatteryConsumer.createBatteryConsumerDataLayout(mCustomPowerComponentNames,
-                        mIncludesPowerModels);
+                        mIncludesPowerModels, mIncludesProcessStateData);
 
         final int numRows = mBatteryConsumersCursorWindow.getNumRows();
 
@@ -361,19 +367,8 @@
 
             mHistoryBuffer = Parcel.obtain();
             mHistoryBuffer.unmarshall(historyBlob, 0, historyBlob.length);
-
-            int historyTagCount = source.readInt();
-            mHistoryTagPool = new ArrayList<>(historyTagCount);
-            for (int i = 0; i < historyTagCount; i++) {
-                BatteryStats.HistoryTag tag = new BatteryStats.HistoryTag();
-                tag.string = source.readString();
-                tag.uid = source.readInt();
-                tag.poolIdx = source.readInt();
-                mHistoryTagPool.add(tag);
-            }
         } else {
             mHistoryBuffer = null;
-            mHistoryTagPool = null;
         }
     }
 
@@ -390,18 +385,13 @@
         dest.writeLong(mChargeTimeRemainingMs);
         dest.writeStringArray(mCustomPowerComponentNames);
         dest.writeBoolean(mIncludesPowerModels);
+        dest.writeBoolean(mIncludesProcessStateData);
+
         mBatteryConsumersCursorWindow.writeToParcel(dest, flags);
 
         if (mHistoryBuffer != null) {
             dest.writeBoolean(true);
             dest.writeBlob(mHistoryBuffer.marshall());
-            dest.writeInt(mHistoryTagPool.size());
-            for (int i = mHistoryTagPool.size() - 1; i >= 0; i--) {
-                final BatteryStats.HistoryTag tag = mHistoryTagPool.get(i);
-                dest.writeString(tag.string);
-                dest.writeInt(tag.uid);
-                dest.writeInt(tag.poolIdx);
-            }
         } else {
             dest.writeBoolean(false);
         }
@@ -558,16 +548,22 @@
 
         for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
                 componentId++) {
-            final double devicePowerMah = deviceConsumer.getConsumedPower(componentId);
-            final double appsPowerMah = appsConsumer.getConsumedPower(componentId);
-            if (devicePowerMah == 0 && appsPowerMah == 0) {
-                continue;
-            }
+            for (BatteryConsumer.Key key : deviceConsumer.getKeys(componentId)) {
+                final double devicePowerMah = deviceConsumer.getConsumedPower(key);
+                final double appsPowerMah = appsConsumer.getConsumedPower(key);
+                if (devicePowerMah == 0 && appsPowerMah == 0) {
+                    continue;
+                }
 
-            final String componentName = BatteryConsumer.powerComponentIdToString(componentId);
-            printPowerComponent(pw, prefix, componentName, devicePowerMah, appsPowerMah,
-                    deviceConsumer.getPowerModel(componentId),
-                    deviceConsumer.getUsageDurationMillis(componentId));
+                String label = BatteryConsumer.powerComponentIdToString(componentId);
+                if (key.processState != BatteryConsumer.PROCESS_STATE_ANY) {
+                    label = label
+                            + "(" + BatteryConsumer.processStateToString(key.processState) + ")";
+                }
+                printPowerComponent(pw, prefix, label, devicePowerMah, appsPowerMah,
+                        deviceConsumer.getPowerModel(key),
+                        deviceConsumer.getUsageDurationMillis(key));
+            }
         }
 
         for (int componentId = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID;
@@ -592,10 +588,10 @@
         dumpSortedBatteryConsumers(pw, prefix, getUserBatteryConsumers());
     }
 
-    private void printPowerComponent(PrintWriter pw, String prefix, String componentName,
+    private void printPowerComponent(PrintWriter pw, String prefix, String label,
             double devicePowerMah, double appsPowerMah, int powerModel, long durationMs) {
         StringBuilder sb = new StringBuilder();
-        sb.append(prefix).append("      ").append(componentName).append(": ")
+        sb.append(prefix).append("      ").append(label).append(": ")
                 .append(PowerCalculator.formatCharge(devicePowerMah));
         if (powerModel != BatteryConsumer.POWER_MODEL_UNDEFINED
                 && powerModel != BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
@@ -636,7 +632,8 @@
             serializer.attribute(null, XML_ATTR_PREFIX_CUSTOM_COMPONENT + i,
                     mCustomPowerComponentNames[i]);
         }
-
+        serializer.attributeBoolean(null, XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA,
+                mIncludesProcessStateData);
         serializer.attributeLong(null, XML_ATTR_START_TIMESTAMP, mStatsStartTimestampMs);
         serializer.attributeLong(null, XML_ATTR_END_TIMESTAMP, mStatsEndTimestampMs);
         serializer.attributeLong(null, XML_ATTR_DURATION, mStatsDurationMs);
@@ -680,7 +677,11 @@
                     i++;
                 }
 
-                builder = new Builder(customComponentNames.toArray(new String[0]), true);
+                final boolean includesProcStateData = parser.getAttributeBoolean(null,
+                        XML_ATTR_PREFIX_INCLUDES_PROC_STATE_DATA, false);
+
+                builder = new Builder(customComponentNames.toArray(new String[0]), true,
+                        includesProcStateData);
 
                 builder.setStatsStartTimestamp(
                         parser.getAttributeLong(null, XML_ATTR_START_TIMESTAMP));
@@ -752,6 +753,7 @@
         @NonNull
         private final String[] mCustomPowerComponentNames;
         private final boolean mIncludePowerModels;
+        private final boolean mIncludesProcessStateData;
         private final BatteryConsumer.BatteryConsumerDataLayout mBatteryConsumerDataLayout;
         private long mStatsStartTimestampMs;
         private long mStatsEndTimestampMs;
@@ -769,22 +771,23 @@
         private final SparseArray<UserBatteryConsumer.Builder> mUserBatteryConsumerBuilders =
                 new SparseArray<>();
         private Parcel mHistoryBuffer;
-        private List<BatteryStats.HistoryTag> mHistoryTagPool;
 
         public Builder(@NonNull String[] customPowerComponentNames) {
-            this(customPowerComponentNames, false);
+            this(customPowerComponentNames, false, false);
         }
 
-        public Builder(@NonNull String[] customPowerComponentNames, boolean includePowerModels) {
+        public Builder(@NonNull String[] customPowerComponentNames, boolean includePowerModels,
+                boolean includeProcessStateData) {
             mBatteryConsumersCursorWindow =
                     new CursorWindow(null, BATTERY_CONSUMER_CURSOR_WINDOW_SIZE);
             mBatteryConsumerDataLayout =
                     BatteryConsumer.createBatteryConsumerDataLayout(customPowerComponentNames,
-                            includePowerModels);
+                            includePowerModels, includeProcessStateData);
             mBatteryConsumersCursorWindow.setNumColumns(mBatteryConsumerDataLayout.columnCount);
 
             mCustomPowerComponentNames = customPowerComponentNames;
             mIncludePowerModels = includePowerModels;
+            mIncludesProcessStateData = includeProcessStateData;
             for (int scope = 0; scope < AGGREGATE_BATTERY_CONSUMER_SCOPE_COUNT; scope++) {
                 final BatteryConsumer.BatteryConsumerData data =
                         BatteryConsumer.BatteryConsumerData.create(mBatteryConsumersCursorWindow,
@@ -794,6 +797,10 @@
             }
         }
 
+        public boolean isProcessStateDataNeeded() {
+            return mIncludesProcessStateData;
+        }
+
         /**
          * Constructs a read-only object using the Builder values.
          */
@@ -888,10 +895,8 @@
          * Sets the parceled recent history.
          */
         @NonNull
-        public Builder setBatteryHistory(Parcel historyBuffer,
-                List<BatteryStats.HistoryTag> historyTagPool) {
+        public Builder setBatteryHistory(Parcel historyBuffer) {
             mHistoryBuffer = historyBuffer;
-            mHistoryTagPool = historyTagPool;
             return this;
         }
 
@@ -978,6 +983,11 @@
                         "BatteryUsageStats have different custom power components");
             }
 
+            if (mIncludesProcessStateData && !stats.mIncludesProcessStateData) {
+                throw new IllegalArgumentException(
+                        "Added BatteryUsageStats does not include process state data");
+            }
+
             if (mUserBatteryConsumerBuilders.size() != 0
                     || !stats.getUserBatteryConsumers().isEmpty()) {
                 throw new UnsupportedOperationException(
diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java
index 97f24cc..187b64f 100644
--- a/core/java/android/os/BatteryUsageStatsQuery.java
+++ b/core/java/android/os/BatteryUsageStatsQuery.java
@@ -41,6 +41,7 @@
     @IntDef(flag = true, prefix = { "FLAG_BATTERY_USAGE_STATS_" }, value = {
             FLAG_BATTERY_USAGE_STATS_POWER_PROFILE_MODEL,
             FLAG_BATTERY_USAGE_STATS_INCLUDE_HISTORY,
+            FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface BatteryUsageStatsFlags {}
@@ -52,19 +53,21 @@
      *
      * @hide
      */
-    public static final int FLAG_BATTERY_USAGE_STATS_POWER_PROFILE_MODEL = 1;
+    public static final int FLAG_BATTERY_USAGE_STATS_POWER_PROFILE_MODEL = 0x0001;
 
     /**
      * Indicates that battery history should be included in the BatteryUsageStats.
      * @hide
      */
-    public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_HISTORY = 2;
+    public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_HISTORY = 0x0002;
 
     /**
      * Indicates that identifiers of power models used for computations of power
      * consumption should be included in the BatteryUsageStats.
      */
-    public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_MODELS = 4;
+    public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_MODELS = 0x0004;
+
+    public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA = 0x0008;
 
     private static final long DEFAULT_MAX_STATS_AGE_MS = 5 * 60 * 1000;
 
@@ -209,6 +212,16 @@
         }
 
         /**
+         * Requests that per-process state data be included in the BatteryUsageStats, if
+         * available. Check {@link BatteryUsageStats#isProcessStateDataIncluded()} on the result
+         * to see if the data is available.
+         */
+        public Builder includeProcessStateData() {
+            mFlags |= BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA;
+            return this;
+        }
+
+        /**
          * Requests to return modeled battery usage stats only, even if on-device
          * power monitoring data is available.
          *
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index b6163f1..92eb7a5 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -33,6 +33,9 @@
 /**
  * A mapping from String keys to various {@link Parcelable} values.
  *
+ * <p><b>Warning:</b> Note that {@link Bundle} is a lazy container and as such it does NOT implement
+ * {@link #equals(Object)} or {@link #hashCode()}.
+ *
  * @see PersistableBundle
  */
 public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
@@ -348,56 +351,6 @@
         return (mFlags & FLAG_HAS_FDS) != 0;
     }
 
-    /**
-     * Filter values in Bundle to only basic types.
-     * @hide
-     */
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public Bundle filterValues() {
-        unparcel(/* itemwise */ true);
-        Bundle bundle = this;
-        if (mMap != null) {
-            ArrayMap<String, Object> map = mMap;
-            for (int i = map.size() - 1; i >= 0; i--) {
-                Object value = map.valueAt(i);
-                if (PersistableBundle.isValidType(value)) {
-                    continue;
-                }
-                if (value instanceof Bundle) {
-                    Bundle newBundle = ((Bundle)value).filterValues();
-                    if (newBundle != value) {
-                        if (map == mMap) {
-                            // The filter had to generate a new bundle, but we have not yet
-                            // created a new one here.  Do that now.
-                            bundle = new Bundle(this);
-                            // Note the ArrayMap<> constructor is guaranteed to generate
-                            // a new object with items in the same order as the original.
-                            map = bundle.mMap;
-                        }
-                        // Replace this current entry with the new child bundle.
-                        map.setValueAt(i, newBundle);
-                    }
-                    continue;
-                }
-                if (value.getClass().getName().startsWith("android.")) {
-                    continue;
-                }
-                if (map == mMap) {
-                    // This is the first time we have had to remove something, that means we
-                    // need to switch to a new Bundle.
-                    bundle = new Bundle(this);
-                    // Note the ArrayMap<> constructor is guaranteed to generate
-                    // a new object with items in the same order as the original.
-                    map = bundle.mMap;
-                }
-                map.removeAt(i);
-            }
-        }
-        mFlags |= FLAG_HAS_FDS_KNOWN;
-        mFlags &= ~FLAG_HAS_FDS;
-        return bundle;
-    }
-
     /** {@hide} */
     @Override
     public void putObject(@Nullable String key, @Nullable Object value) {
@@ -1280,6 +1233,10 @@
         maybePrefillHasFds();
     }
 
+    /**
+     * Returns a string representation of the {@link Bundle} that may be suitable for debugging. It
+     * won't print the internal map if its content hasn't been unparcelled.
+     */
     @Override
     public synchronized String toString() {
         if (mParcelledData != null) {
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index e7c3a83..92861fb 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -1,18 +1,18 @@
 # Haptics
-per-file CombinedVibrationEffect.aidl = michaelwr@google.com
-per-file CombinedVibrationEffect.java = michaelwr@google.com
-per-file ExternalVibration.aidl = michaelwr@google.com
-per-file ExternalVibration.java = michaelwr@google.com
-per-file IExternalVibrationController.aidl = michaelwr@google.com
-per-file IExternalVibratorService.aidl = michaelwr@google.com
-per-file IVibratorManagerService.aidl = michaelwr@google.com
-per-file NullVibrator.java = michaelwr@google.com
-per-file SystemVibrator.java = michaelwr@google.com
-per-file SystemVibratorManager.java = michaelwr@google.com
-per-file VibrationEffect.aidl = michaelwr@google.com
-per-file VibrationEffect.java = michaelwr@google.com
-per-file Vibrator.java = michaelwr@google.com
-per-file VibratorManager.java = michaelwr@google.com
+per-file CombinedVibrationEffect.aidl = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file CombinedVibrationEffect.java = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file ExternalVibration.aidl = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file ExternalVibration.java = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file IExternalVibrationController.aidl = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file IExternalVibratorService.aidl = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file IVibratorManagerService.aidl = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file NullVibrator.java = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file SystemVibrator.java = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file SystemVibratorManager.java = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file VibrationEffect.aidl = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file VibrationEffect.java = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file Vibrator.java = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file VibratorManager.java = file:/services/core/java/com/android/server/vibrator/OWNERS
 
 # PowerManager
 per-file IPowerManager.aidl = michaelwr@google.com, santoscordon@google.com
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 5efa390..ab2c8c0 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -384,6 +384,8 @@
             long thisNativePtr, long otherNativePtr, int offset, int length);
     @CriticalNative
     private static native boolean nativeHasFileDescriptors(long nativePtr);
+    private static native boolean nativeHasFileDescriptorsInRange(
+            long nativePtr, int offset, int length);
     private static native void nativeWriteInterfaceToken(long nativePtr, String interfaceName);
     private static native void nativeEnforceInterface(long nativePtr, String interfaceName);
 
@@ -399,7 +401,7 @@
     private static final int WRITE_EXCEPTION_STACK_TRACE_THRESHOLD_MS = 1000;
 
     @CriticalNative
-    private static native long nativeGetBlobAshmemSize(long nativePtr);
+    private static native long nativeGetOpenAshmemSize(long nativePtr);
 
     public final static Parcelable.Creator<String> STRING_CREATOR
              = new Parcelable.Creator<String>() {
@@ -717,11 +719,26 @@
     /**
      * Report whether the parcel contains any marshalled file descriptors.
      */
-    public final boolean hasFileDescriptors() {
+    public boolean hasFileDescriptors() {
         return nativeHasFileDescriptors(mNativePtr);
     }
 
     /**
+     * Report whether the parcel contains any marshalled file descriptors in the range defined by
+     * {@code offset} and {@code length}.
+     *
+     * @param offset The offset from which the range starts. Should be between 0 and
+     *     {@link #dataSize()}.
+     * @param length The length of the range. Should be between 0 and {@link #dataSize()} - {@code
+     *     offset}.
+     * @return whether there are file descriptors or not.
+     * @throws IllegalArgumentException if the parameters are out of the permitted ranges.
+     */
+    public boolean hasFileDescriptors(int offset, int length) {
+        return nativeHasFileDescriptorsInRange(mNativePtr, offset, length);
+    }
+
+    /**
      * Check if the object used in {@link #readValue(ClassLoader)} / {@link #writeValue(Object)}
      * has file descriptors.
      *
@@ -2874,9 +2891,11 @@
 
     /**
      * Same as {@link #readList(List, ClassLoader)} but accepts {@code clazz} parameter as
-     * the type required for each item. If the item to be deserialized is not an instance
-     * of that class or any of its children class
-     * a {@link BadParcelableException} will be thrown.
+     * the type required for each item.
+     *
+     * @throws BadParcelableException Throws BadParcelableException if the item to be deserialized
+     * is not an instance of that class or any of its children classes or there was an error
+     * trying to instantiate an element.
      */
     public <T> void readList(@NonNull List<? super T> outVal,
             @Nullable ClassLoader loader, @NonNull Class<T> clazz) {
@@ -3536,15 +3555,26 @@
         int start = dataPosition();
         int type = readInt();
         if (isLengthPrefixed(type)) {
-            int length = readInt();
-            setDataPosition(MathUtils.addOrThrow(dataPosition(), length));
-            return new LazyValue(this, start, length, type, loader);
+            int objectLength = readInt();
+            int end = MathUtils.addOrThrow(dataPosition(), objectLength);
+            int valueLength = end - start;
+            setDataPosition(end);
+            return new LazyValue(this, start, valueLength, type, loader);
         } else {
             return readValue(type, loader, /* clazz */ null);
         }
     }
 
+
     private static final class LazyValue implements Supplier<Object> {
+        /**
+         *                      |   4B   |   4B   |
+         * mSource = Parcel{... |  type  | length | object | ...}
+         *                      a        b        c        d
+         * length = d - c
+         * mPosition = a
+         * mLength = d - a
+         */
         private final int mPosition;
         private final int mLength;
         private final int mType;
@@ -3573,14 +3603,17 @@
             Parcel source = mSource;
             if (source != null) {
                 synchronized (source) {
-                    int restore = source.dataPosition();
-                    try {
-                        source.setDataPosition(mPosition);
-                        mObject = source.readValue(mLoader);
-                    } finally {
-                        source.setDataPosition(restore);
+                    // Check mSource != null guarantees callers won't ever see different objects.
+                    if (mSource != null) {
+                        int restore = source.dataPosition();
+                        try {
+                            source.setDataPosition(mPosition);
+                            mObject = source.readValue(mLoader);
+                        } finally {
+                            source.setDataPosition(restore);
+                        }
+                        mSource = null;
                     }
-                    mSource = null;
                 }
             }
             return mObject;
@@ -3589,7 +3622,7 @@
         public void writeToParcel(Parcel out) {
             Parcel source = mSource;
             if (source != null) {
-                out.appendFrom(source, mPosition, mLength + 8);
+                out.appendFrom(source, mPosition, mLength);
             } else {
                 out.writeValue(mObject);
             }
@@ -3598,7 +3631,7 @@
         public boolean hasFileDescriptors() {
             Parcel source = mSource;
             return (source != null)
-                    ? getValueParcel(source).hasFileDescriptors()
+                    ? source.hasFileDescriptors(mPosition, mLength)
                     : Parcel.hasFileDescriptors(mObject);
         }
 
@@ -3659,10 +3692,7 @@
             Parcel parcel = mValueParcel;
             if (parcel == null) {
                 parcel = Parcel.obtain();
-                // mLength is the length of object representation, excluding the type and length.
-                // mPosition is the position of the entire value container, right before the type.
-                // So, we add 4 bytes for the type + 4 bytes for the length written.
-                parcel.appendFrom(source, mPosition, mLength + 8);
+                parcel.appendFrom(source, mPosition, mLength);
                 mValueParcel = parcel;
             }
             return parcel;
@@ -3817,7 +3847,7 @@
 
             default:
                 int off = dataPosition() - 4;
-                throw new RuntimeException(
+                throw new BadParcelableException(
                     "Parcel " + this + ": Unmarshalling unknown type code " + type
                             + " at offset " + off);
         }
@@ -3866,8 +3896,11 @@
 
     /**
      * Same as {@link #readParcelable(ClassLoader)} but accepts {@code clazz} parameter as the type
-     * required for each item. If the item to be deserialized is not an instance of that class or
-     * any of its children classes a {@link BadParcelableException} will be thrown.
+     * required for each item.
+     *
+     * @throws BadParcelableException Throws BadParcelableException if the item to be deserialized
+     * is not an instance of that class or any of its children classes or there was an error
+     * trying to instantiate an element.
      */
     @Nullable
     public <T extends Parcelable> T readParcelable(@Nullable ClassLoader loader,
@@ -3933,8 +3966,11 @@
 
     /**
      * Same as {@link #readParcelableCreator(ClassLoader)} but accepts {@code clazz} parameter
-     * as the required type. If the item to be deserialized is not an instance of that class
-     * or any of its children classes a {@link BadParcelableException} will be thrown.
+     * as the required type.
+     *
+     * @throws BadParcelableException Throws BadParcelableException if the item to be deserialized
+     * is not an instance of that class or any of its children class or there there was an error
+     * trying to read the {@link Parcelable.Creator}.
      */
     @Nullable
     public <T> Parcelable.Creator<T> readParcelableCreator(
@@ -4345,8 +4381,8 @@
     /**
      * @hide For testing
      */
-    public long getBlobAshmemSize() {
-        return nativeGetBlobAshmemSize(mNativePtr);
+    public long getOpenAshmemSize() {
+        return nativeGetOpenAshmemSize(mNativePtr);
     }
 
     private static String valueTypeToString(int type) {
diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java
index cad6e66..7b55e710 100644
--- a/core/java/android/os/PersistableBundle.java
+++ b/core/java/android/os/PersistableBundle.java
@@ -35,6 +35,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.Serializable;
 import java.util.ArrayList;
 
 /**
@@ -42,6 +43,9 @@
  * supported by this class is purposefully restricted to simple objects that can
  * safely be persisted to and restored from disk.
  *
+ * <p><b>Warning:</b> Note that {@link PersistableBundle} is a lazy container and as such it does
+ * NOT implement {@link #equals(Object)} or {@link #hashCode()}.
+ *
  * @see Bundle
  */
 public final class PersistableBundle extends BaseBundle implements Cloneable, Parcelable,
@@ -103,6 +107,10 @@
     /**
      * Constructs a PersistableBundle from a Bundle.  Does only a shallow copy of the Bundle.
      *
+     * <p><b>Warning:</b> This method will deserialize every item on the bundle, including custom
+     * types such as {@link Parcelable} and {@link Serializable}, so only use this when you trust
+     * the source. Specifically don't use this method on app-provided bundles.
+     *
      * @param b a Bundle to be copied.
      *
      * @throws IllegalArgumentException if any element of {@code b} cannot be persisted.
@@ -110,7 +118,7 @@
      * @hide
      */
     public PersistableBundle(Bundle b) {
-        this(b.getMap());
+        this(b.getItemwiseMap());
     }
 
     /**
@@ -319,8 +327,12 @@
         return new PersistableBundle();  // An empty mutable PersistableBundle
     }
 
+    /**
+     * Returns a string representation of the {@link PersistableBundle} that may be suitable for
+     * debugging. It won't print the internal map if its content hasn't been unparcelled.
+     */
     @Override
-    synchronized public String toString() {
+    public synchronized String toString() {
         if (mParcelledData != null) {
             if (isEmptyParcel()) {
                 return "PersistableBundle[EMPTY_PARCEL]";
diff --git a/core/java/android/os/PowerComponents.java b/core/java/android/os/PowerComponents.java
index 259e673..4b12aa6 100644
--- a/core/java/android/os/PowerComponents.java
+++ b/core/java/android/os/PowerComponents.java
@@ -15,6 +15,9 @@
  */
 package android.os;
 
+import static android.os.BatteryConsumer.POWER_COMPONENT_ANY;
+import static android.os.BatteryConsumer.POWER_COMPONENT_COUNT;
+import static android.os.BatteryConsumer.PROCESS_STATE_ANY;
 import static android.os.BatteryConsumer.convertMahToDeciCoulombs;
 
 import android.annotation.NonNull;
@@ -49,25 +52,41 @@
     }
 
     /**
-     * Total power consumed by this consumer, in mAh.
+     * Total power consumed by this consumer, aggregated over the specified dimensions, in mAh.
      */
-    public double getConsumedPower() {
-        return mData.getDouble(mData.layout.consumedPowerColumn);
+    public double getConsumedPower(@NonNull BatteryConsumer.Dimensions dimensions) {
+        if (dimensions.powerComponent != POWER_COMPONENT_ANY) {
+            return mData.getDouble(mData.getKeyOrThrow(dimensions.powerComponent,
+                    dimensions.processState).mPowerColumnIndex);
+        } else if (dimensions.processState != PROCESS_STATE_ANY) {
+            boolean foundSome = false;
+            double totalPowerMah = 0;
+            for (int componentId = 0; componentId < POWER_COMPONENT_COUNT; componentId++) {
+                BatteryConsumer.Key key = mData.getKey(componentId, dimensions.processState);
+                if (key != null) {
+                    foundSome = true;
+                    totalPowerMah += mData.getDouble(key.mPowerColumnIndex);
+                }
+            }
+            if (!foundSome) {
+                throw new IllegalArgumentException(
+                        "No data included in BatteryUsageStats for " + dimensions);
+            }
+            return totalPowerMah;
+        } else {
+            return mData.getDouble(mData.layout.totalConsumedPowerColumnIndex);
+        }
     }
 
     /**
      * Returns the amount of drain attributed to the specified drain type, e.g. CPU, WiFi etc.
      *
-     * @param componentId The ID of the power component, e.g.
-     *                    {@link BatteryConsumer#POWER_COMPONENT_CPU}.
+     * @param key The key of the power component, obtained by calling {@link BatteryConsumer#getKey}
+     *            or {@link BatteryConsumer#getKeys} method.
      * @return Amount of consumed power in mAh.
      */
-    public double getConsumedPower(@BatteryConsumer.PowerComponent int componentId) {
-        if (componentId >= BatteryConsumer.POWER_COMPONENT_COUNT) {
-            throw new IllegalArgumentException(
-                    "Unsupported power component ID: " + componentId);
-        }
-        return mData.getDouble(mData.layout.firstConsumedPowerColumn + componentId);
+    public double getConsumedPower(@NonNull BatteryConsumer.Key key) {
+        return mData.getDouble(key.mPowerColumnIndex);
     }
 
     /**
@@ -102,27 +121,23 @@
     }
 
     @BatteryConsumer.PowerModel
-    int getPowerModel(@BatteryConsumer.PowerComponent int componentId) {
-        if (!mData.layout.powerModelsIncluded) {
+    int getPowerModel(BatteryConsumer.Key key) {
+        if (key.mPowerModelColumnIndex == -1) {
             throw new IllegalStateException(
                     "Power model IDs were not requested in the BatteryUsageStatsQuery");
         }
-        return mData.getInt(mData.layout.firstPowerModelColumn + componentId);
+        return mData.getInt(key.mPowerModelColumnIndex);
     }
 
     /**
      * Returns the amount of time used by the specified component, e.g. CPU, WiFi etc.
      *
-     * @param componentId The ID of the power component, e.g.
-     *                    {@link BatteryConsumer#POWER_COMPONENT_CPU}.
+     * @param key The key of the power component, obtained by calling {@link BatteryConsumer#getKey}
+     *            or {@link BatteryConsumer#getKeys} method.
      * @return Amount of time in milliseconds.
      */
-    public long getUsageDurationMillis(@BatteryConsumer.PowerComponent int componentId) {
-        if (componentId >= BatteryConsumer.POWER_COMPONENT_COUNT) {
-            throw new IllegalArgumentException(
-                    "Unsupported power component ID: " + componentId);
-        }
-        return mData.getLong(mData.layout.firstUsageDurationColumn + componentId);
+    public long getUsageDurationMillis(BatteryConsumer.Key key) {
+        return mData.getLong(key.mDurationColumnIndex);
     }
 
     /**
@@ -143,17 +158,29 @@
 
     public void dump(PrintWriter pw, boolean skipEmptyComponents) {
         String separator = "";
+        StringBuilder sb = new StringBuilder();
+
         for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
                 componentId++) {
-            final double componentPower = getConsumedPower(componentId);
-            if (skipEmptyComponents && componentPower == 0) {
-                continue;
+            for (BatteryConsumer.Key key: mData.getKeys(componentId)) {
+                final double componentPower = getConsumedPower(key);
+                final long durationMs = getUsageDurationMillis(key);
+                if (skipEmptyComponents && componentPower == 0 && durationMs == 0) {
+                    continue;
+                }
+
+                sb.append(separator);
+                separator = " ";
+                sb.append(key.toShortString());
+                sb.append("=");
+                sb.append(PowerCalculator.formatCharge(componentPower));
+
+                if (durationMs != 0) {
+                    sb.append(" (");
+                    BatteryStats.formatTimeMsNoSpace(sb, durationMs);
+                    sb.append(")");
+                }
             }
-            pw.print(separator);
-            separator = " ";
-            pw.print(BatteryConsumer.powerComponentIdToString(componentId));
-            pw.print("=");
-            PowerCalculator.printPowerMah(pw, componentPower);
         }
 
         final int customComponentCount = mData.layout.customPowerComponentCount;
@@ -166,12 +193,14 @@
             if (skipEmptyComponents && customComponentPower == 0) {
                 continue;
             }
-            pw.print(separator);
+            sb.append(separator);
             separator = " ";
-            pw.print(getCustomPowerComponentName(customComponentId));
-            pw.print("=");
-            PowerCalculator.printPowerMah(pw, customComponentPower);
+            sb.append(getCustomPowerComponentName(customComponentId));
+            sb.append("=");
+            sb.append(PowerCalculator.formatCharge(customComponentPower));
         }
+
+        pw.print(sb);
     }
 
     /** Returns whether there are any atoms.proto POWER_COMPONENTS data to write to a proto. */
@@ -193,8 +222,10 @@
 
         for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
                 componentId++) {
-            final long powerDeciCoulombs = convertMahToDeciCoulombs(getConsumedPower(componentId));
-            final long durationMs = getUsageDurationMillis(componentId);
+
+            final BatteryConsumer.Key key = mData.getKey(componentId, PROCESS_STATE_ANY);
+            final long powerDeciCoulombs = convertMahToDeciCoulombs(getConsumedPower(key));
+            final long durationMs = getUsageDurationMillis(key);
 
             if (powerDeciCoulombs == 0 && durationMs == 0) {
                 // No interesting data. Make sure not to even write the COMPONENT int.
@@ -254,25 +285,32 @@
         serializer.startTag(null, BatteryUsageStats.XML_TAG_POWER_COMPONENTS);
         for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
                 componentId++) {
-            final double powerMah = getConsumedPower(componentId);
-            final long durationMs = getUsageDurationMillis(componentId);
-            if (powerMah == 0 && durationMs == 0) {
-                continue;
-            }
+            final BatteryConsumer.Key[] keys = mData.getKeys(componentId);
+            for (BatteryConsumer.Key key : keys) {
+                final double powerMah = getConsumedPower(key);
+                final long durationMs = getUsageDurationMillis(key);
+                if (powerMah == 0 && durationMs == 0) {
+                    continue;
+                }
 
-            serializer.startTag(null, BatteryUsageStats.XML_TAG_COMPONENT);
-            serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, componentId);
-            if (powerMah != 0) {
-                serializer.attributeDouble(null, BatteryUsageStats.XML_ATTR_POWER, powerMah);
+                serializer.startTag(null, BatteryUsageStats.XML_TAG_COMPONENT);
+                serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_ID, componentId);
+                if (key.processState != PROCESS_STATE_ANY) {
+                    serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_PROCESS_STATE,
+                            key.processState);
+                }
+                if (powerMah != 0) {
+                    serializer.attributeDouble(null, BatteryUsageStats.XML_ATTR_POWER, powerMah);
+                }
+                if (durationMs != 0) {
+                    serializer.attributeLong(null, BatteryUsageStats.XML_ATTR_DURATION, durationMs);
+                }
+                if (mData.layout.powerModelsIncluded) {
+                    serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_MODEL,
+                            getPowerModel(key));
+                }
+                serializer.endTag(null, BatteryUsageStats.XML_TAG_COMPONENT);
             }
-            if (durationMs != 0) {
-                serializer.attributeLong(null, BatteryUsageStats.XML_ATTR_DURATION, durationMs);
-            }
-            if (mData.layout.powerModelsIncluded) {
-                serializer.attributeInt(null, BatteryUsageStats.XML_ATTR_MODEL,
-                        getPowerModel(componentId));
-            }
-            serializer.endTag(null, BatteryUsageStats.XML_TAG_COMPONENT);
         }
 
         final int customComponentEnd = BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
@@ -316,6 +354,7 @@
                 switch (parser.getName()) {
                     case BatteryUsageStats.XML_TAG_COMPONENT: {
                         int componentId = -1;
+                        int processState = PROCESS_STATE_ANY;
                         double powerMah = 0;
                         long durationMs = 0;
                         int model = BatteryConsumer.POWER_MODEL_UNDEFINED;
@@ -324,6 +363,9 @@
                                 case BatteryUsageStats.XML_ATTR_ID:
                                     componentId = parser.getAttributeInt(i);
                                     break;
+                                case BatteryUsageStats.XML_ATTR_PROCESS_STATE:
+                                    processState = parser.getAttributeInt(i);
+                                    break;
                                 case BatteryUsageStats.XML_ATTR_POWER:
                                     powerMah = parser.getAttributeDouble(i);
                                     break;
@@ -335,8 +377,10 @@
                                     break;
                             }
                         }
-                        builder.setConsumedPower(componentId, powerMah, model);
-                        builder.setUsageDurationMillis(componentId, durationMs);
+                        final BatteryConsumer.Key key =
+                                builder.mData.getKey(componentId, processState);
+                        builder.setConsumedPower(key, powerMah, model);
+                        builder.setUsageDurationMillis(key, durationMs);
                         break;
                     }
                     case BatteryUsageStats.XML_TAG_CUSTOM_COMPONENT: {
@@ -376,33 +420,22 @@
 
         Builder(BatteryConsumer.BatteryConsumerData data) {
             mData = data;
-            if (mData.layout.powerModelsIncluded) {
-                for (int i = 0; i < BatteryConsumer.POWER_COMPONENT_COUNT; i++) {
-                    mData.putLong(mData.layout.firstPowerModelColumn + i,
-                            POWER_MODEL_UNINITIALIZED);
+            for (BatteryConsumer.Key[] keys : mData.layout.keys) {
+                for (BatteryConsumer.Key key : keys) {
+                    if (key.mPowerModelColumnIndex != -1) {
+                        mData.putInt(key.mPowerModelColumnIndex, POWER_MODEL_UNINITIALIZED);
+                    }
                 }
             }
         }
 
-        /**
-         * Sets the amount of drain attributed to the specified drain type, e.g. CPU, WiFi etc.
-         *
-         * @param componentId    The ID of the power component, e.g.
-         *                       {@link BatteryConsumer#POWER_COMPONENT_CPU}.
-         * @param componentPower Amount of consumed power in mAh.
-         */
         @NonNull
-        public Builder setConsumedPower(@BatteryConsumer.PowerComponent int componentId,
-                double componentPower, @BatteryConsumer.PowerModel int powerModel) {
-            if (componentId >= BatteryConsumer.POWER_COMPONENT_COUNT) {
-                throw new IllegalArgumentException(
-                        "Unsupported power component ID: " + componentId);
+        public Builder setConsumedPower(BatteryConsumer.Key key, double componentPower,
+                int powerModel) {
+            mData.putDouble(key.mPowerColumnIndex, componentPower);
+            if (key.mPowerModelColumnIndex != -1) {
+                mData.putInt(key.mPowerModelColumnIndex, powerModel);
             }
-            mData.putDouble(mData.layout.firstConsumedPowerColumn + componentId, componentPower);
-            if (mData.layout.powerModelsIncluded) {
-                mData.putLong(mData.layout.firstPowerModelColumn + componentId, powerModel);
-            }
-
             return this;
         }
 
@@ -423,22 +456,10 @@
             return this;
         }
 
-        /**
-         * Sets the amount of time used by the specified component, e.g. CPU, WiFi etc.
-         *
-         * @param componentId                  The ID of the power component, e.g.
-         *                                     {@link BatteryConsumer#POWER_COMPONENT_CPU}.
-         * @param componentUsageDurationMillis Amount of time in milliseconds.
-         */
         @NonNull
-        public Builder setUsageDurationMillis(@BatteryConsumer.PowerComponent int componentId,
+        public Builder setUsageDurationMillis(BatteryConsumer.Key key,
                 long componentUsageDurationMillis) {
-            if (componentId >= BatteryConsumer.POWER_COMPONENT_COUNT) {
-                throw new IllegalArgumentException(
-                        "Unsupported power component ID: " + componentId);
-            }
-            mData.putLong(mData.layout.firstUsageDurationColumn + componentId,
-                    componentUsageDurationMillis);
+            mData.putLong(key.mDurationColumnIndex, componentUsageDurationMillis);
             return this;
         }
 
@@ -474,47 +495,75 @@
             if (mData.layout.customPowerComponentCount
                     != otherData.layout.customPowerComponentCount) {
                 throw new IllegalArgumentException(
-                        "Number of power components does not match: "
+                        "Number of custom power components does not match: "
                                 + otherData.layout.customPowerComponentCount
                                 + ", expected: " + mData.layout.customPowerComponentCount);
             }
 
-            for (int i = BatteryConsumer.POWER_COMPONENT_COUNT - 1; i >= 0; i--) {
-                final int powerColumnIndex = mData.layout.firstConsumedPowerColumn + i;
-                mData.putDouble(powerColumnIndex,
-                        mData.getDouble(powerColumnIndex)
-                                + otherData.getDouble(powerColumnIndex));
+            for (int componentId = BatteryConsumer.POWER_COMPONENT_COUNT - 1; componentId >= 0;
+                    componentId--) {
+                final BatteryConsumer.Key[] keys = mData.layout.keys[componentId];
+                for (BatteryConsumer.Key key: keys) {
+                    BatteryConsumer.Key otherKey = null;
+                    for (BatteryConsumer.Key aKey: otherData.layout.keys[componentId]) {
+                        if (aKey.equals(key)) {
+                            otherKey = aKey;
+                            break;
+                        }
+                    }
 
-                final int durationColumnIndex = mData.layout.firstUsageDurationColumn + i;
-                mData.putLong(durationColumnIndex,
-                        mData.getLong(durationColumnIndex)
-                                + otherData.getLong(durationColumnIndex));
+                    if (otherKey == null) {
+                        continue;
+                    }
+
+                    mData.putDouble(key.mPowerColumnIndex,
+                            mData.getDouble(key.mPowerColumnIndex)
+                                    + otherData.getDouble(otherKey.mPowerColumnIndex));
+                    mData.putLong(key.mDurationColumnIndex,
+                            mData.getLong(key.mDurationColumnIndex)
+                                    + otherData.getLong(otherKey.mDurationColumnIndex));
+
+                    if (key.mPowerModelColumnIndex == -1) {
+                        continue;
+                    }
+
+                    boolean undefined = false;
+                    if (otherKey.mPowerModelColumnIndex == -1) {
+                        undefined = true;
+                    } else {
+                        final int powerModel = mData.getInt(key.mPowerModelColumnIndex);
+                        int otherPowerModel = otherData.getInt(otherKey.mPowerModelColumnIndex);
+                        if (powerModel == POWER_MODEL_UNINITIALIZED) {
+                            mData.putInt(key.mPowerModelColumnIndex, otherPowerModel);
+                        } else if (powerModel != otherPowerModel
+                                && otherPowerModel != POWER_MODEL_UNINITIALIZED) {
+                            undefined = true;
+                        }
+                    }
+
+                    if (undefined) {
+                        mData.putInt(key.mPowerModelColumnIndex,
+                                BatteryConsumer.POWER_MODEL_UNDEFINED);
+                    }
+                }
             }
 
             for (int i = mData.layout.customPowerComponentCount - 1; i >= 0; i--) {
                 final int powerColumnIndex = mData.layout.firstCustomConsumedPowerColumn + i;
+                final int otherPowerColumnIndex =
+                        otherData.layout.firstCustomConsumedPowerColumn + i;
                 mData.putDouble(powerColumnIndex,
-                        mData.getDouble(powerColumnIndex) + otherData.getDouble(powerColumnIndex));
+                        mData.getDouble(powerColumnIndex) + otherData.getDouble(
+                                otherPowerColumnIndex));
 
                 final int usageColumnIndex = mData.layout.firstCustomUsageDurationColumn + i;
+                final int otherDurationColumnIndex =
+                        otherData.layout.firstCustomUsageDurationColumn + i;
                 mData.putLong(usageColumnIndex,
-                        mData.getLong(usageColumnIndex) + otherData.getLong(usageColumnIndex)
+                        mData.getLong(usageColumnIndex) + otherData.getLong(
+                                otherDurationColumnIndex)
                 );
             }
-
-            if (mData.layout.powerModelsIncluded && otherData.layout.powerModelsIncluded) {
-                for (int i = BatteryConsumer.POWER_COMPONENT_COUNT - 1; i >= 0; i--) {
-                    final int columnIndex = mData.layout.firstPowerModelColumn + i;
-                    int powerModel = mData.getInt(columnIndex);
-                    int otherPowerModel = otherData.getInt(columnIndex);
-                    if (powerModel == POWER_MODEL_UNINITIALIZED) {
-                        mData.putLong(columnIndex, otherPowerModel);
-                    } else if (powerModel != otherPowerModel
-                            && otherPowerModel != POWER_MODEL_UNINITIALIZED) {
-                        mData.putLong(columnIndex, BatteryConsumer.POWER_MODEL_UNDEFINED);
-                    }
-                }
-            }
         }
 
         /**
@@ -525,11 +574,12 @@
             double totalPowerMah = 0;
             for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT;
                     componentId++) {
-                totalPowerMah +=
-                        mData.getDouble(mData.layout.firstConsumedPowerColumn + componentId);
+                totalPowerMah += mData.getDouble(
+                        mData.getKeyOrThrow(componentId, PROCESS_STATE_ANY).mPowerColumnIndex);
             }
             for (int i = 0; i < mData.layout.customPowerComponentCount; i++) {
-                totalPowerMah += mData.getDouble(mData.layout.firstCustomConsumedPowerColumn + i);
+                totalPowerMah += mData.getDouble(
+                        mData.layout.firstCustomConsumedPowerColumn + i);
             }
             return totalPowerMah;
         }
@@ -539,14 +589,15 @@
          */
         @NonNull
         public PowerComponents build() {
-            mData.putDouble(mData.layout.consumedPowerColumn, getTotalPower());
+            mData.putDouble(mData.layout.totalConsumedPowerColumnIndex, getTotalPower());
 
-            if (mData.layout.powerModelsIncluded) {
-                for (int i = BatteryConsumer.POWER_COMPONENT_COUNT - 1; i >= 0; i--) {
-                    final int powerModel = mData.getInt(mData.layout.firstPowerModelColumn + i);
-                    if (powerModel == POWER_MODEL_UNINITIALIZED) {
-                        mData.putInt(mData.layout.firstPowerModelColumn + i,
-                                BatteryConsumer.POWER_MODEL_UNDEFINED);
+            for (BatteryConsumer.Key[] keys : mData.layout.keys) {
+                for (BatteryConsumer.Key key : keys) {
+                    if (key.mPowerModelColumnIndex != -1) {
+                        if (mData.getInt(key.mPowerModelColumnIndex) == POWER_MODEL_UNINITIALIZED) {
+                            mData.putInt(key.mPowerModelColumnIndex,
+                                    BatteryConsumer.POWER_MODEL_UNDEFINED);
+                        }
                     }
                 }
             }
diff --git a/core/java/android/os/UidBatteryConsumer.java b/core/java/android/os/UidBatteryConsumer.java
index f9f4463..1a082d1 100644
--- a/core/java/android/os/UidBatteryConsumer.java
+++ b/core/java/android/os/UidBatteryConsumer.java
@@ -106,16 +106,39 @@
 
     @Override
     public void dump(PrintWriter pw, boolean skipEmptyComponents) {
-        final double consumedPower = getConsumedPower();
         pw.print("UID ");
         UserHandle.formatUid(pw, getUid());
         pw.print(": ");
-        PowerCalculator.printPowerMah(pw, consumedPower);
+        PowerCalculator.printPowerMah(pw, getConsumedPower());
+
+        if (mData.layout.processStateDataIncluded) {
+            StringBuilder sb = new StringBuilder();
+            appendProcessStateData(sb, BatteryConsumer.PROCESS_STATE_FOREGROUND,
+                    skipEmptyComponents);
+            appendProcessStateData(sb, BatteryConsumer.PROCESS_STATE_BACKGROUND,
+                    skipEmptyComponents);
+            appendProcessStateData(sb, BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE,
+                    skipEmptyComponents);
+            pw.print(sb);
+        }
+
         pw.print(" ( ");
         mPowerComponents.dump(pw, skipEmptyComponents  /* skipTotalPowerComponent */);
         pw.print(" ) ");
     }
 
+    private void appendProcessStateData(StringBuilder sb, @ProcessState int processState,
+            boolean skipEmptyComponents) {
+        Dimensions dimensions = new Dimensions(POWER_COMPONENT_ANY, processState);
+        final double power = mPowerComponents.getConsumedPower(dimensions);
+        if (power == 0 && skipEmptyComponents) {
+            return;
+        }
+
+        sb.append(" ").append(processStateToString(processState)).append(": ")
+                .append(PowerCalculator.formatCharge(power));
+    }
+
     static UidBatteryConsumer create(BatteryConsumerData data) {
         return new UidBatteryConsumer(data);
     }
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index da09375..43d6e47 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -270,6 +270,24 @@
     public static final String DISALLOW_CONFIG_WIFI = "no_config_wifi";
 
     /**
+     * Specifies if a user is disallowed from enabling/disabling Wi-Fi.
+     *
+     * <p>This restriction can only be set by a device owner,
+     * a profile owner of an organization-owned managed profile on the parent profile.
+     * When it is set by any of these owners, it applies globally - i.e., it disables airplane mode
+     * from changing Wi-Fi state.
+     *
+     * <p>The default value is <code>false</code>.
+     *
+     * <p>Key for user restrictions.
+     * <p>Type: Boolean
+     * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+     * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+     * @see #getUserRestrictions()
+     */
+    public static final String DISALLOW_CHANGE_WIFI_STATE = "no_change_wifi_state";
+
+    /**
      * Specifies if a user is disallowed from changing the device
      * language. The default value is <code>false</code>.
      *
@@ -1848,7 +1866,7 @@
             Manifest.permission.MANAGE_USERS}, // Can be INTERACT_ACROSS_USERS instead.
             conditional = true)
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    @UserHandleAware()
+    @UserHandleAware
     public boolean canSwitchUsers() {
         boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
                 mContext.getContentResolver(),
@@ -1929,8 +1947,13 @@
      * Returns the userId for the context user.
      *
      * @return the userId of the context user.
+     *
+     * @deprecated To get the <em>calling</em> user, use {@link UserHandle#myUserId()}.
+     *             To get the <em>context</em> user, get it directly from the context.
+     *
      * @hide
      */
+    @Deprecated
     @UnsupportedAppUsage
     @UserHandleAware(enabledSinceTargetSdkVersion = Build.VERSION_CODES.TIRAMISU)
     // *** Do NOT use this in UserManager. Instead always use mUserId. ***
@@ -1939,6 +1962,21 @@
     }
 
     /**
+     * Returns the userId for the user that this process is running under
+     * (<em>not</em> the context user).
+     *
+     * @return the userId of <em>this process</em>.
+     *
+     * @deprecated Use {@link UserHandle#myUserId()}
+     * @hide
+     */
+    @Deprecated
+    // NOT @UserHandleAware
+    public @UserIdInt int getProcessUserId() {
+        return UserHandle.myUserId();
+    }
+
+    /**
      * @return the user type of the context user.
      * @hide
      */
@@ -2012,7 +2050,7 @@
             return false;
         }
         // Caution: This is NOT @UserHandleAware (because mContext is getApplicationContext and
-        // holds a different userId), but for R+ it returns false, so it doesn't matter anyway.
+        // can hold a different userId), but for R+ it returns false, so it doesn't matter anyway.
         return mContext.getPackageManager()
                 .isPackageAvailable("com.coffeestainstudios.goatsimulator");
     }
@@ -2447,7 +2485,7 @@
      */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.CREATE_USERS})
-    @UserHandleAware()
+    @UserHandleAware
     public boolean isEphemeralUser() {
         return isUserEphemeral(mUserId);
     }
@@ -3315,7 +3353,7 @@
     @TestApi
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.CREATE_USERS})
-    @UserHandleAware()
+    @UserHandleAware
     public @Nullable UserInfo createRestrictedProfile(@Nullable String name) {
         try {
             final int parentUserId = mUserId;
@@ -4605,7 +4643,7 @@
             android.Manifest.permission.MANAGE_USERS,
             android.Manifest.permission.CREATE_USERS // And INTERACT_ if diff profile group
     })
-    @UserHandleAware()
+    @UserHandleAware
     public boolean isUserSwitcherEnabled() {
         return isUserSwitcherEnabled(true);
     }
@@ -4622,7 +4660,7 @@
             android.Manifest.permission.MANAGE_USERS,
             android.Manifest.permission.CREATE_USERS // And INTERACT_ if diff profile group
     })
-    @UserHandleAware()
+    @UserHandleAware
     public boolean isUserSwitcherEnabled(boolean showEvenIfNotActionable) {
         if (!supportsMultipleUsers()) {
             return false;
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 51f19eb..0cc5bfd 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -706,6 +706,25 @@
 
     /**
      * Contains the recent calls.
+     * <p>
+     * Note: If you want to query the call log and limit the results to a single value, you should
+     * append the {@link #LIMIT_PARAM_KEY} parameter to the content URI.  For example:
+     * <pre>
+     * {@code
+     * getContentResolver().query(
+     *                 Calls.CONTENT_URI.buildUpon().appendQueryParameter(LIMIT_PARAM_KEY, "1")
+     *                 .build(),
+     *                 null, null, null, null);
+     * }
+     * </pre>
+     * <p>
+     * The call log provider enforces strict SQL grammar, so you CANNOT append "LIMIT" to the SQL
+     * query as below:
+     * <pre>
+     * {@code
+     * getContentResolver().query(Calls.CONTENT_URI, null, "LIMIT 1", null, null);
+     * }
+     * </pre>
      */
     public static class Calls implements BaseColumns {
         /**
@@ -1742,7 +1761,7 @@
             Uri result = null;
 
             final UserManager userManager = context.getSystemService(UserManager.class);
-            final int currentUserId = userManager.getUserHandle();
+            final int currentUserId = userManager.getProcessUserId();
 
             if (params.mAddForAllUsers) {
                 if (userManager.isUserUnlocked(UserHandle.SYSTEM)) {
diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java
index 28df119..5e66bff 100644
--- a/core/java/android/provider/ContactsContract.java
+++ b/core/java/android/provider/ContactsContract.java
@@ -8692,6 +8692,56 @@
         @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
         public static final String ACTION_SET_DEFAULT_ACCOUNT =
                 "android.provider.action.SET_DEFAULT_ACCOUNT";
+
+        /**
+         * The method to invoke in order to set the default account for new contacts.
+         *
+         * @hide
+         */
+        public static final String SET_DEFAULT_ACCOUNT_METHOD = "setDefaultAccount";
+
+        /**
+         * The method to invoke in order to query the default account for new contacts.
+         *
+         * @hide
+         */
+        public static final String QUERY_DEFAULT_ACCOUNT_METHOD = "queryDefaultAccount";
+
+        /**
+         * Key in the incoming Bundle for the default account.
+         *
+         * @hide
+         */
+        public static final String KEY_DEFAULT_ACCOUNT = "key_default_account";
+
+        /**
+         * Return the account that was set to default account for new contacts.
+         */
+        @Nullable
+        public static Account getDefaultAccount(@NonNull ContentResolver resolver) {
+            Bundle response = resolver.call(ContactsContract.AUTHORITY_URI,
+                    QUERY_DEFAULT_ACCOUNT_METHOD, null, null);
+            return response.getParcelable(KEY_DEFAULT_ACCOUNT);
+        }
+
+        /**
+         * Set the account to be the default account for new contacts.
+         *
+         * @param account the account to be set to default.
+         * @hide
+         */
+        @SystemApi
+        @RequiresPermission(android.Manifest.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS)
+        public static void setDefaultAccount(@NonNull ContentResolver resolver,
+                @Nullable Account account) {
+            Bundle extras = new Bundle();
+            if (account != null) {
+                extras.putString(ACCOUNT_NAME, account.name);
+                extras.putString(ACCOUNT_TYPE, account.type);
+            }
+
+            resolver.call(ContactsContract.AUTHORITY_URI, SET_DEFAULT_ACCOUNT_METHOD, null, extras);
+        }
     }
 
     /**
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index ef486a9..608d006 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -181,6 +181,14 @@
     public static final String NAMESPACE_CONNECTIVITY = "connectivity";
 
     /**
+     * Namespace for CaptivePortalLogin module.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String NAMESPACE_CAPTIVEPORTALLOGIN = "captive_portal_login";
+
+    /**
      * Namespace for content capture feature used by on-device machine intelligence
      * to provide suggestions in a privacy-safe manner.
      *
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index cbd405e..dc6e647 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9723,6 +9723,14 @@
         public static final String NOTIFICATION_DISMISS_RTL = "notification_dismiss_rtl";
 
         /**
+         * Whether the app-level notification setting is represented by a manifest permission.
+         *
+         * @hide
+         */
+        public static final String NOTIFICATION_PERMISSION_ENABLED =
+                "notification_permission_enabled";
+
+        /**
          * Comma separated list of QS tiles that have been auto-added already.
          * @hide
          */
@@ -11050,75 +11058,6 @@
         public static final String HDMI_CONTROL_ENABLED = "hdmi_control_enabled";
 
         /**
-         * Controls whether volume control commands via HDMI CEC are enabled. (0 = false, 1 =
-         * true).
-         *
-         * <p>Effects on different device types:
-         * <table>
-         *     <tr><th>HDMI CEC device type</th><th>0: disabled</th><th>1: enabled</th></tr>
-         *     <tr>
-         *         <td>TV (type: 0)</td>
-         *         <td>Per CEC specification.</td>
-         *         <td>TV changes system volume. TV no longer reacts to incoming volume changes
-         *         via {@code <User Control Pressed>}. TV no longer handles {@code <Report Audio
-         *         Status>}.</td>
-         *     </tr>
-         *     <tr>
-         *         <td>Playback device (type: 4)</td>
-         *         <td>Device sends volume commands to TV/Audio system via {@code <User Control
-         *         Pressed>}</td>
-         *         <td>Device does not send volume commands via {@code <User Control Pressed>}.</td>
-         *     </tr>
-         *     <tr>
-         *         <td>Audio device (type: 5)</td>
-         *         <td>Full "System Audio Control" capabilities.</td>
-         *         <td>Audio device no longer reacts to incoming {@code <User Control Pressed>}
-         *         volume commands. Audio device no longer reports volume changes via {@code
-         *         <Report Audio Status>}.</td>
-         *     </tr>
-         * </table>
-         *
-         * <p> Due to the resulting behavior, usage on TV and Audio devices is discouraged.
-         *
-         * @hide
-         * @see android.hardware.hdmi.HdmiControlManager#setHdmiCecVolumeControlEnabled(boolean)
-         */
-        @Readable
-        public static final String HDMI_CONTROL_VOLUME_CONTROL_ENABLED =
-                "hdmi_control_volume_control_enabled";
-
-        /**
-        * Whether HDMI System Audio Control feature is enabled. If enabled, TV will try to turn on
-        * system audio mode if there's a connected CEC-enabled AV Receiver. Then audio stream will
-        * be played on AVR instead of TV spaeker. If disabled, the system audio mode will never be
-        * activated.
-        * @hide
-        */
-        @Readable
-        public static final String HDMI_SYSTEM_AUDIO_CONTROL_ENABLED =
-                "hdmi_system_audio_control_enabled";
-
-        /**
-         * Whether HDMI Routing Control feature is enabled. If enabled, the switch device will
-         * route to the correct input source on receiving Routing Control related messages. If
-         * disabled, you can only switch the input via controls on this device.
-         * @hide
-         */
-        @Readable
-        public static final String HDMI_CEC_SWITCH_ENABLED =
-                "hdmi_cec_switch_enabled";
-
-        /**
-         * Whether TV will automatically turn on upon reception of the CEC command
-         * &lt;Text View On&gt; or &lt;Image View On&gt;. (0 = false, 1 = true)
-         *
-         * @hide
-         */
-        @Readable
-        public static final String HDMI_CONTROL_AUTO_WAKEUP_ENABLED =
-                "hdmi_control_auto_wakeup_enabled";
-
-        /**
          * Whether TV will also turn off other CEC devices when it goes to standby mode.
          * (0 = false, 1 = true)
          *
@@ -11129,30 +11068,6 @@
                 "hdmi_control_auto_device_off_enabled";
 
         /**
-         * Property to decide which devices the playback device can send a <Standby> message to
-         * upon going to sleep. It additionally controls whether a playback device attempts to turn
-         * on the connected Audio system when waking up. Supported values are:
-         * <ul>
-         * <li>{@link HdmiControlManager#POWER_CONTROL_MODE_TV} Upon going to sleep, device
-         * sends {@code <Standby>} to TV only. Upon waking up, device does not turn on the Audio
-         * system via {@code <System Audio Mode Request>}.</li>
-         * <li>{@link HdmiControlManager#POWER_CONTROL_MODE_TV_AND_AUDIO_SYSTEM} Upon going to
-         * sleep, sends {@code <Standby>} to TV and Audio system. Upon waking up, device attempts
-         * to turn on the Audio system via {@code <System Audio Mode Request>}.</li>
-         * <li>{@link HdmiControlManager#POWER_CONTROL_MODE_BROADCAST} Upon going to sleep,
-         * device sends {@code <Standby>} to all devices in the network. Upon waking up, device
-         * attempts to turn on the Audio system via {@code <System Audio Mode Request>}.</li>
-         * <li>{@link HdmiControlManager#POWER_CONTROL_MODE_NONE} Upon going to sleep, device
-         * does not send any {@code <Standby>} message. Upon waking up, device does not turn on the
-         * Audio system via {@code <System Audio Mode Request>}.</li>
-         * </ul>
-         *
-         * @hide
-         */
-        public static final String HDMI_CONTROL_SEND_STANDBY_ON_SLEEP =
-                "hdmi_control_send_standby_on_sleep";
-
-        /**
          * Whether or not media is shown automatically when bypassing as a heads up.
          * @hide
          */
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 71f90fd2..3d18a89 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -83,11 +83,11 @@
  *     &lt;/intent-filter>
  *     &lt;meta-data
  *               android:name="android.service.notification.default_filter_types"
- *               android:value="conversations,alerting">
+ *               android:value="conversations|alerting">
  *           &lt;/meta-data>
  *     &lt;meta-data
  *               android:name="android.service.notification.disabled_filter_types"
- *               android:value="ongoing,silent">
+ *               android:value="ongoing|silent">
  *           &lt;/meta-data>
  * &lt;/service></pre>
  *
@@ -112,8 +112,9 @@
     private final String TAG = getClass().getSimpleName();
 
     /**
-     * The name of the {@code meta-data} tag containing a comma separated list of default
-     * integer notification types that should be provided to this listener. See
+     * The name of the {@code meta-data} tag containing a pipe separated list of default
+     * integer notification types or "ongoing", "conversations", "alerting", or "silent"
+     * that should be provided to this listener. See
      * {@link #FLAG_FILTER_TYPE_ONGOING},
      * {@link #FLAG_FILTER_TYPE_CONVERSATIONS}, {@link #FLAG_FILTER_TYPE_ALERTING),
      * and {@link #FLAG_FILTER_TYPE_SILENT}.
diff --git a/core/java/android/service/timezone/ITimeZoneProvider.aidl b/core/java/android/service/timezone/ITimeZoneProvider.aidl
index 793bcc6..4a404cf 100644
--- a/core/java/android/service/timezone/ITimeZoneProvider.aidl
+++ b/core/java/android/service/timezone/ITimeZoneProvider.aidl
@@ -22,6 +22,7 @@
  * @hide
  */
 oneway interface ITimeZoneProvider {
-    void startUpdates(in ITimeZoneProviderManager manager, in long initializationTimeoutMillis);
+    void startUpdates(in ITimeZoneProviderManager manager, in long initializationTimeoutMillis,
+            in long eventFilteringAgeThresholdMillis);
     void stopUpdates();
 }
diff --git a/core/java/android/service/timezone/ITimeZoneProviderManager.aidl b/core/java/android/service/timezone/ITimeZoneProviderManager.aidl
index bf4fe0a..bccb096 100644
--- a/core/java/android/service/timezone/ITimeZoneProviderManager.aidl
+++ b/core/java/android/service/timezone/ITimeZoneProviderManager.aidl
@@ -16,13 +16,11 @@
 
 package android.service.timezone;
 
-import android.service.timezone.TimeZoneProviderSuggestion;
+import android.service.timezone.TimeZoneProviderEvent;
 
 /**
  * @hide
  */
 oneway interface ITimeZoneProviderManager {
-    void onTimeZoneProviderSuggestion(in TimeZoneProviderSuggestion timeZoneProviderSuggestion);
-    void onTimeZoneProviderUncertain();
-    void onTimeZoneProviderPermanentFailure(in String failureReason);
+    void onTimeZoneProviderEvent(in TimeZoneProviderEvent timeZoneProviderEvent);
 }
diff --git a/core/java/android/service/timezone/OWNERS b/core/java/android/service/timezone/OWNERS
index 28aff18..b5144d1 100644
--- a/core/java/android/service/timezone/OWNERS
+++ b/core/java/android/service/timezone/OWNERS
@@ -1,3 +1,3 @@
 # Bug component: 847766
-nfuller@google.com
-include /core/java/android/app/timedetector/OWNERS
+# System APIs for system server time zone detection plugins.
+include /services/core/java/com/android/server/timezonedetector/OWNERS
diff --git a/core/java/android/service/timezone/TimeZoneProviderEvent.aidl b/core/java/android/service/timezone/TimeZoneProviderEvent.aidl
new file mode 100644
index 0000000..b7a3533
--- /dev/null
+++ b/core/java/android/service/timezone/TimeZoneProviderEvent.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2021, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.timezone;
+
+/**
+ * @hide
+ */
+parcelable TimeZoneProviderEvent;
diff --git a/core/java/android/service/timezone/TimeZoneProviderEvent.java b/core/java/android/service/timezone/TimeZoneProviderEvent.java
new file mode 100644
index 0000000..7005281
--- /dev/null
+++ b/core/java/android/service/timezone/TimeZoneProviderEvent.java
@@ -0,0 +1,218 @@
+/*
+ * 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 android.service.timezone;
+
+import android.annotation.ElapsedRealtimeLong;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.time.Duration;
+import java.util.Objects;
+
+/**
+ * Encapsulates a reported event from a {@link TimeZoneProviderService}.
+ *
+ * @hide
+ */
+public final class TimeZoneProviderEvent implements Parcelable {
+
+    @IntDef(prefix = "EVENT_TYPE_",
+            value = { EVENT_TYPE_PERMANENT_FAILURE, EVENT_TYPE_SUGGESTION, EVENT_TYPE_UNCERTAIN })
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER })
+    public @interface EventType {}
+
+    /**
+     * The provider failed permanently. See {@link
+     * TimeZoneProviderService#reportPermanentFailure(Throwable)}
+     */
+    public static final @EventType int EVENT_TYPE_PERMANENT_FAILURE = 1;
+
+    /**
+     * The provider made a suggestion. See {@link
+     * TimeZoneProviderService#reportSuggestion(TimeZoneProviderSuggestion)}
+     */
+    public static final @EventType int EVENT_TYPE_SUGGESTION = 2;
+
+    /**
+     * The provider was uncertain about the time zone. See {@link
+     * TimeZoneProviderService#reportUncertain()}
+     */
+    public static final @EventType int EVENT_TYPE_UNCERTAIN = 3;
+
+    private final @EventType int mType;
+
+    @ElapsedRealtimeLong
+    private final long mCreationElapsedMillis;
+
+    @Nullable
+    private final TimeZoneProviderSuggestion mSuggestion;
+
+    @Nullable
+    private final String mFailureCause;
+
+    private TimeZoneProviderEvent(@EventType int type,
+            @ElapsedRealtimeLong long creationElapsedMillis,
+            @Nullable TimeZoneProviderSuggestion suggestion,
+            @Nullable String failureCause) {
+        mType = type;
+        mCreationElapsedMillis = creationElapsedMillis;
+        mSuggestion = suggestion;
+        mFailureCause = failureCause;
+    }
+
+    /** Returns a event of type {@link #EVENT_TYPE_SUGGESTION}. */
+    public static TimeZoneProviderEvent createSuggestionEvent(
+            @ElapsedRealtimeLong long creationElapsedMillis,
+            @NonNull TimeZoneProviderSuggestion suggestion) {
+        return new TimeZoneProviderEvent(EVENT_TYPE_SUGGESTION, creationElapsedMillis,
+                Objects.requireNonNull(suggestion), null);
+    }
+
+    /** Returns a event of type {@link #EVENT_TYPE_UNCERTAIN}. */
+    public static TimeZoneProviderEvent createUncertainEvent(
+            @ElapsedRealtimeLong long creationElapsedMillis) {
+        return new TimeZoneProviderEvent(EVENT_TYPE_UNCERTAIN, creationElapsedMillis, null, null);
+    }
+
+    /** Returns a event of type {@link #EVENT_TYPE_PERMANENT_FAILURE}. */
+    public static TimeZoneProviderEvent createPermanentFailureEvent(
+            @ElapsedRealtimeLong long creationElapsedMillis,
+            @NonNull String cause) {
+        return new TimeZoneProviderEvent(EVENT_TYPE_PERMANENT_FAILURE, creationElapsedMillis, null,
+                Objects.requireNonNull(cause));
+    }
+
+    /**
+     * Returns the event type.
+     */
+    public @EventType int getType() {
+        return mType;
+    }
+
+    /** Returns the time according to the elapsed realtime clock when the event was created. */
+    @ElapsedRealtimeLong
+    public long getCreationElapsedMillis() {
+        return mCreationElapsedMillis;
+    }
+
+    /**
+     * Returns the suggestion. Populated when {@link #getType()} is {@link #EVENT_TYPE_SUGGESTION}.
+     */
+    @Nullable
+    public TimeZoneProviderSuggestion getSuggestion() {
+        return mSuggestion;
+    }
+
+    /**
+     * Returns the failure cauese. Populated when {@link #getType()} is {@link
+     * #EVENT_TYPE_PERMANENT_FAILURE}.
+     */
+    @Nullable
+    public String getFailureCause() {
+        return mFailureCause;
+    }
+
+    public static final @NonNull Creator<TimeZoneProviderEvent> CREATOR =
+            new Creator<TimeZoneProviderEvent>() {
+                @Override
+                public TimeZoneProviderEvent createFromParcel(Parcel in) {
+                    int type = in.readInt();
+                    long creationElapsedMillis = in.readLong();
+                    TimeZoneProviderSuggestion suggestion =
+                            in.readParcelable(getClass().getClassLoader());
+                    String failureCause = in.readString8();
+                    return new TimeZoneProviderEvent(
+                            type, creationElapsedMillis, suggestion, failureCause);
+                }
+
+                @Override
+                public TimeZoneProviderEvent[] newArray(int size) {
+                    return new TimeZoneProviderEvent[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel parcel, int flags) {
+        parcel.writeInt(mType);
+        parcel.writeLong(mCreationElapsedMillis);
+        parcel.writeParcelable(mSuggestion, 0);
+        parcel.writeString8(mFailureCause);
+    }
+
+    @Override
+    public String toString() {
+        return "TimeZoneProviderEvent{"
+                + "mType=" + mType
+                + ", mCreationElapsedMillis=" + Duration.ofMillis(mCreationElapsedMillis).toString()
+                + ", mSuggestion=" + mSuggestion
+                + ", mFailureCause=" + mFailureCause
+                + '}';
+    }
+
+    /**
+     * Similar to {@link #equals} except this methods checks for equivalence, not equality.
+     * i.e. two {@link #EVENT_TYPE_UNCERTAIN} and {@link #EVENT_TYPE_PERMANENT_FAILURE} events are
+     * always equivalent, two {@link #EVENT_TYPE_SUGGESTION} events are equivalent if they suggest
+     * the same time zones.
+     */
+    @SuppressWarnings("ReferenceEquality")
+    public boolean isEquivalentTo(@Nullable TimeZoneProviderEvent other) {
+        if (this == other) {
+            return true;
+        }
+        if (other == null || mType != other.mType) {
+            return false;
+        }
+        if (mType == EVENT_TYPE_SUGGESTION) {
+            return mSuggestion.isEquivalentTo(other.getSuggestion());
+        }
+        return true;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        TimeZoneProviderEvent that = (TimeZoneProviderEvent) o;
+        return mType == that.mType
+                && mCreationElapsedMillis == that.mCreationElapsedMillis
+                && Objects.equals(mSuggestion, that.mSuggestion)
+                && Objects.equals(mFailureCause, that.mFailureCause);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mCreationElapsedMillis, mSuggestion, mFailureCause);
+    }
+}
diff --git a/core/java/android/service/timezone/TimeZoneProviderService.java b/core/java/android/service/timezone/TimeZoneProviderService.java
index b516b02..0d215f6 100644
--- a/core/java/android/service/timezone/TimeZoneProviderService.java
+++ b/core/java/android/service/timezone/TimeZoneProviderService.java
@@ -26,10 +26,14 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.os.BackgroundThread;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.util.Objects;
 
 /**
@@ -122,7 +126,9 @@
  * #onDestroy()} can occur on a different thread from those made to {@link
  * TimeZoneProviderService}-defined service methods, so implementations must be defensive and not
  * assume an ordering between them, e.g. a call to {@link #onStopUpdates()} can occur after {@link
- * #onDestroy()} and should be handled safely.
+ * #onDestroy()} and should be handled safely. {@link #mLock} is used to ensure that synchronous
+ * calls like {@link #dump(FileDescriptor, PrintWriter, String[])} are safe with respect to
+ * asynchronous behavior.
  *
  * @hide
  */
@@ -162,12 +168,30 @@
 
     private final TimeZoneProviderServiceWrapper mWrapper = new TimeZoneProviderServiceWrapper();
 
+    /** The object used for operations that occur between the main / handler thread. */
+    private final Object mLock = new Object();
+
+    /** The handler used for most operations. */
     private final Handler mHandler = BackgroundThread.getHandler();
 
     /** Set by {@link #mHandler} thread. */
+    @GuardedBy("mLock")
     @Nullable
     private ITimeZoneProviderManager mManager;
 
+    /** Set by {@link #mHandler} thread. */
+    @GuardedBy("mLock")
+    private long mEventFilteringAgeThresholdMillis;
+
+    /**
+     * The type of the last suggestion sent to the system server. Used to de-dupe suggestions client
+     * side and avoid calling into the system server unnecessarily. {@code null} means no previous
+     * event has been sent this cycle; this field is cleared when the service is started.
+     */
+    @GuardedBy("mLock")
+    @Nullable
+    private TimeZoneProviderEvent mLastEventSent;
+
     @Override
     @NonNull
     public final IBinder onBind(@NonNull Intent intent) {
@@ -182,12 +206,20 @@
         Objects.requireNonNull(suggestion);
 
         mHandler.post(() -> {
-            ITimeZoneProviderManager manager = mManager;
-            if (manager != null) {
-                try {
-                    manager.onTimeZoneProviderSuggestion(suggestion);
-                } catch (RemoteException | RuntimeException e) {
-                    Log.w(TAG, e);
+            synchronized (mLock) {
+                ITimeZoneProviderManager manager = mManager;
+                if (manager != null) {
+                    try {
+                        TimeZoneProviderEvent thisEvent =
+                                TimeZoneProviderEvent.createSuggestionEvent(
+                                        SystemClock.elapsedRealtime(), suggestion);
+                        if (shouldSendEvent(thisEvent)) {
+                            manager.onTimeZoneProviderEvent(thisEvent);
+                            mLastEventSent = thisEvent;
+                        }
+                    } catch (RemoteException | RuntimeException e) {
+                        Log.w(TAG, e);
+                    }
                 }
             }
         });
@@ -200,12 +232,20 @@
      */
     public final void reportUncertain() {
         mHandler.post(() -> {
-            ITimeZoneProviderManager manager = mManager;
-            if (manager != null) {
-                try {
-                    manager.onTimeZoneProviderUncertain();
-                } catch (RemoteException | RuntimeException e) {
-                    Log.w(TAG, e);
+            synchronized (mLock) {
+                ITimeZoneProviderManager manager = mManager;
+                if (manager != null) {
+                    try {
+                        TimeZoneProviderEvent thisEvent =
+                                TimeZoneProviderEvent.createUncertainEvent(
+                                        SystemClock.elapsedRealtime());
+                        if (shouldSendEvent(thisEvent)) {
+                            manager.onTimeZoneProviderEvent(thisEvent);
+                            mLastEventSent = thisEvent;
+                        }
+                    } catch (RemoteException | RuntimeException e) {
+                        Log.w(TAG, e);
+                    }
                 }
             }
         });
@@ -219,21 +259,56 @@
         Objects.requireNonNull(cause);
 
         mHandler.post(() -> {
-            ITimeZoneProviderManager manager = mManager;
-            if (manager != null) {
-                try {
-                    manager.onTimeZoneProviderPermanentFailure(cause.getMessage());
-                } catch (RemoteException | RuntimeException e) {
-                    Log.w(TAG, e);
+            synchronized (mLock) {
+                ITimeZoneProviderManager manager = mManager;
+                if (manager != null) {
+                    try {
+                        String causeString = cause.getMessage();
+                        TimeZoneProviderEvent thisEvent =
+                                TimeZoneProviderEvent.createPermanentFailureEvent(
+                                        SystemClock.elapsedRealtime(), causeString);
+                        if (shouldSendEvent(thisEvent)) {
+                            manager.onTimeZoneProviderEvent(thisEvent);
+                            mLastEventSent = thisEvent;
+                        }
+                    } catch (RemoteException | RuntimeException e) {
+                        Log.w(TAG, e);
+                    }
                 }
             }
         });
     }
 
+    @GuardedBy("mLock")
+    private boolean shouldSendEvent(TimeZoneProviderEvent newEvent) {
+        // Always send an event if it indicates a state or suggestion change.
+        if (!newEvent.isEquivalentTo(mLastEventSent)) {
+            return true;
+        }
+
+        // Guard against implementations that generate a lot of uninteresting events in a short
+        // space of time and would cause the time_zone_detector to evaluate time zone suggestions
+        // too frequently.
+        //
+        // If the new event and last event sent are equivalent, the client will still send an update
+        // if their creation times are sufficiently different. This enables the time_zone_detector
+        // to better understand how recently the location time zone provider was certain /
+        // uncertain, which can be useful when working out ordering of events, e.g. to work out
+        // whether a suggestion was generated before or after a device left airplane mode.
+        long timeSinceLastEventMillis =
+                newEvent.getCreationElapsedMillis() - mLastEventSent.getCreationElapsedMillis();
+        return timeSinceLastEventMillis > mEventFilteringAgeThresholdMillis;
+    }
+
     private void onStartUpdatesInternal(@NonNull ITimeZoneProviderManager manager,
-            @DurationMillisLong long initializationTimeoutMillis) {
-        mManager = manager;
-        onStartUpdates(initializationTimeoutMillis);
+            @DurationMillisLong long initializationTimeoutMillis,
+            @DurationMillisLong long eventFilteringAgeThresholdMillis) {
+        synchronized (mLock) {
+            mManager = manager;
+            mEventFilteringAgeThresholdMillis =  eventFilteringAgeThresholdMillis;
+            mLastEventSent = null;
+            onStartUpdates(initializationTimeoutMillis);
+        }
     }
 
     /**
@@ -265,8 +340,10 @@
     public abstract void onStartUpdates(@DurationMillisLong long initializationTimeoutMillis);
 
     private void onStopUpdatesInternal() {
-        onStopUpdates();
-        mManager = null;
+        synchronized (mLock) {
+            onStopUpdates();
+            mManager = null;
+        }
     }
 
     /**
@@ -275,12 +352,22 @@
      */
     public abstract void onStopUpdates();
 
+    /** @hide */
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        synchronized (mLock) {
+            writer.append("mLastEventSent=" + mLastEventSent);
+        }
+    }
+
     private class TimeZoneProviderServiceWrapper extends ITimeZoneProvider.Stub {
 
         public void startUpdates(@NonNull ITimeZoneProviderManager manager,
-                @DurationMillisLong long initializationTimeoutMillis) {
+                @DurationMillisLong long initializationTimeoutMillis,
+                @DurationMillisLong long eventFilteringAgeThresholdMillis) {
             Objects.requireNonNull(manager);
-            mHandler.post(() -> onStartUpdatesInternal(manager, initializationTimeoutMillis));
+            mHandler.post(() -> onStartUpdatesInternal(
+                    manager, initializationTimeoutMillis, eventFilteringAgeThresholdMillis));
         }
 
         public void stopUpdates() {
diff --git a/core/java/android/service/timezone/TimeZoneProviderSuggestion.java b/core/java/android/service/timezone/TimeZoneProviderSuggestion.java
index cf299a7..229fa26 100644
--- a/core/java/android/service/timezone/TimeZoneProviderSuggestion.java
+++ b/core/java/android/service/timezone/TimeZoneProviderSuggestion.java
@@ -18,6 +18,7 @@
 
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -121,6 +122,24 @@
         parcel.writeLong(mElapsedRealtimeMillis);
     }
 
+    /**
+     * Similar to {@link #equals} except this methods checks for equivalence, not equality.
+     * i.e. two suggestions are equivalent if they suggest the same time zones.
+     *
+     * @hide
+     */
+    @SuppressWarnings("ReferenceEquality")
+    public boolean isEquivalentTo(@Nullable TimeZoneProviderSuggestion other) {
+        if (this == other) {
+            return true;
+        }
+        if (other == null) {
+            return false;
+        }
+        // Only check the time zone IDs. The times can be different, but we don't mind.
+        return mTimeZoneIds.equals(other.mTimeZoneIds);
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java
index 6e25160..946fc30 100644
--- a/core/java/android/text/util/Linkify.java
+++ b/core/java/android/text/util/Linkify.java
@@ -111,19 +111,25 @@
      *  {@link android.webkit.WebView#findAddress(String)} for more information.
      *
      *  @deprecated use {@link android.view.textclassifier.TextClassifier#generateLinks(
-     *  TextLinks.Request)} instead and avoid it even when targeting API levels where no alternative
-     *  is available.
+     *  TextLinks.Request)} instead, and avoid {@link #MAP_ADDRESSES} even when targeting API levels
+     *  where no alternative is available.
      */
     @Deprecated
     public static final int MAP_ADDRESSES = 0x08;
 
     /**
-     *  Bit mask indicating that all available patterns should be matched in
-     *  methods that take an options mask
-     *  <p><strong>Note:</strong></p> {@link #MAP_ADDRESSES} is deprecated.
-     *  Use {@link android.view.textclassifier.TextClassifier#generateLinks(TextLinks.Request)}
-     *  instead and avoid it even when targeting API levels where no alternative is available.
+     *  Bit mask indicating that all available patterns should be matched in methods
+     *  that take an options mask. Note that this should be avoided, as the {@link
+     *  #MAP_ADDRESSES} field uses the {@link android.webkit.WebView#findAddress(
+     *  String)} method, which has various limitations and has been deprecated: see
+     *  the documentation for {@link android.webkit.WebView#findAddress(String)} for
+     *  more information.
+     *
+     *  @deprecated use {@link android.view.textclassifier.TextClassifier#generateLinks(
+     *  TextLinks.Request)} instead, and avoid {@link #ALL} even when targeting API levels where no
+     *  alternative is available.
      */
+    @Deprecated
     public static final int ALL = WEB_URLS | EMAIL_ADDRESSES | PHONE_NUMBERS | MAP_ADDRESSES;
 
     /**
diff --git a/core/java/android/timezone/OWNERS b/core/java/android/timezone/OWNERS
index 8f80897..8b5e156 100644
--- a/core/java/android/timezone/OWNERS
+++ b/core/java/android/timezone/OWNERS
@@ -1,3 +1,5 @@
-# Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+# Bug component: 24949
+# APIs originally intended to provide a stable API surface to access time zone rules data for use by
+# unbundled components like a telephony mainline module and the ART module. Not exposed, potentially
+# deletable if callers do not unbundle.
+include platform/libcore:/OWNERS
diff --git a/core/java/android/view/AccessibilityEmbeddedConnection.java b/core/java/android/view/AccessibilityEmbeddedConnection.java
index 5d34669..de895d9 100644
--- a/core/java/android/view/AccessibilityEmbeddedConnection.java
+++ b/core/java/android/view/AccessibilityEmbeddedConnection.java
@@ -63,7 +63,6 @@
                     viewRootImpl.mContext);
             viewRootImpl.mAttachInfo.mLeashedParentToken = null;
             viewRootImpl.mAttachInfo.mLeashedParentAccessibilityViewId = View.NO_ID;
-            viewRootImpl.mAttachInfo.mLocationInParentDisplay.set(0, 0);
             if (accessibilityManager.isEnabled()) {
                 accessibilityManager.disassociateEmbeddedHierarchy(viewRootImpl.mLeashToken);
             }
diff --git a/core/java/android/view/AccessibilityInteractionController.java b/core/java/android/view/AccessibilityInteractionController.java
index 656fdbd..9cd8313 100644
--- a/core/java/android/view/AccessibilityInteractionController.java
+++ b/core/java/android/view/AccessibilityInteractionController.java
@@ -878,21 +878,6 @@
         return false;
     }
 
-    private void adjustBoundsInScreenIfNeeded(AccessibilityNodeInfo info) {
-        if (info == null || shouldBypassAdjustBoundsInScreen()) {
-            return;
-        }
-        final Rect boundsInScreen = mTempRect;
-        info.getBoundsInScreen(boundsInScreen);
-        boundsInScreen.offset(mViewRootImpl.mAttachInfo.mLocationInParentDisplay.x,
-                mViewRootImpl.mAttachInfo.mLocationInParentDisplay.y);
-        info.setBoundsInScreen(boundsInScreen);
-    }
-
-    private boolean shouldBypassAdjustBoundsInScreen() {
-        return mViewRootImpl.mAttachInfo.mLocationInParentDisplay.equals(0, 0);
-    }
-
     private void applyScreenMatrixIfNeeded(List<AccessibilityNodeInfo> infos) {
         if (infos == null || shouldBypassApplyScreenMatrix()) {
             return;
@@ -1009,7 +994,6 @@
                                        Region interactiveRegion) {
         associateLeashedParentIfNeeded(info);
         applyScreenMatrixIfNeeded(info);
-        adjustBoundsInScreenIfNeeded(info);
         // To avoid applyAppScaleAndMagnificationSpecIfNeeded changing the bounds of node,
         // then impact the visibility result, we need to adjust visibility before apply scale.
         adjustIsVisibleToUserIfNeeded(info, interactiveRegion);
diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java
index b2fc9a0..69af2a5 100644
--- a/core/java/android/view/AttachedSurfaceControl.java
+++ b/core/java/android/view/AttachedSurfaceControl.java
@@ -18,6 +18,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UiThread;
+import android.hardware.HardwareBuffer;
 
 /**
  * Provides an interface to the root-Surface of a View Hierarchy or Window. This
@@ -84,41 +85,43 @@
      * Note, when using ANativeWindow APIs in conjunction with a NativeActivity Surface or
      * SurfaceView Surface, the buffer producer will already have access to the transform hint and
      * no additional work is needed.
+     *
+     * @see HardwareBuffer
      */
-    default @Surface.Rotation int getSurfaceTransformHint() {
-        return Surface.ROTATION_0;
+    default @SurfaceControl.BufferTransform int getBufferTransformHint() {
+        return SurfaceControl.BUFFER_TRANSFORM_IDENTITY;
     }
 
     /**
-     * Surface transform hint change listener.
-     * @see #getSurfaceTransformHint
+     * Buffer transform hint change listener.
+     * @see #getBufferTransformHint
      */
     @UiThread
-    interface OnSurfaceTransformHintChangedListener {
+    interface OnBufferTransformHintChangedListener {
         /**
          * @param hint new surface transform hint
-         * @see #getSurfaceTransformHint
+         * @see #getBufferTransformHint
          */
-        void onSurfaceTransformHintChanged(@Surface.Rotation int hint);
+        void onBufferTransformHintChanged(@SurfaceControl.BufferTransform int hint);
     }
 
     /**
-     * Registers a surface transform hint changed listener to receive notifications about when
+     * Registers a {@link OnBufferTransformHintChangedListener} to receive notifications about when
      * the transform hint changes.
      *
-     * @see #getSurfaceTransformHint
-     * @see #removeOnSurfaceTransformHintChangedListener
+     * @see #getBufferTransformHint
+     * @see #removeOnBufferTransformHintChangedListener
      */
-    default void addOnSurfaceTransformHintChangedListener(
-            @NonNull OnSurfaceTransformHintChangedListener listener) {
+    default void addOnBufferTransformHintChangedListener(
+            @NonNull OnBufferTransformHintChangedListener listener) {
     }
 
     /**
-     * Unregisters a surface transform hint changed listener.
+     * Unregisters a {@link OnBufferTransformHintChangedListener}.
      *
-     * @see #addOnSurfaceTransformHintChangedListener
+     * @see #addOnBufferTransformHintChangedListener
      */
-    default void removeOnSurfaceTransformHintChangedListener(
-            @NonNull OnSurfaceTransformHintChangedListener listener) {
+    default void removeOnBufferTransformHintChangedListener(
+            @NonNull OnBufferTransformHintChangedListener listener) {
     }
 }
diff --git a/core/java/android/view/IApplicationToken.aidl b/core/java/android/view/IApplicationToken.aidl
deleted file mode 100644
index a063a70..0000000
--- a/core/java/android/view/IApplicationToken.aidl
+++ /dev/null
@@ -1,25 +0,0 @@
-/* //device/java/android/android/view/IApplicationToken.aidl
-**
-** Copyright 2007, 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;
-
-/** {@hide} */
-interface IApplicationToken
-{
-  String getName();
-}
-
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index b8b13b9..2ee112b 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -58,12 +58,6 @@
             boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId);
 
     /**
-     * Called when the window location in parent display has changed. The offset will only be a
-     * nonzero value if the window is on an embedded display that is re-parented to another window.
-     */
-    void locationInParentDisplayChanged(in Point offset);
-
-    /**
      * Called when the window insets configuration has changed.
      *
      * @param willMove The window frame will be moved soon.
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index ae2eed8..f74008b 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -34,7 +34,6 @@
 import android.os.ParcelFileDescriptor;
 import android.view.DisplayCutout;
 import android.view.DisplayInfo;
-import android.view.IApplicationToken;
 import android.view.IAppTransitionAnimationSpecsFuture;
 import android.view.ICrossWindowBlurEnabledListener;
 import android.view.IDisplayWindowInsetsController;
@@ -548,16 +547,6 @@
     boolean isWindowTraceEnabled();
 
     /**
-     * Notify WindowManager that it should not override the info in DisplayManager for the specified
-     * display. This can disable letter- or pillar-boxing applied in DisplayManager when the metrics
-     * of the logical display reported from WindowManager do not correspond to the metrics of the
-     * physical display it is based on.
-     *
-     * @param displayId The id of the display.
-     */
-    void dontOverrideDisplayInfo(int displayId);
-
-    /**
      * Gets the windowing mode of the display.
      *
      * @param displayId The id of the display.
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 9da5088..921ce53 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -273,17 +273,6 @@
     oneway void updatePointerIcon(IWindow window);
 
     /**
-     * Update the location of a child display in its parent window. This enables windows in the
-     * child display to compute the global transformation matrix.
-     *
-     * @param window The parent window of the display.
-     * @param x The x coordinate in the parent window.
-     * @param y The y coordinate in the parent window.
-     * @param displayId The id of the display to be notified.
-     */
-    oneway void updateDisplayContentLocation(IWindow window, int x, int y, int displayId);
-
-    /**
      * Update a tap exclude region identified by provided id in the window. Touches on this region
      * will neither be dispatched to this window nor change the focus to this window. Passing an
      * invalid region will remove the area from the exclude region of this window.
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index 40942ea7..3d57db9 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -434,8 +434,8 @@
 
     /**
      * This flag indicates that the window that received this motion event is partly
-     * or wholly obscured by another visible window above it. This flag is set to true
-     * if the event directly passed through the obscured area.
+     * or wholly obscured by another visible window above it and the event directly passed through
+     * the obscured area.
      *
      * A security sensitive application can check this flag to identify situations in which
      * a malicious application may have covered up part of its content for the purpose
@@ -447,8 +447,8 @@
 
     /**
      * This flag indicates that the window that received this motion event is partly
-     * or wholly obscured by another visible window above it. This flag is set to true
-     * even if the event did not directly pass through the obscured area.
+     * or wholly obscured by another visible window above it and the event did not directly pass
+     * through the obscured area.
      *
      * A security sensitive application can check this flag to identify situations in which
      * a malicious application may have covered up part of its content for the purpose
@@ -456,7 +456,7 @@
      * to drop the suspect touches or to take additional precautions to confirm the user's
      * actual intent.
      *
-     * Unlike FLAG_WINDOW_IS_OBSCURED, this is true even if the window that received this event is
+     * Unlike FLAG_WINDOW_IS_OBSCURED, this is only true if the window that received this event is
      * obstructed in areas other than the touched location.
      */
     public static final int FLAG_WINDOW_IS_PARTIALLY_OBSCURED = 0x2;
diff --git a/core/java/android/view/ScrollCaptureConnection.java b/core/java/android/view/ScrollCaptureConnection.java
index 99141c3..278b2fc 100644
--- a/core/java/android/view/ScrollCaptureConnection.java
+++ b/core/java/android/view/ScrollCaptureConnection.java
@@ -24,9 +24,9 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.CancellationSignal;
+import android.os.IBinder;
 import android.os.ICancellationSignal;
 import android.os.RemoteException;
-import android.os.Trace;
 import android.util.CloseGuard;
 import android.util.Log;
 
@@ -44,7 +44,8 @@
  *
  * @hide
  */
-public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub {
+public class ScrollCaptureConnection extends IScrollCaptureConnection.Stub implements
+        IBinder.DeathRecipient {
 
     private static final String TAG = "ScrollCaptureConnection";
 
@@ -54,15 +55,13 @@
     private final Executor mUiThread;
     private final CloseGuard mCloseGuard = new CloseGuard();
 
-
     private ScrollCaptureCallback mLocal;
     private IScrollCaptureCallbacks mRemote;
-
     private ScrollCaptureSession mSession;
-
     private CancellationSignal mCancellation;
 
     private volatile boolean mActive;
+    private volatile boolean mConnected;
 
     /**
      * Constructs a ScrollCaptureConnection.
@@ -87,13 +86,14 @@
     @Override
     public ICancellationSignal startCapture(@NonNull Surface surface,
             @NonNull IScrollCaptureCallbacks remote) throws RemoteException {
-
         mCloseGuard.open("close");
 
         if (!surface.isValid()) {
             throw new RemoteException(new IllegalArgumentException("surface must be valid"));
         }
         mRemote = requireNonNull(remote, "<callbacks> must non-null");
+        mRemote.asBinder().linkToDeath(this, 0);
+        mConnected = true;
 
         ICancellationSignal cancellation = CancellationSignal.createTransport();
         mCancellation = CancellationSignal.fromTransport(cancellation);
@@ -115,14 +115,14 @@
             Log.w(TAG, "Shutting down due to error: ", e);
             close();
         }
+        mCancellation = null;
     }
 
     @BinderThread
     @Override
     public ICancellationSignal requestImage(Rect requestRect) throws RemoteException {
-        Trace.beginSection("requestImage");
         checkActive();
-
+        cancelPendingAction();
         ICancellationSignal cancellation = CancellationSignal.createTransport();
         mCancellation = CancellationSignal.fromTransport(cancellation);
 
@@ -131,7 +131,6 @@
         // -> UiThread
         mUiThread.execute(() -> mLocal.onScrollCaptureImageRequest(
                 mSession, mCancellation, new Rect(requestRect), listener));
-        Trace.endSection();
         return cancellation;
     }
 
@@ -142,6 +141,8 @@
         } catch (RemoteException e) {
             Log.w(TAG, "Shutting down due to error: ", e);
             close();
+        } finally {
+            mCancellation = null;
         }
     }
 
@@ -149,7 +150,7 @@
     @Override
     public ICancellationSignal endCapture() throws RemoteException {
         checkActive();
-
+        cancelPendingAction();
         ICancellationSignal cancellation = CancellationSignal.createTransport();
         mCancellation = CancellationSignal.fromTransport(cancellation);
 
@@ -170,26 +171,32 @@
         } catch (RemoteException e) {
             Log.w(TAG, "Caught exception confirming capture end!", e);
         } finally {
+            mCancellation = null;
             close();
         }
     }
 
+    @Override
+    public void binderDied() {
+        Log.e(TAG, "Controlling process just died.");
+        close();
+    }
+
     @BinderThread
     @Override
     public void close() {
         if (mActive) {
-            if (mCancellation != null) {
-                Log.w(TAG, "close(): cancelling pending operation.");
-                mCancellation.cancel();
-                mCancellation = null;
-            }
             Log.w(TAG, "close(): capture session still active! Ending now.");
-            // -> UiThread
+            cancelPendingAction();
             final ScrollCaptureCallback callback = mLocal;
             mUiThread.execute(() -> callback.onScrollCaptureEnd(() -> { /* ignore */ }));
             mActive = false;
         }
+        if (mRemote != null) {
+            mRemote.asBinder().unlinkToDeath(this, 0);
+        }
         mActive = false;
+        mConnected = false;
         mSession = null;
         mRemote = null;
         mLocal = null;
@@ -197,6 +204,19 @@
         Reference.reachabilityFence(this);
     }
 
+    private void cancelPendingAction() {
+        if (mCancellation != null) {
+            Log.w(TAG, "cancelling pending operation.");
+            mCancellation.cancel();
+            mCancellation = null;
+        }
+    }
+
+    @VisibleForTesting
+    public boolean isConnected() {
+        return mConnected;
+    }
+
     @VisibleForTesting
     public boolean isActive() {
         return mActive;
@@ -236,7 +256,7 @@
 
         protected SafeCallback(CancellationSignal signal, Executor executor, T value) {
             mSignal = signal;
-            mValue = new AtomicReference<T>(value);
+            mValue = new AtomicReference<>(value);
             mExecutor = executor;
         }
 
@@ -257,7 +277,7 @@
 
         static <T> Consumer<T> create(CancellationSignal signal, Executor executor,
                 Consumer<T> target) {
-            return new ConsumerCallback<T>(signal, executor, target);
+            return new ConsumerCallback<>(signal, executor, target);
         }
     }
 
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 1a79e4c..30160c3 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -243,12 +243,83 @@
     private static native void nativeRemoveJankDataListener(long nativeListener);
     private static native long nativeCreateJankDataListenerWrapper(OnJankDataListener listener);
     private static native int nativeGetGPUContextPriority();
-    private static native void nativeSetTransformHint(long nativeObject, int transformHint);
+    private static native void nativeSetTransformHint(long nativeObject,
+            @SurfaceControl.BufferTransform int transformHint);
     private static native int nativeGetTransformHint(long nativeObject);
     private static native int nativeGetLayerId(long nativeObject);
     private static native void nativeAddTransactionCommittedListener(long nativeObject,
             TransactionCommittedListener listener);
 
+    /**
+     * Transforms that can be applied to buffers as they are displayed to a window.
+     *
+     * Supported transforms are any combination of horizontal mirror, vertical mirror, and
+     * clock-wise 90 degree rotation, in that order. Rotations of 180 and 270 degrees are made up
+     * of those basic transforms.
+     * Mirrors {@code ANativeWindowTransform} definitions.
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"BUFFER_TRANSFORM_"},
+            value = {BUFFER_TRANSFORM_IDENTITY, BUFFER_TRANSFORM_MIRROR_HORIZONTAL,
+                    BUFFER_TRANSFORM_MIRROR_VERTICAL, BUFFER_TRANSFORM_ROTATE_90,
+                    BUFFER_TRANSFORM_ROTATE_180, BUFFER_TRANSFORM_ROTATE_270,
+                    BUFFER_TRANSFORM_MIRROR_HORIZONTAL | BUFFER_TRANSFORM_ROTATE_90,
+                    BUFFER_TRANSFORM_MIRROR_VERTICAL | BUFFER_TRANSFORM_ROTATE_90})
+    public @interface BufferTransform {
+    }
+
+    /**
+     * Identity transform.
+     *
+     * These transforms that can be applied to buffers as they are displayed to a window.
+     * @see HardwareBuffer
+     *
+     * Supported transforms are any combination of horizontal mirror, vertical mirror, and
+     * clock-wise 90 degree rotation, in that order. Rotations of 180 and 270 degrees are
+     * made up of those basic transforms.
+     */
+    public static final int BUFFER_TRANSFORM_IDENTITY = 0x00;
+    /**
+     * Mirror horizontally. Can be combined with {@link #BUFFER_TRANSFORM_MIRROR_VERTICAL}
+     * and {@link #BUFFER_TRANSFORM_ROTATE_90}.
+     */
+    public static final int BUFFER_TRANSFORM_MIRROR_HORIZONTAL = 0x01;
+    /**
+     * Mirror vertically. Can be combined with {@link #BUFFER_TRANSFORM_MIRROR_HORIZONTAL}
+     * and {@link #BUFFER_TRANSFORM_ROTATE_90}.
+     */
+    public static final int BUFFER_TRANSFORM_MIRROR_VERTICAL = 0x02;
+    /**
+     * Rotate 90 degrees clock-wise. Can be combined with {@link
+     * #BUFFER_TRANSFORM_MIRROR_HORIZONTAL} and {@link #BUFFER_TRANSFORM_MIRROR_VERTICAL}.
+     */
+    public static final int BUFFER_TRANSFORM_ROTATE_90 = 0x04;
+    /**
+     * Rotate 180 degrees clock-wise. Cannot be combined with other transforms.
+     */
+    public static final int BUFFER_TRANSFORM_ROTATE_180 =
+            BUFFER_TRANSFORM_MIRROR_HORIZONTAL | BUFFER_TRANSFORM_MIRROR_VERTICAL;
+    /**
+     * Rotate 270 degrees clock-wise. Cannot be combined with other transforms.
+     */
+    public static final int BUFFER_TRANSFORM_ROTATE_270 =
+            BUFFER_TRANSFORM_ROTATE_180 | BUFFER_TRANSFORM_ROTATE_90;
+
+    /**
+     * @hide
+     */
+    public static @BufferTransform int rotationToBufferTransform(@Surface.Rotation int rotation) {
+        switch (rotation) {
+            case Surface.ROTATION_0: return BUFFER_TRANSFORM_IDENTITY;
+            case Surface.ROTATION_90: return BUFFER_TRANSFORM_ROTATE_90;
+            case Surface.ROTATION_180: return BUFFER_TRANSFORM_ROTATE_180;
+            case Surface.ROTATION_270: return BUFFER_TRANSFORM_ROTATE_270;
+        }
+        Log.e(TAG, "Trying to convert unknown rotation=" + rotation);
+        return BUFFER_TRANSFORM_IDENTITY;
+    }
+
     @Nullable
     @GuardedBy("mLock")
     private ArrayList<OnReparentListener> mReparentListeners;
@@ -3683,7 +3754,7 @@
     /**
      * @hide
      */
-    public int getTransformHint() {
+    public @SurfaceControl.BufferTransform int getTransformHint() {
         checkNotReleased();
         return nativeGetTransformHint(mNativeObject);
     }
@@ -3697,7 +3768,7 @@
      * with the same size.
      * @hide
      */
-    public void setTransformHint(@Surface.Rotation int transformHint) {
+    public void setTransformHint(@SurfaceControl.BufferTransform int transformHint) {
         nativeSetTransformHint(mNativeObject, transformHint);
     }
 
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 945758f..1c44f443 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -214,7 +214,7 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     final Rect mSurfaceFrame = new Rect();
     int mLastSurfaceWidth = -1, mLastSurfaceHeight = -1;
-    int mTransformHint = 0;
+    @SurfaceControl.BufferTransform int mTransformHint = 0;
 
     private boolean mGlobalListenersAdded;
     private boolean mAttachedToWindow;
@@ -1104,7 +1104,7 @@
             || mWindowSpaceTop != mLocation[1];
         final boolean layoutSizeChanged = getWidth() != mScreenRect.width()
             || getHeight() != mScreenRect.height();
-        final boolean hintChanged = (viewRoot.getSurfaceTransformHint() != mTransformHint)
+        final boolean hintChanged = (viewRoot.getBufferTransformHint() != mTransformHint)
                 && mRequestedVisible;
 
         if (creating || formatChanged || sizeChanged || visibleChanged ||
@@ -1130,7 +1130,7 @@
                 mSurfaceHeight = myHeight;
                 mFormat = mRequestedFormat;
                 mLastWindowVisibility = mWindowVisibility;
-                mTransformHint = viewRoot.getSurfaceTransformHint();
+                mTransformHint = viewRoot.getBufferTransformHint();
 
                 mScreenRect.left = mWindowSpaceLeft;
                 mScreenRect.top = mWindowSpaceTop;
@@ -1362,7 +1362,7 @@
         if (mBlastBufferQueue != null) {
             mBlastBufferQueue.destroy();
         }
-        mTransformHint = viewRoot.getSurfaceTransformHint();
+        mTransformHint = viewRoot.getBufferTransformHint();
         mBlastSurfaceControl.setTransformHint(mTransformHint);
         mBlastBufferQueue = new BLASTBufferQueue(name, mBlastSurfaceControl, mSurfaceWidth,
                 mSurfaceHeight, mFormat);
@@ -1889,6 +1889,24 @@
      * @param p The SurfacePackage to embed.
      */
     public void setChildSurfacePackage(@NonNull SurfaceControlViewHost.SurfacePackage p) {
+        setChildSurfacePackage(p, false /* applyTransactionOnDraw */);
+    }
+
+    /**
+     * Similar to setChildSurfacePackage, but using the BLAST queue so the transaction can be
+     * synchronized with the ViewRootImpl frame.
+     * @hide
+     */
+    public void setChildSurfacePackageOnDraw(
+            @NonNull SurfaceControlViewHost.SurfacePackage p) {
+        setChildSurfacePackage(p, true /* applyTransactionOnDraw */);
+    }
+
+    /**
+     * @param applyTransactionOnDraw Whether to apply transaction at onDraw or immediately.
+     */
+    private void setChildSurfacePackage(
+            @NonNull SurfaceControlViewHost.SurfacePackage p, boolean applyTransactionOnDraw) {
         final SurfaceControl lastSc = mSurfacePackage != null ?
                 mSurfacePackage.getSurfaceControl() : null;
         if (mSurfaceControl != null) {
@@ -1896,13 +1914,20 @@
                 mTmpTransaction.reparent(lastSc, null);
                 mSurfacePackage.release();
             }
-
             reparentSurfacePackage(mTmpTransaction, p);
-            mTmpTransaction.apply();
+            applyTransaction(applyTransactionOnDraw);
         }
         mSurfacePackage = p;
     }
 
+    private void applyTransaction(boolean applyTransactionOnDraw) {
+        if (applyTransactionOnDraw) {
+            getViewRootImpl().applyTransactionOnDraw(mTmpTransaction);
+        } else {
+            mTmpTransaction.apply();
+        }
+    }
+
     private void reparentSurfacePackage(SurfaceControl.Transaction t,
             SurfaceControlViewHost.SurfacePackage p) {
         final SurfaceControl sc = p.getSurfaceControl();
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 6420236..9030828 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -744,8 +744,9 @@
  * To enable touch filtering, call {@link #setFilterTouchesWhenObscured(boolean)} or set the
  * android:filterTouchesWhenObscured layout attribute to true.  When enabled, the framework
  * will discard touches that are received whenever the view's window is obscured by
- * another visible window.  As a result, the view will not receive touches whenever a
- * toast, dialog or other window appears above the view's window.
+ * another visible window at the touched location.  As a result, the view will not receive touches
+ * whenever the touch passed through a toast, dialog or other window that appears above the view's
+ * window.
  * </p><p>
  * For more fine-grained control over security, consider overriding the
  * {@link #onFilterTouchEventForSecurity(MotionEvent)} method to implement your own
@@ -12671,7 +12672,7 @@
 
     /**
      * Gets whether the framework should discard touches when the view's
-     * window is obscured by another visible window.
+     * window is obscured by another visible window at the touched location.
      * Refer to the {@link View} security documentation for more details.
      *
      * @return True if touch filtering is enabled.
@@ -12687,7 +12688,7 @@
 
     /**
      * Sets whether the framework should discard touches when the view's
-     * window is obscured by another visible window.
+     * window is obscured by another visible window at the touched location.
      * Refer to the {@link View} security documentation for more details.
      *
      * @param enabled True if touch filtering should be enabled.
@@ -27077,7 +27078,7 @@
 
         switch (event.mAction) {
             case DragEvent.ACTION_DRAG_STARTED: {
-                if (result && li.mOnDragListener != null) {
+                if (result && li != null && li.mOnDragListener != null) {
                     sendWindowContentChangedAccessibilityEvent(
                             AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
                 }
@@ -27091,7 +27092,8 @@
                 refreshDrawableState();
             } break;
             case DragEvent.ACTION_DROP: {
-                if (result && (li.mOnDragListener != null | li.mOnReceiveContentListener != null)) {
+                if (result && li != null && (li.mOnDragListener != null
+                        || li.mOnReceiveContentListener != null)) {
                     sendWindowContentChangedAccessibilityEvent(
                             AccessibilityEvent.CONTENT_CHANGE_TYPE_DRAG_DROPPED);
                 }
@@ -29520,12 +29522,6 @@
         boolean mHandlingPointerEvent;
 
         /**
-         * The offset of this view's window when it's on an embedded display that is re-parented
-         * to another window.
-         */
-        final Point mLocationInParentDisplay = new Point();
-
-        /**
          * The screen matrix of this view when it's on a {@link SurfaceControlViewHost} that is
          * embedded within a SurfaceView.
          */
diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java
index 73294b3..07d5fc5 100644
--- a/core/java/android/view/ViewDebug.java
+++ b/core/java/android/view/ViewDebug.java
@@ -1066,6 +1066,7 @@
      */
     @TestApi
     @Nullable
+    @UnsupportedAppUsage // Visible for Studio; least-worst option available
     public static AutoCloseable startRenderingCommandsCapture(View tree, Executor executor,
             Callable<OutputStream> callback) {
         final View.AttachInfo attachInfo = tree.mAttachInfo;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9af0a02..cddf9ee 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -312,9 +312,10 @@
     static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList<>();
     static boolean sFirstDrawComplete = false;
 
-    private ArrayList<OnSurfaceTransformHintChangedListener> mTransformHintListeners =
+    private ArrayList<OnBufferTransformHintChangedListener> mTransformHintListeners =
             new ArrayList<>();
-    private @Surface.Rotation int mPreviousTransformHint = Surface.ROTATION_0;
+    private @SurfaceControl.BufferTransform
+            int mPreviousTransformHint = SurfaceControl.BUFFER_TRANSFORM_IDENTITY;
     /**
      * Callback for notifying about global configuration changes.
      */
@@ -4685,13 +4686,6 @@
         }
     }
 
-    void updateLocationInParentDisplay(int x, int y) {
-        if (mAttachInfo != null
-                && !mAttachInfo.mLocationInParentDisplay.equals(x, y)) {
-            mAttachInfo.mLocationInParentDisplay.set(x, y);
-        }
-    }
-
     /**
      * Set the root-level system gesture exclusion rects. These are added to those provided by
      * the root's view hierarchy.
@@ -5203,7 +5197,6 @@
     private static final int MSG_INSETS_CHANGED = 30;
     private static final int MSG_INSETS_CONTROL_CHANGED = 31;
     private static final int MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED = 32;
-    private static final int MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED = 33;
     private static final int MSG_SHOW_INSETS = 34;
     private static final int MSG_HIDE_INSETS = 35;
     private static final int MSG_REQUEST_SCROLL_CAPTURE = 36;
@@ -5269,8 +5262,6 @@
                     return "MSG_INSETS_CONTROL_CHANGED";
                 case MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED:
                     return "MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED";
-                case MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED:
-                    return "MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED";
                 case MSG_SHOW_INSETS:
                     return "MSG_SHOW_INSETS";
                 case MSG_HIDE_INSETS:
@@ -5496,9 +5487,6 @@
                 case MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED: {
                     systemGestureExclusionChanged();
                 } break;
-                case MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED: {
-                    updateLocationInParentDisplay(msg.arg1, msg.arg2);
-                } break;
                 case MSG_REQUEST_SCROLL_CAPTURE:
                     handleScrollCaptureRequest((IScrollCaptureResponseListener) msg.obj);
                     break;
@@ -9030,17 +9018,6 @@
         mHandler.sendMessage(msg);
     }
 
-    /**
-     * Dispatch the offset changed.
-     *
-     * @param offset the offset of this view in the parent window.
-     */
-    public void dispatchLocationInParentDisplayChanged(Point offset) {
-        Message msg =
-                mHandler.obtainMessage(MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED, offset.x, offset.y);
-        mHandler.sendMessage(msg);
-    }
-
     public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
         synchronized (this) {
             mWindowFocusChanged = true;
@@ -9716,14 +9693,6 @@
         }
 
         @Override
-        public void locationInParentDisplayChanged(Point offset) {
-            final ViewRootImpl viewAncestor = mViewAncestor.get();
-            if (viewAncestor != null) {
-                viewAncestor.dispatchLocationInParentDisplayChanged(offset);
-            }
-        }
-
-        @Override
         public void insetsChanged(InsetsState insetsState, boolean willMove, boolean willResize) {
             final ViewRootImpl viewAncestor = mViewAncestor.get();
             if (viewAncestor != null) {
@@ -10494,38 +10463,38 @@
     }
 
     @Override
-    public @Surface.Rotation int getSurfaceTransformHint() {
+    public @SurfaceControl.BufferTransform int getBufferTransformHint() {
         return mSurfaceControl.getTransformHint();
     }
 
     @Override
-    public void addOnSurfaceTransformHintChangedListener(
-            OnSurfaceTransformHintChangedListener listener) {
+    public void addOnBufferTransformHintChangedListener(
+            OnBufferTransformHintChangedListener listener) {
         Objects.requireNonNull(listener);
         if (mTransformHintListeners.contains(listener)) {
             throw new IllegalArgumentException(
-                    "attempt to call addOnSurfaceTransformHintChangedListener() "
+                    "attempt to call addOnBufferTransformHintChangedListener() "
                             + "with a previously registered listener");
         }
         mTransformHintListeners.add(listener);
     }
 
     @Override
-    public void removeOnSurfaceTransformHintChangedListener(
-            OnSurfaceTransformHintChangedListener listener) {
+    public void removeOnBufferTransformHintChangedListener(
+            OnBufferTransformHintChangedListener listener) {
         Objects.requireNonNull(listener);
         mTransformHintListeners.remove(listener);
     }
 
-    private void dispatchTransformHintChanged(@Surface.Rotation int hint) {
+    private void dispatchTransformHintChanged(@SurfaceControl.BufferTransform int hint) {
         if (mTransformHintListeners.isEmpty()) {
             return;
         }
-        ArrayList<OnSurfaceTransformHintChangedListener> listeners =
-                (ArrayList<OnSurfaceTransformHintChangedListener>) mTransformHintListeners.clone();
+        ArrayList<OnBufferTransformHintChangedListener> listeners =
+                (ArrayList<OnBufferTransformHintChangedListener>) mTransformHintListeners.clone();
         for (int i = 0; i < listeners.size(); i++) {
-            OnSurfaceTransformHintChangedListener listener = listeners.get(i);
-            listener.onSurfaceTransformHintChanged(hint);
+            OnBufferTransformHintChangedListener listener = listeners.get(i);
+            listener.onBufferTransformHintChanged(hint);
         }
     }
 }
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index 5dd7c84..dd80416 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -351,25 +351,11 @@
             throw e.rethrowFromSystemServer();
         }
 
-        int size = possibleDisplayInfos.size();
-        DisplayInfo currentDisplayInfo;
-        WindowInsets windowInsets = null;
-        if (size > 0) {
-            currentDisplayInfo = possibleDisplayInfos.get(0);
-
-            final WindowManager.LayoutParams params =  new WindowManager.LayoutParams();
-            final boolean isScreenRound = (currentDisplayInfo.flags & Display.FLAG_ROUND) != 0;
-            // TODO(181127261) not computing insets correctly - need to have underlying
-            // frame reflect the faked orientation.
-            windowInsets = getWindowInsetsFromServerForDisplay(
-                    currentDisplayInfo.displayId, params,
-                    new Rect(0, 0, currentDisplayInfo.getNaturalWidth(),
-                            currentDisplayInfo.getNaturalHeight()), isScreenRound,
-                    WINDOWING_MODE_FULLSCREEN);
-        }
-
         Set<WindowMetrics> maxMetrics = new HashSet<>();
-        for (int i = 0; i < size; i++) {
+        WindowInsets windowInsets;
+        DisplayInfo currentDisplayInfo;
+        final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+        for (int i = 0; i < possibleDisplayInfos.size(); i++) {
             currentDisplayInfo = possibleDisplayInfos.get(i);
 
             // Calculate max bounds for this rotation and state.
@@ -377,7 +363,18 @@
                     currentDisplayInfo.logicalHeight);
 
             // Calculate insets for the rotated max bounds.
-            // TODO(181127261) calculate insets for each display rotation and state.
+            final boolean isScreenRound = (currentDisplayInfo.flags & Display.FLAG_ROUND) != 0;
+            // Initialize insets based upon display rotation. Note any window-provided insets
+            // will not be set.
+            windowInsets = getWindowInsetsFromServerForDisplay(
+                    currentDisplayInfo.displayId, params,
+                    new Rect(0, 0, currentDisplayInfo.getNaturalWidth(),
+                            currentDisplayInfo.getNaturalHeight()), isScreenRound,
+                    WINDOWING_MODE_FULLSCREEN);
+            // Set the hardware-provided insets.
+            windowInsets = new WindowInsets.Builder(windowInsets).setRoundedCorners(
+                    currentDisplayInfo.roundedCorners)
+                    .setDisplayCutout(currentDisplayInfo.displayCutout).build();
 
             maxMetrics.add(new WindowMetrics(maxBounds, windowInsets));
         }
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index ffb7efa..d699194d 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -444,11 +444,6 @@
     }
 
     @Override
-    public void updateDisplayContentLocation(android.view.IWindow window, int x, int y,
-            int displayId) {
-    }
-
-    @Override
     public void updateTapExcludeRegion(android.view.IWindow window,
             android.graphics.Region region) {
     }
diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java
index f58a07f..3db7136 100644
--- a/core/java/android/view/translation/UiTranslationController.java
+++ b/core/java/android/view/translation/UiTranslationController.java
@@ -428,15 +428,19 @@
                     continue;
                 }
                 mActivity.runOnUiThread(() -> {
+                    ViewTranslationCallback callback = view.getViewTranslationCallback();
                     if (view.getViewTranslationResponse() != null
                             && view.getViewTranslationResponse().equals(response)) {
-                        if (DEBUG) {
-                            Log.d(TAG, "Duplicate ViewTranslationResponse for " + autofillId
-                                    + ". Ignoring.");
+                        if (callback instanceof TextViewTranslationCallback) {
+                            if (((TextViewTranslationCallback) callback).isShowingTranslation()) {
+                                if (DEBUG) {
+                                    Log.d(TAG, "Duplicate ViewTranslationResponse for " + autofillId
+                                            + ". Ignoring.");
+                                }
+                                return;
+                            }
                         }
-                        return;
                     }
-                    ViewTranslationCallback callback = view.getViewTranslationCallback();
                     if (callback == null) {
                         if (view instanceof TextView) {
                             // developer doesn't provide their override, we set the default TextView
diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java
index d661bc6..2c61280 100644
--- a/core/java/android/widget/EditText.java
+++ b/core/java/android/widget/EditText.java
@@ -110,7 +110,7 @@
             return null;
         }
         if (text instanceof Editable) {
-            return (Editable) super.getText();
+            return (Editable) text;
         }
         super.setText(text, BufferType.EDITABLE);
         return (Editable) super.getText();
diff --git a/core/java/android/widget/TextViewTranslationCallback.java b/core/java/android/widget/TextViewTranslationCallback.java
index 152405b..4a78f3e 100644
--- a/core/java/android/widget/TextViewTranslationCallback.java
+++ b/core/java/android/widget/TextViewTranslationCallback.java
@@ -64,6 +64,12 @@
      */
     @Override
     public boolean onShowTranslation(@NonNull View view) {
+        if (mIsShowingTranslation) {
+            if (DEBUG) {
+                Log.d(TAG, view + " is already showing translated text.");
+            }
+            return false;
+        }
         ViewTranslationResponse response = view.getViewTranslationResponse();
         if (response == null) {
             Log.e(TAG, "onShowTranslation() shouldn't be called before "
@@ -152,7 +158,7 @@
         return true;
     }
 
-    boolean isShowingTranslation() {
+    public boolean isShowingTranslation() {
         return mIsShowingTranslation;
     }
 
diff --git a/core/java/android/window/ITaskOrganizer.aidl b/core/java/android/window/ITaskOrganizer.aidl
index 69bc1b5..fd86769 100644
--- a/core/java/android/window/ITaskOrganizer.aidl
+++ b/core/java/android/window/ITaskOrganizer.aidl
@@ -20,6 +20,7 @@
 import android.app.ActivityManager;
 import android.graphics.Rect;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
 import android.window.WindowContainerToken;
 
 /**
@@ -39,12 +40,9 @@
 
     /**
      * Called when the Task want to remove the starting window.
-     * @param leash A persistent leash for the top window in this task.
-     * @param frame Window frame of the top window.
-     * @param playRevealAnimation Play vanish animation.
+     * @param removalInfo The information used to remove the starting window.
      */
-    void removeStartingWindow(int taskId, in SurfaceControl leash, in Rect frame,
-            in boolean playRevealAnimation);
+    void removeStartingWindow(in StartingWindowRemovalInfo removalInfo);
 
     /**
      * Called when the Task want to copy the splash screen.
diff --git a/core/java/android/window/SplashScreen.java b/core/java/android/window/SplashScreen.java
index 3e00758..3354a6c 100644
--- a/core/java/android/window/SplashScreen.java
+++ b/core/java/android/window/SplashScreen.java
@@ -241,7 +241,6 @@
 
         public void handOverSplashScreenView(@NonNull IBinder token,
                 @NonNull SplashScreenView splashScreenView) {
-            transferSurface(splashScreenView);
             dispatchOnExitAnimation(token, splashScreenView);
         }
 
@@ -265,9 +264,5 @@
                 return impl != null && impl.mExitAnimationListener != null;
             }
         }
-
-        private void transferSurface(@NonNull SplashScreenView splashScreenView) {
-            splashScreenView.transferSurface();
-        }
     }
 }
diff --git a/core/java/android/window/SplashScreenView.java b/core/java/android/window/SplashScreenView.java
index f14294e..f748d4b 100644
--- a/core/java/android/window/SplashScreenView.java
+++ b/core/java/android/window/SplashScreenView.java
@@ -464,7 +464,10 @@
     }
 
 
-    void transferSurface() {
+    /**
+     * @hide
+     */
+    public void syncTransferSurfaceOnDraw() {
         if (mSurfacePackage == null) {
             return;
         }
@@ -474,8 +477,8 @@
                             String.format("SurfacePackage'surface reparented to %s", parent)));
             Log.d(TAG, "Transferring surface " + mSurfaceView.toString());
         }
-        mSurfaceView.setChildSurfacePackage(mSurfacePackage);
 
+        mSurfaceView.setChildSurfacePackageOnDraw(mSurfacePackage);
     }
 
     void initIconAnimation(Drawable iconDrawable, long duration) {
@@ -533,10 +536,6 @@
             restoreSystemUIColors();
             mWindow = null;
         }
-        if (mHostActivity != null) {
-            mHostActivity.setSplashScreenView(null);
-            mHostActivity = null;
-        }
         mHasRemoved = true;
     }
 
@@ -582,7 +581,6 @@
      * @hide
      */
     public void attachHostActivityAndSetSystemUIColors(Activity activity, Window window) {
-        activity.setSplashScreenView(this);
         mHostActivity = activity;
         mWindow = window;
         final WindowManager.LayoutParams attr = window.getAttributes();
diff --git a/core/java/android/window/StartingWindowInfo.java b/core/java/android/window/StartingWindowInfo.java
index 10d21a0..aec910b 100644
--- a/core/java/android/window/StartingWindowInfo.java
+++ b/core/java/android/window/StartingWindowInfo.java
@@ -19,7 +19,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.TestApi;
 import android.app.ActivityManager;
 import android.app.TaskInfo;
 import android.content.pm.ActivityInfo;
@@ -34,7 +33,6 @@
  * start in the system.
  * @hide
  */
-@TestApi
 public final class StartingWindowInfo implements Parcelable {
     /**
      * Prefer nothing or not care the type of starting window.
@@ -138,6 +136,11 @@
     /** @hide */
     public static final int TYPE_PARAMETER_USE_EMPTY_SPLASH_SCREEN = 0x00000020;
     /**
+     * The parameter which indicates if the activity has finished drawing.
+     * @hide
+     */
+    public static final int TYPE_PARAMETER_ACTIVITY_DRAWN = 0x00000040;
+    /**
      * Application is allowed to use the legacy splash screen
      * @hide
      */
diff --git a/core/java/android/window/StartingWindowRemovalInfo.aidl b/core/java/android/window/StartingWindowRemovalInfo.aidl
new file mode 100644
index 0000000..8e4ac04
--- /dev/null
+++ b/core/java/android/window/StartingWindowRemovalInfo.aidl
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2021, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+/** @hide */
+parcelable StartingWindowRemovalInfo;
\ No newline at end of file
diff --git a/core/java/android/window/StartingWindowRemovalInfo.java b/core/java/android/window/StartingWindowRemovalInfo.java
new file mode 100644
index 0000000..573db0d
--- /dev/null
+++ b/core/java/android/window/StartingWindowRemovalInfo.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.window;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.SurfaceControl;
+
+/**
+ * Information when removing a starting window of a particular task.
+ * @hide
+ */
+public final class StartingWindowRemovalInfo implements Parcelable {
+
+    /**
+     * The identifier of a task.
+     * @hide
+     */
+    public int taskId;
+
+    /**
+     * The animation container layer of the top activity.
+     * @hide
+     */
+    @Nullable
+    public SurfaceControl windowAnimationLeash;
+
+    /**
+     * The main window frame for the window of the top activity.
+     * @hide
+     */
+    @Nullable
+    public Rect mainFrame;
+
+    /**
+     * Whether need to play reveal animation.
+     * @hide
+     */
+    public boolean playRevealAnimation;
+
+    /**
+     * Whether need to defer removing the starting window for IME.
+     * @hide
+     */
+    public boolean deferRemoveForIme;
+
+    public StartingWindowRemovalInfo() {
+
+    }
+
+    private StartingWindowRemovalInfo(@NonNull Parcel source) {
+        readFromParcel(source);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    void readFromParcel(@NonNull Parcel source) {
+        taskId = source.readInt();
+        windowAnimationLeash = source.readTypedObject(SurfaceControl.CREATOR);
+        mainFrame = source.readTypedObject(Rect.CREATOR);
+        playRevealAnimation = source.readBoolean();
+        deferRemoveForIme = source.readBoolean();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(taskId);
+        dest.writeTypedObject(windowAnimationLeash, flags);
+        dest.writeTypedObject(mainFrame, flags);
+        dest.writeBoolean(playRevealAnimation);
+        dest.writeBoolean(deferRemoveForIme);
+    }
+
+    @Override
+    public String toString() {
+        return "StartingWindowRemovalInfo{taskId=" + taskId
+                + " frame=" + mainFrame
+                + " playRevealAnimation=" + playRevealAnimation
+                + " deferRemoveForIme=" + deferRemoveForIme + "}";
+    }
+
+    public static final @android.annotation.NonNull Creator<StartingWindowRemovalInfo> CREATOR =
+            new Creator<StartingWindowRemovalInfo>() {
+                public StartingWindowRemovalInfo createFromParcel(@NonNull Parcel source) {
+                    return new StartingWindowRemovalInfo(source);
+                }
+                public StartingWindowRemovalInfo[] newArray(int size) {
+                    return new StartingWindowRemovalInfo[size];
+                }
+            };
+}
diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java
index 845c13d..27c7d315 100644
--- a/core/java/android/window/TaskOrganizer.java
+++ b/core/java/android/window/TaskOrganizer.java
@@ -24,7 +24,6 @@
 import android.annotation.SuppressLint;
 import android.annotation.TestApi;
 import android.app.ActivityManager;
-import android.graphics.Rect;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.view.SurfaceControl;
@@ -94,6 +93,7 @@
      * @param info The information about the Task that's available
      * @param appToken Token of the application being started.
      *        context to for resources
+     * @hide
      */
     @BinderThread
     public void addStartingWindow(@NonNull StartingWindowInfo info,
@@ -101,14 +101,11 @@
 
     /**
      * Called when the Task want to remove the starting window.
-     * @param leash A persistent leash for the top window in this task. Release it once exit
-     *              animation has finished.
-     * @param frame Window frame of the top window.
-     * @param playRevealAnimation Play vanish animation.
+     * @param removalInfo The information used to remove the starting window.
+     * @hide
      */
     @BinderThread
-    public void removeStartingWindow(int taskId, @Nullable SurfaceControl leash,
-            @Nullable Rect frame, boolean playRevealAnimation) {}
+    public void removeStartingWindow(@NonNull StartingWindowRemovalInfo removalInfo) {}
 
     /**
      * Called when the Task want to copy the splash screen.
@@ -257,10 +254,8 @@
         }
 
         @Override
-        public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-                boolean playRevealAnimation) {
-            mExecutor.execute(() -> TaskOrganizer.this.removeStartingWindow(taskId, leash, frame,
-                    playRevealAnimation));
+        public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
+            mExecutor.execute(() -> TaskOrganizer.this.removeStartingWindow(removalInfo));
         }
 
         @Override
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 00bc202..7a960c6 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -295,6 +295,36 @@
     }
 
     /**
+     * Reparent's all children tasks or the top task of {@param currentParent} in the specified
+     * {@param windowingMode} and {@param activityType} to {@param newParent} in their current
+     * z-order.
+     *
+     * @param currentParent of the tasks to perform the operation no.
+     *                      {@code null} will perform the operation on the display.
+     * @param newParent for the tasks. {@code null} will perform the operation on the display.
+     * @param windowingModes of the tasks to reparent.
+     * @param activityTypes of the tasks to reparent.
+     * @param onTop When {@code true}, the child goes to the top of parent; otherwise it goes to
+     *              the bottom.
+     * @param reparentTopOnly When {@code true}, only reparent the top task which fit windowingModes
+     *                        and activityTypes.
+     * @hide
+     */
+    @NonNull
+    public WindowContainerTransaction reparentTasks(@Nullable WindowContainerToken currentParent,
+            @Nullable WindowContainerToken newParent, @Nullable int[] windowingModes,
+            @Nullable int[] activityTypes, boolean onTop, boolean reparentTopOnly) {
+        mHierarchyOps.add(HierarchyOp.createForChildrenTasksReparent(
+                currentParent != null ? currentParent.asBinder() : null,
+                newParent != null ? newParent.asBinder() : null,
+                windowingModes,
+                activityTypes,
+                onTop,
+                reparentTopOnly));
+        return this;
+    }
+
+    /**
      * Reparent's all children tasks of {@param currentParent} in the specified
      * {@param windowingMode} and {@param activityType} to {@param newParent} in their current
      * z-order.
@@ -311,13 +341,8 @@
     public WindowContainerTransaction reparentTasks(@Nullable WindowContainerToken currentParent,
             @Nullable WindowContainerToken newParent, @Nullable int[] windowingModes,
             @Nullable int[] activityTypes, boolean onTop) {
-        mHierarchyOps.add(HierarchyOp.createForChildrenTasksReparent(
-                currentParent != null ? currentParent.asBinder() : null,
-                newParent != null ? newParent.asBinder() : null,
-                windowingModes,
-                activityTypes,
-                onTop));
-        return this;
+        return reparentTasks(currentParent, newParent, windowingModes, activityTypes, onTop,
+                false /* reparentTopOnly */);
     }
 
     /**
@@ -948,6 +973,8 @@
         // Moves/reparents to top of parent when {@code true}, otherwise moves/reparents to bottom.
         private boolean mToTop;
 
+        private boolean mReparentTopOnly;
+
         @Nullable
         private int[]  mWindowingModes;
 
@@ -985,13 +1012,15 @@
         }
 
         public static HierarchyOp createForChildrenTasksReparent(IBinder currentParent,
-                IBinder newParent, int[] windowingModes, int[] activityTypes, boolean onTop) {
+                IBinder newParent, int[] windowingModes, int[] activityTypes, boolean onTop,
+                boolean reparentTopOnly) {
             return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT)
                     .setContainer(currentParent)
                     .setReparentContainer(newParent)
                     .setWindowingModes(windowingModes)
                     .setActivityTypes(activityTypes)
                     .setToTop(onTop)
+                    .setReparentTopOnly(reparentTopOnly)
                     .build();
         }
 
@@ -1040,6 +1069,7 @@
             mContainer = copy.mContainer;
             mReparent = copy.mReparent;
             mToTop = copy.mToTop;
+            mReparentTopOnly = copy.mReparentTopOnly;
             mWindowingModes = copy.mWindowingModes;
             mActivityTypes = copy.mActivityTypes;
             mLaunchOptions = copy.mLaunchOptions;
@@ -1053,6 +1083,7 @@
             mContainer = in.readStrongBinder();
             mReparent = in.readStrongBinder();
             mToTop = in.readBoolean();
+            mReparentTopOnly = in.readBoolean();
             mWindowingModes = in.createIntArray();
             mActivityTypes = in.createIntArray();
             mLaunchOptions = in.readBundle();
@@ -1093,6 +1124,10 @@
             return mToTop;
         }
 
+        public boolean getReparentTopOnly() {
+            return mReparentTopOnly;
+        }
+
         public int[] getWindowingModes() {
             return mWindowingModes;
         }
@@ -1126,12 +1161,13 @@
             switch (mType) {
                 case HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT:
                     return "{ChildrenTasksReparent: from=" + mContainer + " to=" + mReparent
-                            + " mToTop=" + mToTop + " mWindowingMode=" + mWindowingModes
-                            + " mActivityType=" + mActivityTypes + "}";
+                            + " mToTop=" + mToTop + " mReparentTopOnly=" + mReparentTopOnly
+                            + " mWindowingMode=" + Arrays.toString(mWindowingModes)
+                            + " mActivityType=" + Arrays.toString(mActivityTypes) + "}";
                 case HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT:
                     return "{SetLaunchRoot: container=" + mContainer
-                            + " mWindowingMode=" + mWindowingModes
-                            + " mActivityType=" + mActivityTypes + "}";
+                            + " mWindowingMode=" + Arrays.toString(mWindowingModes)
+                            + " mActivityType=" + Arrays.toString(mActivityTypes) + "}";
                 case HIERARCHY_OP_TYPE_REPARENT:
                     return "{reparent: " + mContainer + " to " + (mToTop ? "top of " : "bottom of ")
                             + mReparent + "}";
@@ -1163,8 +1199,9 @@
                             + " adjacentContainer=" + mReparent + "}";
                 default:
                     return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent
-                            + " mToTop=" + mToTop + " mWindowingMode=" + mWindowingModes
-                            + " mActivityType=" + mActivityTypes + "}";
+                            + " mToTop=" + mToTop
+                            + " mWindowingMode=" + Arrays.toString(mWindowingModes)
+                            + " mActivityType=" + Arrays.toString(mActivityTypes) + "}";
             }
         }
 
@@ -1174,6 +1211,7 @@
             dest.writeStrongBinder(mContainer);
             dest.writeStrongBinder(mReparent);
             dest.writeBoolean(mToTop);
+            dest.writeBoolean(mReparentTopOnly);
             dest.writeIntArray(mWindowingModes);
             dest.writeIntArray(mActivityTypes);
             dest.writeBundle(mLaunchOptions);
@@ -1211,6 +1249,8 @@
 
             private boolean mToTop;
 
+            private boolean mReparentTopOnly;
+
             @Nullable
             private int[]  mWindowingModes;
 
@@ -1248,6 +1288,11 @@
                 return this;
             }
 
+            Builder setReparentTopOnly(boolean reparentTopOnly) {
+                mReparentTopOnly = reparentTopOnly;
+                return this;
+            }
+
             Builder setWindowingModes(@Nullable int[] windowingModes) {
                 mWindowingModes = windowingModes;
                 return this;
@@ -1290,6 +1335,7 @@
                         ? Arrays.copyOf(mActivityTypes, mActivityTypes.length)
                         : null;
                 hierarchyOp.mToTop = mToTop;
+                hierarchyOp.mReparentTopOnly = mReparentTopOnly;
                 hierarchyOp.mLaunchOptions = mLaunchOptions;
                 hierarchyOp.mActivityIntent = mActivityIntent;
                 hierarchyOp.mPendingIntent = mPendingIntent;
diff --git a/core/java/com/android/internal/app/LaunchAfterAuthenticationActivity.java b/core/java/com/android/internal/app/LaunchAfterAuthenticationActivity.java
new file mode 100644
index 0000000..20a025a
--- /dev/null
+++ b/core/java/com/android/internal/app/LaunchAfterAuthenticationActivity.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.util.Slog;
+
+import java.util.Objects;
+
+/**
+ * Activity used to intercept lock screen intents and show the bouncer before launching the
+ * original intent.
+ */
+public class LaunchAfterAuthenticationActivity extends Activity {
+    private static final String TAG = LaunchAfterAuthenticationActivity.class.getSimpleName();
+    private static final String EXTRA_ON_SUCCESS_INTENT =
+            "com.android.internal.app.extra.ON_SUCCESS_INTENT";
+
+    /**
+     * Builds the intent used to launch this activity.
+     *
+     * @param onSuccessIntent The intent to launch after the user has authenticated.
+     */
+    public static Intent createLaunchAfterAuthenticationIntent(IntentSender onSuccessIntent) {
+        return new Intent()
+                .setClassName(/* packageName= */"android",
+                        LaunchAfterAuthenticationActivity.class.getName())
+                .putExtra(EXTRA_ON_SUCCESS_INTENT, onSuccessIntent)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                        | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+    }
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        final IntentSender onSuccessIntent = getIntent().getParcelableExtra(
+                EXTRA_ON_SUCCESS_INTENT);
+        requestDismissKeyguardIfNeeded(onSuccessIntent);
+    }
+
+    private void requestDismissKeyguardIfNeeded(IntentSender onSuccessIntent) {
+        final KeyguardManager km = Objects.requireNonNull(getSystemService(KeyguardManager.class));
+        if (km.isKeyguardLocked()) {
+            km.requestDismissKeyguard(this,
+                    new KeyguardManager.KeyguardDismissCallback() {
+                        @Override
+                        public void onDismissCancelled() {
+                            LaunchAfterAuthenticationActivity.this.finish();
+                        }
+
+                        @Override
+                        public void onDismissSucceeded() {
+                            if (onSuccessIntent != null) {
+                                onUnlocked(onSuccessIntent);
+                            }
+                            LaunchAfterAuthenticationActivity.this.finish();
+                        }
+
+                        @Override
+                        public void onDismissError() {
+                            Slog.e(TAG, "Error while dismissing keyguard.");
+                            LaunchAfterAuthenticationActivity.this.finish();
+                        }
+                    });
+        } else {
+            finish();
+        }
+    }
+
+    private void onUnlocked(@NonNull IntentSender targetIntent) {
+        try {
+            targetIntent.sendIntent(
+                    /* context= */ this,
+                    /* code= */ 0,
+                    /* intent= */null,
+                    /* onFinished= */ null,
+                    /* handler= */ null);
+        } catch (IntentSender.SendIntentException e) {
+            Slog.e(TAG, "Error while sending original intent", e);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 91b4b7f..9af4f91 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -314,6 +314,15 @@
         return intent;
     }
 
+    /**
+     * Call {@link Activity#onCreate} without initializing anything further. This should
+     * only be used when the activity is about to be immediately finished to avoid wasting
+     * initializing steps and leaking resources.
+     */
+    protected void super_onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         // Use a specialized prompt when we're handling the 'Home' app startActivity()
@@ -892,7 +901,8 @@
         if (!isChangingConfigurations() && mPickOptionRequest != null) {
             mPickOptionRequest.cancel();
         }
-        if (mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
+        if (mMultiProfilePagerAdapter != null
+                && mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
             mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
         }
     }
diff --git a/core/java/com/android/internal/inputmethod/InputMethodDebug.java b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
index a00b993..bf094db 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodDebug.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodDebug.java
@@ -236,6 +236,8 @@
                 return "HIDE_TOGGLE_SOFT_INPUT";
             case SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API:
                 return "SHOW_SOFT_INPUT_BY_INSETS_API";
+            case SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE:
+                return "HIDE_DISPLAY_IME_POLICY_HIDE";
             default:
                 return "Unknown=" + reason;
         }
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
index 049f952..f8c6640 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperationsRegistry.java
@@ -74,12 +74,7 @@
             if (sRegistry == null) {
                 sRegistry = new WeakHashMap<>();
             }
-            final WeakReference<InputMethodPrivilegedOperations> previousOps =
-                    sRegistry.put(token, new WeakReference<>(ops));
-            if (previousOps != null) {
-                throw new IllegalStateException(previousOps.get() + " is already registered for "
-                        + " this token=" + token + " newOps=" + ops);
-            }
+            sRegistry.put(token, new WeakReference<>(ops));
         }
     }
 
@@ -132,21 +127,4 @@
             }
         }
     }
-
-    /**
-     * Check the given IME token registration status.
-     *
-     * @param token IME token
-     * @return {@code true} when the IME token has already registered
-     *         {@link InputMethodPrivilegedOperations}, {@code false} otherwise.
-     */
-    @AnyThread
-    public static boolean isRegistered(IBinder token) {
-        synchronized (sLock) {
-            if (sRegistry == null) {
-                return false;
-            }
-            return sRegistry.containsKey(token);
-        }
-    }
 }
diff --git a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
index e3713a3..9e57762 100644
--- a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
+++ b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java
@@ -19,6 +19,7 @@
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.annotation.IntDef;
+import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams;
 
 import java.lang.annotation.Retention;
@@ -53,7 +54,8 @@
         SoftInputShowHideReason.SHOW_RESTORE_IME_VISIBILITY,
         SoftInputShowHideReason.SHOW_TOGGLE_SOFT_INPUT,
         SoftInputShowHideReason.HIDE_TOGGLE_SOFT_INPUT,
-        SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API})
+        SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API,
+        SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE})
 public @interface SoftInputShowHideReason {
     /** Show soft input by {@link android.view.inputmethod.InputMethodManager#showSoftInput}. */
     int SHOW_SOFT_INPUT = 0;
@@ -195,4 +197,10 @@
      * {@link android.view.InsetsController#show(int)};
      */
     int SHOW_SOFT_INPUT_BY_INSETS_API = 25;
+
+    /**
+     * Hide soft input if Ime policy has been set to {@link WindowManager#DISPLAY_IME_POLICY_HIDE}.
+     * See also {@code InputMethodManagerService#mImeHiddenByDisplayPolicy}.
+     */
+    int HIDE_DISPLAY_IME_POLICY_HIDE = 26;
 }
diff --git a/core/java/com/android/internal/os/AmbientDisplayPowerCalculator.java b/core/java/com/android/internal/os/AmbientDisplayPowerCalculator.java
index 0307268..93baa19 100644
--- a/core/java/com/android/internal/os/AmbientDisplayPowerCalculator.java
+++ b/core/java/com/android/internal/os/AmbientDisplayPowerCalculator.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.os;
 
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_AMBIENT;
+
 import android.os.BatteryConsumer;
 import android.os.BatteryStats;
 import android.os.BatteryUsageStats;
@@ -32,8 +34,9 @@
     private final UsageBasedPowerEstimator mPowerEstimator;
 
     public AmbientDisplayPowerCalculator(PowerProfile powerProfile) {
+        // TODO(b/200239964): update to support multidisplay.
         mPowerEstimator = new UsageBasedPowerEstimator(
-                powerProfile.getAveragePower(PowerProfile.POWER_AMBIENT_DISPLAY));
+                powerProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, 0));
     }
 
     /**
diff --git a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
index 1871ac5..c5f1848 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
@@ -21,8 +21,7 @@
 import android.os.BatteryStats;
 import android.os.Parcel;
 import android.util.Slog;
-
-import java.util.List;
+import android.util.SparseArray;
 
 /**
  * An iterator for {@link BatteryStats.HistoryItem}'s.
@@ -33,23 +32,11 @@
     private final BatteryStatsHistory mBatteryStatsHistory;
     private final BatteryStats.HistoryStepDetails mReadHistoryStepDetails =
             new BatteryStats.HistoryStepDetails();
-    private final String[] mReadHistoryStrings;
-    private final int[] mReadHistoryUids;
+    private final SparseArray<BatteryStats.HistoryTag> mHistoryTags = new SparseArray<>();
 
-    public BatteryStatsHistoryIterator(@NonNull BatteryStatsHistory history,
-            @NonNull List<BatteryStats.HistoryTag> historyTagPool) {
+    public BatteryStatsHistoryIterator(@NonNull BatteryStatsHistory history) {
         mBatteryStatsHistory = history;
-
         mBatteryStatsHistory.startIteratingHistory();
-
-        mReadHistoryStrings = new String[historyTagPool.size()];
-        mReadHistoryUids = new int[historyTagPool.size()];
-        for (int i = historyTagPool.size() - 1; i >= 0; i--) {
-            BatteryStats.HistoryTag tag = historyTagPool.get(i);
-            final int idx = tag.poolIdx;
-            mReadHistoryStrings[idx] = tag.string;
-            mReadHistoryUids[idx] = tag.uid;
-        }
     }
 
     /**
@@ -161,26 +148,16 @@
         }
 
         if ((firstToken & BatteryStatsImpl.DELTA_WAKELOCK_FLAG) != 0) {
-            int indexes = src.readInt();
-            int wakeLockIndex = indexes & 0xffff;
-            int wakeReasonIndex = (indexes >> 16) & 0xffff;
-            if (wakeLockIndex != 0xffff) {
+            final int indexes = src.readInt();
+            final int wakeLockIndex = indexes & 0xffff;
+            final int wakeReasonIndex = (indexes >> 16) & 0xffff;
+            if (readHistoryTag(src, wakeLockIndex, cur.localWakelockTag)) {
                 cur.wakelockTag = cur.localWakelockTag;
-                readHistoryTag(wakeLockIndex, cur.wakelockTag);
-                if (DEBUG) {
-                    Slog.i(TAG, "READ DELTA: wakelockTag=#" + cur.wakelockTag.poolIdx
-                            + " " + cur.wakelockTag.uid + ":" + cur.wakelockTag.string);
-                }
             } else {
                 cur.wakelockTag = null;
             }
-            if (wakeReasonIndex != 0xffff) {
+            if (readHistoryTag(src, wakeReasonIndex, cur.localWakeReasonTag)) {
                 cur.wakeReasonTag = cur.localWakeReasonTag;
-                readHistoryTag(wakeReasonIndex, cur.wakeReasonTag);
-                if (DEBUG) {
-                    Slog.i(TAG, "READ DELTA: wakeReasonTag=#" + cur.wakeReasonTag.poolIdx
-                            + " " + cur.wakeReasonTag.uid + ":" + cur.wakeReasonTag.string);
-                }
             } else {
                 cur.wakeReasonTag = null;
             }
@@ -195,7 +172,11 @@
             final int codeAndIndex = src.readInt();
             cur.eventCode = (codeAndIndex & 0xffff);
             final int index = ((codeAndIndex >> 16) & 0xffff);
-            readHistoryTag(index, cur.eventTag);
+            if (readHistoryTag(src, index, cur.localEventTag)) {
+                cur.eventTag = cur.localEventTag;
+            } else {
+                cur.eventTag = null;
+            }
             cur.numReadInts += 1;
             if (DEBUG) {
                 Slog.i(TAG, "READ DELTA: event=" + cur.eventCode + " tag=#"
@@ -220,40 +201,29 @@
         cur.wifiRailChargeMah = src.readDouble();
     }
 
-    int getHistoryStringPoolSize() {
-        return mReadHistoryStrings.length;
-    }
-
-    int getHistoryStringPoolBytes() {
-        int totalChars = 0;
-        for (int i = mReadHistoryStrings.length - 1; i >= 0; i--) {
-            if (mReadHistoryStrings[i] != null) {
-                totalChars += mReadHistoryStrings[i].length() + 1;
-            }
+    private boolean readHistoryTag(Parcel src, int index, BatteryStats.HistoryTag outTag) {
+        if (index == 0xffff) {
+            return false;
         }
 
-        // Each entry is a fixed 12 bytes: 4 for index, 4 for uid, 4 for string size
-        // Each string character is 2 bytes.
-        return (mReadHistoryStrings.length * 12) + (totalChars * 2);
-    }
+        if ((index & BatteryStatsImpl.TAG_FIRST_OCCURRENCE_FLAG) != 0) {
+            BatteryStats.HistoryTag tag = new BatteryStats.HistoryTag();
+            tag.readFromParcel(src);
+            tag.poolIdx = index & ~BatteryStatsImpl.TAG_FIRST_OCCURRENCE_FLAG;
+            mHistoryTags.put(tag.poolIdx, tag);
 
-    String getHistoryTagPoolString(int index) {
-        return mReadHistoryStrings[index];
-    }
-
-    int getHistoryTagPoolUid(int index) {
-        return mReadHistoryUids[index];
-    }
-
-    private void readHistoryTag(int index, BatteryStats.HistoryTag tag) {
-        if (index < mReadHistoryStrings.length) {
-            tag.string = mReadHistoryStrings[index];
-            tag.uid = mReadHistoryUids[index];
+            outTag.setTo(tag);
         } else {
-            tag.string = null;
-            tag.uid = 0;
+            BatteryStats.HistoryTag historyTag = mHistoryTags.get(index);
+            if (historyTag != null) {
+                outTag.setTo(historyTag);
+            } else {
+                outTag.string = null;
+                outTag.uid = 0;
+            }
+            outTag.poolIdx = index;
         }
-        tag.poolIdx = index;
+        return true;
     }
 
     private static void readBatteryLevelInt(int batteryLevelInt, BatteryStats.HistoryItem out) {
diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 94a1efe..3aa63a0 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -160,7 +160,7 @@
     private static final int MAGIC = 0xBA757475; // 'BATSTATS'
 
     // Current on-disk Parcel version
-    static final int VERSION = 201;
+    static final int VERSION = 202;
 
     // The maximum number of names wakelocks we will keep track of
     // per uid; once the limit is reached, we batch the remaining wakelocks
@@ -678,7 +678,7 @@
          * Schedule a sync because of a screen state change.
          */
         Future<?> scheduleSyncDueToScreenStateChange(int flags, boolean onBattery,
-                boolean onBatteryScreenOff, int screenState);
+                boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates);
         Future<?> scheduleCpuSyncDueToWakelockChange(long delayMillis);
         void cancelCpuSyncDueToWakelockChange();
         Future<?> scheduleSyncDueToBatteryLevelChange(long delayMillis);
@@ -757,7 +757,11 @@
     protected boolean mRecordingHistory = false;
     int mNumHistoryItems;
 
+    private static final int HISTORY_TAG_INDEX_LIMIT = 0x7ffe;
+    private static final int MAX_HISTORY_TAG_STRING_LENGTH = 256;
+
     final HashMap<HistoryTag, Integer> mHistoryTagPool = new HashMap<>();
+    private SparseArray<HistoryTag> mHistoryTags;
     final Parcel mHistoryBuffer = Parcel.obtain();
     final HistoryItem mHistoryLastWritten = new HistoryItem();
     final HistoryItem mHistoryLastLastWritten = new HistoryItem();
@@ -816,7 +820,6 @@
 
     private BatteryStatsHistoryIterator mBatteryStatsHistoryIterator;
     private HistoryItem mHistoryIterator;
-    private boolean mReadOverflow;
 
     int mStartCount;
 
@@ -841,17 +844,84 @@
     public boolean mRecordAllHistory;
     boolean mNoAutoReset;
 
+    /**
+     * Overall screen state. For multidisplay devices, this represents the current highest screen
+     * state of the displays.
+     */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     protected int mScreenState = Display.STATE_UNKNOWN;
+    /**
+     * Overall screen on timer. For multidisplay devices, this represents the time spent with at
+     * least one display in the screen on state.
+     */
     StopwatchTimer mScreenOnTimer;
+    /**
+     * Overall screen doze timer. For multidisplay devices, this represents the time spent with
+     * screen doze being the highest screen state.
+     */
     StopwatchTimer mScreenDozeTimer;
-
+    /**
+     * Overall screen brightness bin. For multidisplay devices, this represents the current
+     * brightest screen.
+     */
     int mScreenBrightnessBin = -1;
+    /**
+     * Overall screen brightness timers. For multidisplay devices, the {@link mScreenBrightnessBin}
+     * timer will be active at any given time
+     */
     final StopwatchTimer[] mScreenBrightnessTimer =
             new StopwatchTimer[NUM_SCREEN_BRIGHTNESS_BINS];
 
     boolean mPretendScreenOff;
 
+    private static class DisplayBatteryStats {
+        /**
+         * Per display screen state.
+         */
+        public int screenState = Display.STATE_UNKNOWN;
+        /**
+         * Per display screen on timers.
+         */
+        public StopwatchTimer screenOnTimer;
+        /**
+         * Per display screen doze timers.
+         */
+        public StopwatchTimer screenDozeTimer;
+        /**
+         * Per display screen brightness bins.
+         */
+        public int screenBrightnessBin = -1;
+        /**
+         * Per display screen brightness timers.
+         */
+        public StopwatchTimer[] screenBrightnessTimers =
+                new StopwatchTimer[NUM_SCREEN_BRIGHTNESS_BINS];
+
+        DisplayBatteryStats(Clock clock, TimeBase timeBase) {
+            screenOnTimer = new StopwatchTimer(clock, null, -1, null,
+                    timeBase);
+            screenDozeTimer = new StopwatchTimer(clock, null, -1, null,
+                    timeBase);
+            for (int i = 0; i < NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+                screenBrightnessTimers[i] = new StopwatchTimer(clock, null, -100 - i, null,
+                        timeBase);
+            }
+        }
+
+        /**
+         * Reset display timers.
+         */
+        public void reset(long elapsedRealtimeUs) {
+            screenOnTimer.reset(false, elapsedRealtimeUs);
+            screenDozeTimer.reset(false, elapsedRealtimeUs);
+            for (int i = 0; i < NUM_SCREEN_BRIGHTNESS_BINS; i++) {
+                screenBrightnessTimers[i].reset(false, elapsedRealtimeUs);
+            }
+        }
+    }
+
+    DisplayBatteryStats[] mPerDisplayBatteryStats;
+
     boolean mInteractive;
     StopwatchTimer mInteractiveTimer;
 
@@ -1191,12 +1261,21 @@
     }
 
     public BatteryStatsImpl(Clock clock) {
+        this(clock, (File) null);
+    }
+
+    public BatteryStatsImpl(Clock clock, File historyDirectory) {
         init(clock);
         mStartClockTimeMs = clock.currentTimeMillis();
-        mStatsFile = null;
         mCheckinFile = null;
         mDailyFile = null;
-        mBatteryStatsHistory = new BatteryStatsHistory(mHistoryBuffer);
+        if (historyDirectory == null) {
+            mStatsFile = null;
+            mBatteryStatsHistory = new BatteryStatsHistory(mHistoryBuffer);
+        } else {
+            mStatsFile = new AtomicFile(new File(historyDirectory, "batterystats.bin"));
+            mBatteryStatsHistory = new BatteryStatsHistory(this, historyDirectory, mHistoryBuffer);
+        }
         mHandler = null;
         mPlatformIdleStateCallback = null;
         mMeasuredEnergyRetriever = null;
@@ -3321,21 +3400,43 @@
         return kmt;
     }
 
+    /**
+     * Returns the index for the specified tag. If this is the first time the tag is encountered
+     * while writing the current history buffer, the method returns
+     * <code>(index | TAG_FIRST_OCCURRENCE_FLAG)</code>
+     */
     private int writeHistoryTag(HistoryTag tag) {
         Integer idxObj = mHistoryTagPool.get(tag);
         int idx;
         if (idxObj != null) {
             idx = idxObj;
-        } else {
+            if ((idx & TAG_FIRST_OCCURRENCE_FLAG) != 0) {
+                idx &= ~TAG_FIRST_OCCURRENCE_FLAG;
+                mHistoryTagPool.put(tag, idx);
+            }
+            return idx;
+        } else if (mNextHistoryTagIdx < HISTORY_TAG_INDEX_LIMIT) {
             idx = mNextHistoryTagIdx;
             HistoryTag key = new HistoryTag();
             key.setTo(tag);
             tag.poolIdx = idx;
             mHistoryTagPool.put(key, idx);
             mNextHistoryTagIdx++;
-            mNumHistoryTagChars += key.string.length() + 1;
+            final int stringLength = key.string.length();
+
+            if (stringLength > MAX_HISTORY_TAG_STRING_LENGTH) {
+                Slog.wtf(TAG, "Long battery history tag: " + key.string);
+            }
+
+            mNumHistoryTagChars += stringLength + 1;
+            if (mHistoryTags != null) {
+                mHistoryTags.put(idx, key);
+            }
+            return idx | TAG_FIRST_OCCURRENCE_FLAG;
+        } else {
+            // Tag pool overflow: include the tag itself in the parcel
+            return HISTORY_TAG_INDEX_LIMIT | TAG_FIRST_OCCURRENCE_FLAG;
         }
-        return idx;
     }
 
     /*
@@ -3439,6 +3540,10 @@
     // These upper bits are the frequently changing state bits.
     static final int DELTA_STATE_MASK                       = 0xfe000000;
 
+    // Flag in history tag index: indicates that this is the first occurrence of this tag,
+    // therefore the tag value is written in the parcel
+    static final int TAG_FIRST_OCCURRENCE_FLAG = 0x8000;
+
     // These are the pieces of battery state that are packed in to the upper bits of
     // the state int that have been packed in to the first delta int.  They must fit
     // in STATE_BATTERY_MASK.
@@ -3556,11 +3661,23 @@
                 wakeReasonIndex = 0xffff;
             }
             dest.writeInt((wakeReasonIndex<<16) | wakeLockIndex);
+            if (cur.wakelockTag != null && (wakeLockIndex & TAG_FIRST_OCCURRENCE_FLAG) != 0) {
+                cur.wakelockTag.writeToParcel(dest, 0);
+                cur.tagsFirstOccurrence = true;
+            }
+            if (cur.wakeReasonTag != null && (wakeReasonIndex & TAG_FIRST_OCCURRENCE_FLAG) != 0) {
+                cur.wakeReasonTag.writeToParcel(dest, 0);
+                cur.tagsFirstOccurrence = true;
+            }
         }
         if (cur.eventCode != HistoryItem.EVENT_NONE) {
-            int index = writeHistoryTag(cur.eventTag);
-            int codeAndIndex = (cur.eventCode&0xffff) | (index<<16);
+            final int index = writeHistoryTag(cur.eventTag);
+            final int codeAndIndex = (cur.eventCode & 0xffff) | (index << 16);
             dest.writeInt(codeAndIndex);
+            if ((index & TAG_FIRST_OCCURRENCE_FLAG) != 0) {
+                cur.eventTag.writeToParcel(dest, 0);
+                cur.tagsFirstOccurrence = true;
+            }
             if (DEBUG) Slog.i(TAG, "WRITE DELTA: event=" + cur.eventCode + " tag=#"
                     + cur.eventTag.poolIdx + " " + cur.eventTag.uid + ":"
                     + cur.eventTag.string);
@@ -3750,6 +3867,7 @@
         if (mHistoryBufferLastPos >= 0 && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE
                 && timeDiffMs < 1000 && (diffStates & lastDiffStates) == 0
                 && (diffStates2&lastDiffStates2) == 0
+                && (!mHistoryLastWritten.tagsFirstOccurrence && !cur.tagsFirstOccurrence)
                 && (mHistoryLastWritten.wakelockTag == null || cur.wakelockTag == null)
                 && (mHistoryLastWritten.wakeReasonTag == null || cur.wakeReasonTag == null)
                 && mHistoryLastWritten.stepDetails == null
@@ -3809,9 +3927,17 @@
             mHistoryBuffer.setDataPosition(0);
             mHistoryBuffer.setDataCapacity(mConstants.MAX_HISTORY_BUFFER / 2);
             mHistoryBufferLastPos = -1;
+            mHistoryLastWritten.clear();
+            mHistoryLastLastWritten.clear();
+
+            // Mark every entry in the pool with a flag indicating that the tag
+            // has not yet been encountered while writing the current history buffer.
+            for (Map.Entry<HistoryTag, Integer> entry: mHistoryTagPool.entrySet()) {
+                entry.setValue(entry.getValue() | TAG_FIRST_OCCURRENCE_FLAG);
+            }
+            startRecordingHistory(elapsedRealtimeMs, uptimeMs, false);
             HistoryItem newItem = new HistoryItem();
             newItem.setTo(cur);
-            startRecordingHistory(elapsedRealtimeMs, uptimeMs, false);
             addHistoryBufferLocked(elapsedRealtimeMs, HistoryItem.CMD_UPDATE, newItem);
             return;
         }
@@ -3830,7 +3956,9 @@
         }
         mHistoryBufferLastPos = mHistoryBuffer.dataPosition();
         mHistoryLastLastWritten.setTo(mHistoryLastWritten);
+        final boolean hasTags = mHistoryLastWritten.tagsFirstOccurrence || cur.tagsFirstOccurrence;
         mHistoryLastWritten.setTo(mHistoryBaseTimeMs + elapsedRealtimeMs, cmd, cur);
+        mHistoryLastWritten.tagsFirstOccurrence = hasTags;
         mHistoryLastWritten.states &= mActiveHistoryStates;
         mHistoryLastWritten.states2 &= mActiveHistoryStates2;
         writeHistoryDelta(mHistoryBuffer, mHistoryLastWritten, mHistoryLastLastWritten);
@@ -3839,6 +3967,7 @@
         cur.wakeReasonTag = null;
         cur.eventCode = HistoryItem.EVENT_NONE;
         cur.eventTag = null;
+        cur.tagsFirstOccurrence = false;
         if (DEBUG_HISTORY) Slog.i(TAG, "Writing history buffer: was " + mHistoryBufferLastPos
                 + " now " + mHistoryBuffer.dataPosition()
                 + " size is now " + mHistoryBuffer.dataSize());
@@ -4393,8 +4522,10 @@
     public void setPretendScreenOff(boolean pretendScreenOff) {
         if (mPretendScreenOff != pretendScreenOff) {
             mPretendScreenOff = pretendScreenOff;
-            noteScreenStateLocked(pretendScreenOff ? Display.STATE_OFF : Display.STATE_ON,
-                    mClock.elapsedRealtime(), mClock.uptimeMillis(), mClock.currentTimeMillis());
+            final int primaryScreenState = mPerDisplayBatteryStats[0].screenState;
+            noteScreenStateLocked(0, primaryScreenState,
+                    mClock.elapsedRealtime(), mClock.uptimeMillis(),
+                    mClock.currentTimeMillis());
         }
     }
 
@@ -4992,29 +5123,158 @@
     }
 
     @GuardedBy("this")
-    public void noteScreenStateLocked(int state) {
-        noteScreenStateLocked(state, mClock.elapsedRealtime(), mClock.uptimeMillis(),
+    public void noteScreenStateLocked(int display, int state) {
+        noteScreenStateLocked(display, state, mClock.elapsedRealtime(), mClock.uptimeMillis(),
                 mClock.currentTimeMillis());
     }
 
     @GuardedBy("this")
-    public void noteScreenStateLocked(int state,
+    public void noteScreenStateLocked(int display, int displayState,
             long elapsedRealtimeMs, long uptimeMs, long currentTimeMs) {
-        state = mPretendScreenOff ? Display.STATE_OFF : state;
-
         // Battery stats relies on there being 4 states. To accommodate this, new states beyond the
         // original 4 are mapped to one of the originals.
-        if (state > MAX_TRACKED_SCREEN_STATE) {
-            switch (state) {
-                case Display.STATE_VR:
-                    state = Display.STATE_ON;
+        if (displayState > MAX_TRACKED_SCREEN_STATE) {
+            if (Display.isOnState(displayState)) {
+                displayState = Display.STATE_ON;
+            } else if (Display.isDozeState(displayState)) {
+                if (Display.isSuspendedState(displayState)) {
+                    displayState = Display.STATE_DOZE_SUSPEND;
+                } else {
+                    displayState = Display.STATE_DOZE;
+                }
+            } else if (Display.isOffState(displayState)) {
+                displayState = Display.STATE_OFF;
+            } else {
+                Slog.wtf(TAG, "Unknown screen state (not mapped): " + displayState);
+                displayState = Display.STATE_UNKNOWN;
+            }
+        }
+        // As of this point, displayState should be mapped to one of:
+        //  - Display.STATE_ON,
+        //  - Display.STATE_DOZE
+        //  - Display.STATE_DOZE_SUSPEND
+        //  - Display.STATE_OFF
+        //  - Display.STATE_UNKNOWN
+
+        int state;
+        int overallBin = mScreenBrightnessBin;
+        int externalUpdateFlag = 0;
+        boolean shouldScheduleSync = false;
+        final int numDisplay = mPerDisplayBatteryStats.length;
+        if (display < 0 || display >= numDisplay) {
+            Slog.wtf(TAG, "Unexpected note screen state for display " + display + " (only "
+                    + mPerDisplayBatteryStats.length + " displays exist...)");
+            return;
+        }
+        final DisplayBatteryStats displayStats = mPerDisplayBatteryStats[display];
+        final int oldDisplayState = displayStats.screenState;
+
+        if (oldDisplayState == displayState) {
+            // Nothing changed
+            state = mScreenState;
+        } else {
+            displayStats.screenState = displayState;
+
+            // Stop timer for previous display state.
+            switch (oldDisplayState) {
+                case Display.STATE_ON:
+                    displayStats.screenOnTimer.stopRunningLocked(elapsedRealtimeMs);
+                    final int bin = displayStats.screenBrightnessBin;
+                    if (bin >= 0) {
+                        displayStats.screenBrightnessTimers[bin].stopRunningLocked(
+                                elapsedRealtimeMs);
+                    }
+                    overallBin = evaluateOverallScreenBrightnessBinLocked();
+                    shouldScheduleSync = true;
+                    break;
+                case Display.STATE_DOZE:
+                    // Transition from doze to doze suspend can be ignored.
+                    if (displayState == Display.STATE_DOZE_SUSPEND) break;
+                    displayStats.screenDozeTimer.stopRunningLocked(elapsedRealtimeMs);
+                    shouldScheduleSync = true;
+                    break;
+                case Display.STATE_DOZE_SUSPEND:
+                    // Transition from doze suspend to doze can be ignored.
+                    if (displayState == Display.STATE_DOZE) break;
+                    displayStats.screenDozeTimer.stopRunningLocked(elapsedRealtimeMs);
+                    shouldScheduleSync = true;
+                    break;
+                case Display.STATE_OFF: // fallthrough
+                case Display.STATE_UNKNOWN:
+                    // Not tracked by timers.
                     break;
                 default:
-                    Slog.wtf(TAG, "Unknown screen state (not mapped): " + state);
+                    Slog.wtf(TAG,
+                            "Attempted to stop timer for unexpected display state " + display);
+            }
+
+            // Start timer for new display state.
+            switch (displayState) {
+                case Display.STATE_ON:
+                    displayStats.screenOnTimer.startRunningLocked(elapsedRealtimeMs);
+                    final int bin = displayStats.screenBrightnessBin;
+                    if (bin >= 0) {
+                        displayStats.screenBrightnessTimers[bin].startRunningLocked(
+                                elapsedRealtimeMs);
+                    }
+                    overallBin = evaluateOverallScreenBrightnessBinLocked();
+                    shouldScheduleSync = true;
                     break;
+                case Display.STATE_DOZE:
+                    // Transition from doze suspend to doze can be ignored.
+                    if (oldDisplayState == Display.STATE_DOZE_SUSPEND) break;
+                    displayStats.screenDozeTimer.startRunningLocked(elapsedRealtimeMs);
+                    shouldScheduleSync = true;
+                    break;
+                case Display.STATE_DOZE_SUSPEND:
+                    // Transition from doze to doze suspend can be ignored.
+                    if (oldDisplayState == Display.STATE_DOZE) break;
+                    displayStats.screenDozeTimer.startRunningLocked(elapsedRealtimeMs);
+                    shouldScheduleSync = true;
+                    break;
+                case Display.STATE_OFF: // fallthrough
+                case Display.STATE_UNKNOWN:
+                    // Not tracked by timers.
+                    break;
+                default:
+                    Slog.wtf(TAG,
+                            "Attempted to start timer for unexpected display state " + displayState
+                                    + " for display " + display);
+            }
+
+            if (shouldScheduleSync
+                    && mGlobalMeasuredEnergyStats != null
+                    && mGlobalMeasuredEnergyStats.isStandardBucketSupported(
+                    MeasuredEnergyStats.POWER_BUCKET_SCREEN_ON)) {
+                // Display measured energy stats is available. Prepare to schedule an
+                // external sync.
+                externalUpdateFlag |= ExternalStatsSync.UPDATE_DISPLAY;
+            }
+
+            // Reevaluate most important display screen state.
+            state = Display.STATE_UNKNOWN;
+            for (int i = 0; i < numDisplay; i++) {
+                final int tempState = mPerDisplayBatteryStats[i].screenState;
+                if (tempState == Display.STATE_ON
+                        || state == Display.STATE_ON) {
+                    state = Display.STATE_ON;
+                } else if (tempState == Display.STATE_DOZE
+                        || state == Display.STATE_DOZE) {
+                    state = Display.STATE_DOZE;
+                } else if (tempState == Display.STATE_DOZE_SUSPEND
+                        || state == Display.STATE_DOZE_SUSPEND) {
+                    state = Display.STATE_DOZE_SUSPEND;
+                } else if (tempState == Display.STATE_OFF
+                        || state == Display.STATE_OFF) {
+                    state = Display.STATE_OFF;
+                }
             }
         }
 
+        final boolean batteryRunning = mOnBatteryTimeBase.isRunning();
+        final boolean batteryScreenOffRunning = mOnBatteryScreenOffTimeBase.isRunning();
+
+        state = mPretendScreenOff ? Display.STATE_OFF : state;
         if (mScreenState != state) {
             recordDailyStatsIfNeededLocked(true, currentTimeMs);
             final int oldState = mScreenState;
@@ -5068,11 +5328,11 @@
                         + Display.stateToString(state));
                 addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
             }
-            // TODO: (Probably overkill) Have mGlobalMeasuredEnergyStats store supported flags and
-            //       only update DISPLAY if it is. Currently overkill since CPU is scheduled anyway.
-            final int updateFlag = ExternalStatsSync.UPDATE_CPU | ExternalStatsSync.UPDATE_DISPLAY;
-            mExternalSync.scheduleSyncDueToScreenStateChange(updateFlag,
-                    mOnBatteryTimeBase.isRunning(), mOnBatteryScreenOffTimeBase.isRunning(), state);
+
+            // Per screen state Cpu stats needed. Prepare to schedule an external sync.
+            externalUpdateFlag |= ExternalStatsSync.UPDATE_CPU;
+            shouldScheduleSync = true;
+
             if (Display.isOnState(state)) {
                 updateTimeBasesLocked(mOnBatteryTimeBase.isRunning(), state,
                         uptimeMs * 1000, elapsedRealtimeMs * 1000);
@@ -5090,33 +5350,116 @@
                 updateDischargeScreenLevelsLocked(oldState, state);
             }
         }
+
+        // Changing display states might have changed the screen used to determine the overall
+        // brightness.
+        maybeUpdateOverallScreenBrightness(overallBin, elapsedRealtimeMs, uptimeMs);
+
+        if (shouldScheduleSync) {
+            final int numDisplays = mPerDisplayBatteryStats.length;
+            final int[] displayStates = new int[numDisplays];
+            for (int i = 0; i < numDisplays; i++) {
+                displayStates[i] = mPerDisplayBatteryStats[i].screenState;
+            }
+            mExternalSync.scheduleSyncDueToScreenStateChange(externalUpdateFlag,
+                    batteryRunning, batteryScreenOffRunning, state, displayStates);
+        }
     }
 
     @UnsupportedAppUsage
     public void noteScreenBrightnessLocked(int brightness) {
-        noteScreenBrightnessLocked(brightness, mClock.elapsedRealtime(), mClock.uptimeMillis());
+        noteScreenBrightnessLocked(0, brightness);
     }
 
-    public void noteScreenBrightnessLocked(int brightness, long elapsedRealtimeMs, long uptimeMs) {
+    /**
+     * Note screen brightness change for a display.
+     */
+    public void noteScreenBrightnessLocked(int display, int brightness) {
+        noteScreenBrightnessLocked(display, brightness, mClock.elapsedRealtime(),
+                mClock.uptimeMillis());
+    }
+
+
+    /**
+     * Note screen brightness change for a display.
+     */
+    public void noteScreenBrightnessLocked(int display, int brightness, long elapsedRealtimeMs,
+            long uptimeMs) {
         // Bin the brightness.
         int bin = brightness / (256/NUM_SCREEN_BRIGHTNESS_BINS);
         if (bin < 0) bin = 0;
         else if (bin >= NUM_SCREEN_BRIGHTNESS_BINS) bin = NUM_SCREEN_BRIGHTNESS_BINS-1;
-        if (mScreenBrightnessBin != bin) {
-            mHistoryCur.states = (mHistoryCur.states&~HistoryItem.STATE_BRIGHTNESS_MASK)
-                    | (bin << HistoryItem.STATE_BRIGHTNESS_SHIFT);
-            if (DEBUG_HISTORY) Slog.v(TAG, "Screen brightness " + bin + " to: "
-                    + Integer.toHexString(mHistoryCur.states));
-            addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+
+        final int overallBin;
+
+        final int numDisplays = mPerDisplayBatteryStats.length;
+        if (display < 0 || display >= numDisplays) {
+            Slog.wtf(TAG, "Unexpected note screen brightness for display " + display + " (only "
+                    + mPerDisplayBatteryStats.length + " displays exist...)");
+            return;
+        }
+
+        final DisplayBatteryStats displayStats = mPerDisplayBatteryStats[display];
+        final int oldBin = displayStats.screenBrightnessBin;
+        if (oldBin == bin) {
+            // Nothing changed
+            overallBin = mScreenBrightnessBin;
+        } else {
+            displayStats.screenBrightnessBin = bin;
+            if (displayStats.screenState == Display.STATE_ON) {
+                if (oldBin >= 0) {
+                    displayStats.screenBrightnessTimers[oldBin].stopRunningLocked(
+                            elapsedRealtimeMs);
+                }
+                displayStats.screenBrightnessTimers[bin].startRunningLocked(
+                        elapsedRealtimeMs);
+            }
+            overallBin = evaluateOverallScreenBrightnessBinLocked();
+        }
+
+        maybeUpdateOverallScreenBrightness(overallBin, elapsedRealtimeMs, uptimeMs);
+    }
+
+    private int evaluateOverallScreenBrightnessBinLocked() {
+        int overallBin = -1;
+        final int numDisplays = getDisplayCount();
+        for (int display = 0; display < numDisplays; display++) {
+            final int displayBrightnessBin;
+            if (mPerDisplayBatteryStats[display].screenState == Display.STATE_ON) {
+                displayBrightnessBin = mPerDisplayBatteryStats[display].screenBrightnessBin;
+            } else {
+                displayBrightnessBin = -1;
+            }
+            if (displayBrightnessBin > overallBin) {
+                overallBin = displayBrightnessBin;
+            }
+        }
+        return overallBin;
+    }
+
+    private void maybeUpdateOverallScreenBrightness(int overallBin, long elapsedRealtimeMs,
+            long uptimeMs) {
+        if (mScreenBrightnessBin != overallBin) {
+            if (overallBin >= 0) {
+                mHistoryCur.states = (mHistoryCur.states & ~HistoryItem.STATE_BRIGHTNESS_MASK)
+                        | (overallBin << HistoryItem.STATE_BRIGHTNESS_SHIFT);
+                if (DEBUG_HISTORY) {
+                    Slog.v(TAG, "Screen brightness " + overallBin + " to: "
+                            + Integer.toHexString(mHistoryCur.states));
+                }
+                addHistoryRecordLocked(elapsedRealtimeMs, uptimeMs);
+            }
             if (mScreenState == Display.STATE_ON) {
                 if (mScreenBrightnessBin >= 0) {
                     mScreenBrightnessTimer[mScreenBrightnessBin]
                             .stopRunningLocked(elapsedRealtimeMs);
                 }
-                mScreenBrightnessTimer[bin]
-                        .startRunningLocked(elapsedRealtimeMs);
+                if (overallBin >= 0) {
+                    mScreenBrightnessTimer[overallBin]
+                            .startRunningLocked(elapsedRealtimeMs);
+                }
             }
-            mScreenBrightnessBin = bin;
+            mScreenBrightnessBin = overallBin;
         }
     }
 
@@ -6780,6 +7123,31 @@
         return mScreenBrightnessTimer[brightnessBin];
     }
 
+    @Override
+    public int getDisplayCount() {
+        return mPerDisplayBatteryStats.length;
+    }
+
+    @Override
+    public long getDisplayScreenOnTime(int display, long elapsedRealtimeUs) {
+        return mPerDisplayBatteryStats[display].screenOnTimer.getTotalTimeLocked(elapsedRealtimeUs,
+                STATS_SINCE_CHARGED);
+    }
+
+    @Override
+    public long getDisplayScreenDozeTime(int display, long elapsedRealtimeUs) {
+        return mPerDisplayBatteryStats[display].screenDozeTimer.getTotalTimeLocked(
+                elapsedRealtimeUs, STATS_SINCE_CHARGED);
+    }
+
+    @Override
+    public long getDisplayScreenBrightnessTime(int display, int brightnessBin,
+            long elapsedRealtimeUs) {
+        final DisplayBatteryStats displayStats = mPerDisplayBatteryStats[display];
+        return displayStats.screenBrightnessTimers[brightnessBin].getTotalTimeLocked(
+                elapsedRealtimeUs, STATS_SINCE_CHARGED);
+    }
+
     @Override public long getInteractiveTime(long elapsedRealtimeUs, int which) {
         return mInteractiveTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
     }
@@ -10813,6 +11181,10 @@
             mScreenBrightnessTimer[i] = new StopwatchTimer(mClock, null, -100 - i, null,
                     mOnBatteryTimeBase);
         }
+
+        mPerDisplayBatteryStats = new DisplayBatteryStats[1];
+        mPerDisplayBatteryStats[0] = new DisplayBatteryStats(mClock, mOnBatteryTimeBase);
+
         mInteractiveTimer = new StopwatchTimer(mClock, null, -10, null, mOnBatteryTimeBase);
         mPowerSaveModeEnabledTimer = new StopwatchTimer(mClock, null, -2, null,
                 mOnBatteryTimeBase);
@@ -10925,6 +11297,8 @@
             // Initialize the estimated battery capacity to a known preset one.
             mEstimatedBatteryCapacityMah = (int) mPowerProfile.getBatteryCapacity();
         }
+
+        setDisplayCountLocked(mPowerProfile.getNumDisplays());
     }
 
     PowerProfile getPowerProfile() {
@@ -10957,6 +11331,16 @@
         mExternalSync = sync;
     }
 
+    /**
+     * Initialize and set multi display timers and states.
+     */
+    public void setDisplayCountLocked(int numDisplays) {
+        mPerDisplayBatteryStats = new DisplayBatteryStats[numDisplays];
+        for (int i = 0; i < numDisplays; i++) {
+            mPerDisplayBatteryStats[i] = new DisplayBatteryStats(mClock, mOnBatteryTimeBase);
+        }
+    }
+
     public void updateDailyDeadlineLocked() {
         // Get the current time.
         long currentTimeMs = mDailyStartTimeMs = mClock.currentTimeMillis();
@@ -11281,7 +11665,6 @@
     @Override
     @UnsupportedAppUsage
     public boolean startIteratingHistoryLocked() {
-        mReadOverflow = false;
         mBatteryStatsHistoryIterator = createBatteryStatsHistoryIterator();
         return true;
     }
@@ -11291,34 +11674,42 @@
      */
     @VisibleForTesting
     public BatteryStatsHistoryIterator createBatteryStatsHistoryIterator() {
-        ArrayList<HistoryTag> tags = new ArrayList<>(mHistoryTagPool.size());
-        for (Map.Entry<HistoryTag, Integer> entry: mHistoryTagPool.entrySet()) {
-            final HistoryTag tag = entry.getKey();
-            tag.poolIdx = entry.getValue();
-            tags.add(tag);
-        }
-
-        return new BatteryStatsHistoryIterator(mBatteryStatsHistory, tags);
+        return new BatteryStatsHistoryIterator(mBatteryStatsHistory);
     }
 
     @Override
     public int getHistoryStringPoolSize() {
-        return mBatteryStatsHistoryIterator.getHistoryStringPoolSize();
+        return mHistoryTagPool.size();
     }
 
     @Override
     public int getHistoryStringPoolBytes() {
-        return mBatteryStatsHistoryIterator.getHistoryStringPoolBytes();
+        return mNumHistoryTagChars;
     }
 
     @Override
     public String getHistoryTagPoolString(int index) {
-        return mBatteryStatsHistoryIterator.getHistoryTagPoolString(index);
+        ensureHistoryTagArray();
+        HistoryTag historyTag = mHistoryTags.get(index);
+        return historyTag != null ? historyTag.string : null;
     }
 
     @Override
     public int getHistoryTagPoolUid(int index) {
-        return mBatteryStatsHistoryIterator.getHistoryTagPoolUid(index);
+        ensureHistoryTagArray();
+        HistoryTag historyTag = mHistoryTags.get(index);
+        return historyTag != null ? historyTag.uid : Process.INVALID_UID;
+    }
+
+    private void ensureHistoryTagArray() {
+        if (mHistoryTags != null) {
+            return;
+        }
+
+        mHistoryTags = new SparseArray<>(mHistoryTagPool.size());
+        for (Map.Entry<HistoryTag, Integer> entry: mHistoryTagPool.entrySet()) {
+            mHistoryTags.put(entry.getValue(), entry.getKey());
+        }
     }
 
     @Override
@@ -11433,6 +11824,11 @@
             mScreenBrightnessTimer[i].reset(false, elapsedRealtimeUs);
         }
 
+        final int numDisplays = mPerDisplayBatteryStats.length;
+        for (int i = 0; i < numDisplays; i++) {
+            mPerDisplayBatteryStats[i].reset(elapsedRealtimeUs);
+        }
+
         if (mPowerProfile != null) {
             mEstimatedBatteryCapacityMah = (int) mPowerProfile.getBatteryCapacity();
         } else {
@@ -14700,6 +15096,11 @@
         mShuttingDown = true;
     }
 
+    @Override
+    public boolean isProcessStateDataAvailable() {
+        return trackPerProcStateCpuTimes();
+    }
+
     public boolean trackPerProcStateCpuTimes() {
         return mConstants.TRACK_CPU_TIMES_BY_PROC_STATE && mPerProcStateCpuTimesAvailable;
     }
diff --git a/core/java/com/android/internal/os/BatteryUsageStatsProvider.java b/core/java/com/android/internal/os/BatteryUsageStatsProvider.java
index e9e489d..88425be 100644
--- a/core/java/com/android/internal/os/BatteryUsageStatsProvider.java
+++ b/core/java/com/android/internal/os/BatteryUsageStatsProvider.java
@@ -31,7 +31,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Uses accumulated battery stats data and PowerCalculators to produce power
@@ -153,9 +152,13 @@
 
         final boolean includePowerModels = (query.getFlags()
                 & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_MODELS) != 0;
+        final boolean includeProcessStateData = ((query.getFlags()
+                & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA) != 0)
+                && mStats.isProcessStateDataAvailable();
 
         final BatteryUsageStats.Builder batteryUsageStatsBuilder = new BatteryUsageStats.Builder(
-                mStats.getCustomEnergyConsumerNames(), includePowerModels);
+                mStats.getCustomEnergyConsumerNames(), includePowerModels,
+                includeProcessStateData);
         // TODO(b/188068523): use a monotonic clock to ensure resilience of order and duration
         // of stats sessions to wall-clock adjustments
         batteryUsageStatsBuilder.setStatsStartTimestamp(mStats.getStartClockTime());
@@ -186,16 +189,7 @@
             }
 
             BatteryStatsImpl batteryStatsImpl = (BatteryStatsImpl) mStats;
-            ArrayList<BatteryStats.HistoryTag> tags = new ArrayList<>(
-                    batteryStatsImpl.mHistoryTagPool.size());
-            for (Map.Entry<BatteryStats.HistoryTag, Integer> entry :
-                    batteryStatsImpl.mHistoryTagPool.entrySet()) {
-                final BatteryStats.HistoryTag tag = entry.getKey();
-                tag.poolIdx = entry.getValue();
-                tags.add(tag);
-            }
-
-            batteryUsageStatsBuilder.setBatteryHistory(batteryStatsImpl.mHistoryBuffer, tags);
+            batteryUsageStatsBuilder.setBatteryHistory(batteryStatsImpl.mHistoryBuffer);
         }
 
         return batteryUsageStatsBuilder.build();
@@ -234,10 +228,13 @@
     private BatteryUsageStats getAggregatedBatteryUsageStats(BatteryUsageStatsQuery query) {
         final boolean includePowerModels = (query.getFlags()
                 & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_MODELS) != 0;
+        final boolean includeProcessStateData = ((query.getFlags()
+                & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA) != 0)
+                && mStats.isProcessStateDataAvailable();
 
         final String[] customEnergyConsumerNames = mStats.getCustomEnergyConsumerNames();
         final BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(
-                customEnergyConsumerNames, includePowerModels);
+                customEnergyConsumerNames, includePowerModels, includeProcessStateData);
         if (mBatteryUsageStatsStore == null) {
             Log.e(TAG, "BatteryUsageStatsStore is unavailable");
             return builder.build();
@@ -248,16 +245,25 @@
             if (timestamp > query.getFromTimestamp() && timestamp <= query.getToTimestamp()) {
                 final BatteryUsageStats snapshot =
                         mBatteryUsageStatsStore.loadBatteryUsageStats(timestamp);
-                if (snapshot != null) {
-                    if (Arrays.equals(snapshot.getCustomPowerComponentNames(),
-                            customEnergyConsumerNames)) {
-                        builder.add(snapshot);
-                    } else {
-                        Log.w(TAG, "Ignoring older BatteryUsageStats snapshot, which has different "
-                                + "custom power components: "
-                                + Arrays.toString(snapshot.getCustomPowerComponentNames()));
-                    }
+                if (snapshot == null) {
+                    continue;
                 }
+
+                if (!Arrays.equals(snapshot.getCustomPowerComponentNames(),
+                        customEnergyConsumerNames)) {
+                    Log.w(TAG, "Ignoring older BatteryUsageStats snapshot, which has different "
+                            + "custom power components: "
+                            + Arrays.toString(snapshot.getCustomPowerComponentNames()));
+                    continue;
+                }
+
+                if (includeProcessStateData && !snapshot.isProcessStateDataIncluded()) {
+                    Log.w(TAG, "Ignoring older BatteryUsageStats snapshot, which "
+                            + " does not include process state data");
+                    continue;
+                }
+
+                builder.add(snapshot);
             }
         }
         return builder.build();
diff --git a/core/java/com/android/internal/os/LongMultiStateCounter.java b/core/java/com/android/internal/os/LongMultiStateCounter.java
new file mode 100644
index 0000000..518d9ab
--- /dev/null
+++ b/core/java/com/android/internal/os/LongMultiStateCounter.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.os;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import dalvik.annotation.optimization.CriticalNative;
+import dalvik.annotation.optimization.FastNative;
+
+import libcore.util.NativeAllocationRegistry;
+
+/**
+ * Performs per-state counting of long integers over time.  The tracked "value" is expected
+ * to increase monotonously. The counter keeps track of the current state.  When the
+ * updateValue method is called, the delta from the previous invocation of this method
+ * and the new value is added to the counter corresponding to the current state.  If the
+ * state changed in the interim, the delta is distributed proptionally.
+ *
+ * The class's behavior is illustrated by this example:
+ * <pre>
+ *   // At 0 ms, the state of the tracked object is 0 and the initial tracked value is 100
+ *   counter.setState(0, 0);
+ *   counter.updateValue(100, 0);
+ *
+ *   // At 1000 ms, the state changes to 1
+ *   counter.setState(1, 1000);
+ *
+ *   // At 3000 ms, the tracked value is updated to 130
+ *   counter.updateValue(130, 3000);
+ *
+ *   // The delta (130 - 100 = 30) is distributed between states 0 and 1 according to the time
+ *   // spent in those respective states; in this specific case, 1000 and 2000 ms.
+ *   long countForState0 == counter.getCount(0);  // 10
+ *   long countForState1 == counter.getCount(1);  // 20
+ * </pre>
+ *
+ * The tracked values are expected to increase monotonically.
+ *
+ * @hide
+ */
+public final class LongMultiStateCounter implements Parcelable {
+
+    private static final NativeAllocationRegistry sRegistry =
+            NativeAllocationRegistry.createMalloced(
+                    LongMultiStateCounter.class.getClassLoader(), native_getReleaseFunc());
+
+    private final int mStateCount;
+
+    // Visible to other objects in this package so that it can be passed to @CriticalNative
+    // methods.
+    final long mNativeObject;
+
+    public LongMultiStateCounter(int stateCount) {
+        Preconditions.checkArgumentPositive(stateCount, "stateCount must be greater than 0");
+        mStateCount = stateCount;
+        mNativeObject = native_init(stateCount);
+        sRegistry.registerNativeAllocation(this, mNativeObject);
+    }
+
+    private LongMultiStateCounter(Parcel in) {
+        mNativeObject = native_initFromParcel(in);
+        sRegistry.registerNativeAllocation(this, mNativeObject);
+
+        mStateCount = native_getStateCount(mNativeObject);
+    }
+
+    public int getStateCount() {
+        return mStateCount;
+    }
+
+    /**
+     * Enables or disables the counter.  When the counter is disabled, it does not
+     * accumulate counts supplied by the {@link #updateValue} method.
+     */
+    public void setEnabled(boolean enabled, long timestampMs) {
+        native_setEnabled(mNativeObject, enabled, timestampMs);
+    }
+
+    /**
+     * Sets the current state to the supplied value.
+     *
+     * @param state The new state
+     * @param timestampMs The time when the state change occurred, e.g.
+     *                    SystemClock.elapsedRealtime()
+     */
+    public void setState(int state, long timestampMs) {
+        if (state < 0 || state >= mStateCount) {
+            throw new IllegalArgumentException(
+                    "State: " + state + ", outside the range: [0-" + (mStateCount - 1) + "]");
+        }
+        native_setState(mNativeObject, state, timestampMs);
+    }
+
+    /**
+     * Sets the new values.  The delta between the previously set values and these values
+     * is distributed among the state according to the time the object spent in those states
+     * since the previous call to updateValues.
+     */
+    public void updateValue(long value, long timestampMs) {
+        native_updateValue(mNativeObject, value, timestampMs);
+    }
+
+    /**
+     * Adds the supplied values to the current accumulated values in the counter.
+     */
+    public void addCount(long count) {
+        native_addCount(mNativeObject, count);
+    }
+
+    /**
+     * Resets the accumulated counts to 0.
+     */
+    public void reset() {
+        native_reset(mNativeObject);
+    }
+
+    /**
+     * Returns the accumulated count for the specified state.
+     */
+    public long getCount(int state) {
+        if (state < 0 || state >= mStateCount) {
+            throw new IllegalArgumentException(
+                    "State: " + state + ", outside the range: [0-" + mStateCount + "]");
+        }
+        return native_getCount(mNativeObject, state);
+    }
+
+    @Override
+    public String toString() {
+        return native_toString(mNativeObject);
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        native_writeToParcel(mNativeObject, dest, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Creator<LongMultiStateCounter> CREATOR =
+            new Creator<LongMultiStateCounter>() {
+                @Override
+                public LongMultiStateCounter createFromParcel(Parcel in) {
+                    return new LongMultiStateCounter(in);
+                }
+
+                @Override
+                public LongMultiStateCounter[] newArray(int size) {
+                    return new LongMultiStateCounter[size];
+                }
+            };
+
+
+    @CriticalNative
+    private static native long native_init(int stateCount);
+
+    @CriticalNative
+    private static native long native_getReleaseFunc();
+
+    @CriticalNative
+    private static native void native_setEnabled(long nativeObject, boolean enabled,
+            long timestampMs);
+
+    @CriticalNative
+    private static native void native_setState(long nativeObject, int state, long timestampMs);
+
+    @CriticalNative
+    private static native void native_updateValue(long nativeObject, long value, long timestampMs);
+
+    @CriticalNative
+    private static native void native_addCount(long nativeObject, long count);
+
+    @CriticalNative
+    private static native void native_reset(long nativeObject);
+
+    @CriticalNative
+    private static native long native_getCount(long nativeObject, int state);
+
+    @FastNative
+    private native String native_toString(long nativeObject);
+
+    @FastNative
+    private native void native_writeToParcel(long nativeObject, Parcel dest, int flags);
+
+    @FastNative
+    private static native long native_initFromParcel(Parcel parcel);
+
+    @CriticalNative
+    private static native int native_getStateCount(long nativeObject);
+}
diff --git a/core/java/com/android/internal/os/PowerProfile.java b/core/java/com/android/internal/os/PowerProfile.java
index add2304..4d19b35 100644
--- a/core/java/com/android/internal/os/PowerProfile.java
+++ b/core/java/com/android/internal/os/PowerProfile.java
@@ -17,10 +17,12 @@
 package com.android.internal.os;
 
 
+import android.annotation.StringDef;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
+import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -30,6 +32,8 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.HashMap;
 
@@ -40,6 +44,8 @@
  */
 public class PowerProfile {
 
+    public static final String TAG = "PowerProfile";
+
     /*
      * POWER_CPU_SUSPEND: Power consumption when CPU is in power collapse mode.
      * POWER_CPU_IDLE: Power consumption when CPU is awake (when a wake lock is held). This should
@@ -145,12 +151,18 @@
 
     /**
      * Power consumption when screen is in doze/ambient/always-on mode, including backlight power.
+     *
+     * @deprecated Use {@link #POWER_GROUP_DISPLAY_AMBIENT} instead.
      */
+    @Deprecated
     public static final String POWER_AMBIENT_DISPLAY = "ambient.on";
 
     /**
      * Power consumption when screen is on, not including the backlight power.
+     *
+     * @deprecated Use {@link #POWER_GROUP_DISPLAY_SCREEN_ON} instead.
      */
+    @Deprecated
     @UnsupportedAppUsage
     public static final String POWER_SCREEN_ON = "screen.on";
 
@@ -175,7 +187,10 @@
     /**
      * Power consumption at full backlight brightness. If the backlight is at
      * 50% brightness, then this should be multiplied by 0.5
+     *
+     * @deprecated Use {@link #POWER_GROUP_DISPLAY_SCREEN_FULL} instead.
      */
+    @Deprecated
     @UnsupportedAppUsage
     public static final String POWER_SCREEN_FULL = "screen.full";
 
@@ -221,6 +236,29 @@
     public static final String POWER_BATTERY_CAPACITY = "battery.capacity";
 
     /**
+     * Power consumption when a screen is in doze/ambient/always-on mode, including backlight power.
+     */
+    public static final String POWER_GROUP_DISPLAY_AMBIENT = "ambient.on.display";
+
+    /**
+     * Power consumption when a screen is on, not including the backlight power.
+     */
+    public static final String POWER_GROUP_DISPLAY_SCREEN_ON = "screen.on.display";
+
+    /**
+     * Power consumption of a screen at full backlight brightness.
+     */
+    public static final String POWER_GROUP_DISPLAY_SCREEN_FULL = "screen.full.display";
+
+    @StringDef(prefix = { "POWER_GROUP_" }, value = {
+            POWER_GROUP_DISPLAY_AMBIENT,
+            POWER_GROUP_DISPLAY_SCREEN_ON,
+            POWER_GROUP_DISPLAY_SCREEN_FULL,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PowerGroup {}
+
+    /**
      * A map from Power Use Item to its power consumption.
      */
     static final HashMap<String, Double> sPowerItemMap = new HashMap<>();
@@ -255,6 +293,7 @@
                 readPowerValuesFromXml(context, forTest);
             }
             initCpuClusters();
+            initDisplays();
         }
     }
 
@@ -424,6 +463,58 @@
         return 0;
     }
 
+    private int mNumDisplays;
+
+    private void initDisplays() {
+        // Figure out how many displays are listed in the power profile.
+        mNumDisplays = 0;
+        while (!Double.isNaN(
+                getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, mNumDisplays, Double.NaN))
+                || !Double.isNaN(
+                getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, mNumDisplays, Double.NaN))
+                || !Double.isNaN(
+                getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, mNumDisplays,
+                        Double.NaN))) {
+            mNumDisplays++;
+        }
+
+        // Handle legacy display power constants.
+        final Double deprecatedAmbientDisplay = sPowerItemMap.get(POWER_AMBIENT_DISPLAY);
+        boolean legacy = false;
+        if (deprecatedAmbientDisplay != null && mNumDisplays == 0) {
+            final String key = getOrdinalPowerType(POWER_GROUP_DISPLAY_AMBIENT, 0);
+            Slog.w(TAG, POWER_AMBIENT_DISPLAY + " is deprecated! Use " + key + " instead.");
+            sPowerItemMap.put(key, deprecatedAmbientDisplay);
+            legacy = true;
+        }
+
+        final Double deprecatedScreenOn = sPowerItemMap.get(POWER_SCREEN_ON);
+        if (deprecatedScreenOn != null && mNumDisplays == 0) {
+            final String key = getOrdinalPowerType(POWER_GROUP_DISPLAY_SCREEN_ON, 0);
+            Slog.w(TAG, POWER_SCREEN_ON + " is deprecated! Use " + key + " instead.");
+            sPowerItemMap.put(key, deprecatedScreenOn);
+            legacy = true;
+        }
+
+        final Double deprecatedScreenFull = sPowerItemMap.get(POWER_SCREEN_FULL);
+        if (deprecatedScreenFull != null && mNumDisplays == 0) {
+            final String key = getOrdinalPowerType(POWER_GROUP_DISPLAY_SCREEN_FULL, 0);
+            Slog.w(TAG, POWER_SCREEN_FULL + " is deprecated! Use " + key + " instead.");
+            sPowerItemMap.put(key, deprecatedScreenFull);
+            legacy = true;
+        }
+        if (legacy) {
+            mNumDisplays = 1;
+        }
+    }
+
+    /**
+     * Returns the number built in displays on the device as defined in the power_profile.xml.
+     */
+    public int getNumDisplays() {
+        return mNumDisplays;
+    }
+
     /**
      * Returns the number of memory bandwidth buckets defined in power_profile.xml, or a
      * default value if the subsystem has no recorded value.
@@ -496,6 +587,32 @@
     }
 
     /**
+     * Returns the average current in mA consumed by an ordinaled subsystem, or the given
+     * default value if the subsystem has no recorded value.
+     *
+     * @param group        the subsystem {@link PowerGroup}.
+     * @param ordinal      which entity in the {@link PowerGroup}.
+     * @param defaultValue the value to return if the subsystem has no recorded value.
+     * @return the average current in milliAmps.
+     */
+    public double getAveragePowerForOrdinal(@PowerGroup String group, int ordinal,
+            double defaultValue) {
+        final String type = getOrdinalPowerType(group, ordinal);
+        return getAveragePowerOrDefault(type, defaultValue);
+    }
+
+    /**
+     * Returns the average current in mA consumed by an ordinaled subsystem.
+     *
+     * @param group        the subsystem {@link PowerGroup}.
+     * @param ordinal      which entity in the {@link PowerGroup}.
+     * @return the average current in milliAmps.
+     */
+    public double getAveragePowerForOrdinal(@PowerGroup String group, int ordinal) {
+        return getAveragePowerForOrdinal(group, ordinal, 0);
+    }
+
+    /**
      * Returns the battery capacity, if available, in milli Amp Hours. If not available,
      * it returns zero.
      *
@@ -682,4 +799,9 @@
             }
         }
     }
+
+    // Creates the key for an ordinaled power constant from the group and ordinal.
+    private static String getOrdinalPowerType(@PowerGroup String group, int ordinal) {
+        return group + ordinal;
+    }
 }
diff --git a/core/java/com/android/internal/os/ScreenPowerCalculator.java b/core/java/com/android/internal/os/ScreenPowerCalculator.java
index 1b3bc23..72ad4e7 100644
--- a/core/java/com/android/internal/os/ScreenPowerCalculator.java
+++ b/core/java/com/android/internal/os/ScreenPowerCalculator.java
@@ -16,6 +16,9 @@
 
 package com.android.internal.os;
 
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL;
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_ON;
+
 import android.os.BatteryConsumer;
 import android.os.BatteryStats;
 import android.os.BatteryUsageStats;
@@ -50,10 +53,11 @@
     }
 
     public ScreenPowerCalculator(PowerProfile powerProfile) {
+        // TODO(b/200239964): update to support multidisplay.
         mScreenOnPowerEstimator = new UsageBasedPowerEstimator(
-                powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_ON));
+                powerProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, 0));
         mScreenFullPowerEstimator = new UsageBasedPowerEstimator(
-                powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL));
+                powerProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, 0));
     }
 
     @Override
diff --git a/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl b/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl
index 8e454db..a8003a1 100644
--- a/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl
+++ b/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl
@@ -16,7 +16,7 @@
 package com.android.internal.policy;
 
 interface IKeyguardStateCallback {
-    void onShowingStateChanged(boolean showing);
+    void onShowingStateChanged(boolean showing, int userId);
     void onSimSecureStateChanged(boolean simSecure);
     void onInputRestrictedStateChanged(boolean inputRestricted);
     void onTrustedChanged(boolean trusted);
diff --git a/core/java/com/android/internal/protolog/ProtoLogGroup.java b/core/java/com/android/internal/protolog/ProtoLogGroup.java
index db019a67..954204f 100644
--- a/core/java/com/android/internal/protolog/ProtoLogGroup.java
+++ b/core/java/com/android/internal/protolog/ProtoLogGroup.java
@@ -84,6 +84,7 @@
             Consts.TAG_WM),
     WM_DEBUG_LAYER_MIRRORING(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
             Consts.TAG_WM),
+    WM_DEBUG_WALLPAPER(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, Consts.TAG_WM),
     TEST_GROUP(true, true, false, "WindowManagerProtoLogTest");
 
     private final boolean mEnabled;
diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
index 92031b4..49c32be 100644
--- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl
@@ -160,5 +160,5 @@
      */
     void suppressAmbientDisplay(boolean suppress);
 
-    int requestAddTile(in ComponentName componentName, in CharSequence label, in Icon icon, int userId, in IAddTileResultCallback callback);
+    void requestAddTile(in ComponentName componentName, in CharSequence label, in Icon icon, int userId, in IAddTileResultCallback callback);
 }
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index f212fc7..9e741e2 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -17,7 +17,6 @@
 package com.android.internal.view;
 
 import android.compat.annotation.UnsupportedAppUsage;
-import android.graphics.Point;
 import android.hardware.input.InputManager;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
@@ -62,10 +61,6 @@
     }
 
     @Override
-    public void locationInParentDisplayChanged(Point offset) {
-    }
-
-    @Override
     public void insetsChanged(InsetsState insetsState, boolean willMove, boolean willResize) {
     }
 
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 945adb7..1a1a8ba 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -218,6 +218,7 @@
                 "com_android_internal_os_KernelSingleProcessCpuThreadReader.cpp",
                 "com_android_internal_os_KernelSingleUidTimeReader.cpp",
                 "com_android_internal_os_LongArrayMultiStateCounter.cpp",
+                "com_android_internal_os_LongMultiStateCounter.cpp",
                 "com_android_internal_os_Zygote.cpp",
                 "com_android_internal_os_ZygoteCommandBuffer.cpp",
                 "com_android_internal_os_ZygoteInit.cpp",
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 91179c2..5c9b6df 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -205,6 +205,7 @@
 extern int register_com_android_internal_os_KernelSingleProcessCpuThreadReader(JNIEnv* env);
 extern int register_com_android_internal_os_KernelSingleUidTimeReader(JNIEnv *env);
 extern int register_com_android_internal_os_LongArrayMultiStateCounter(JNIEnv* env);
+extern int register_com_android_internal_os_LongMultiStateCounter(JNIEnv* env);
 extern int register_com_android_internal_os_Zygote(JNIEnv *env);
 extern int register_com_android_internal_os_ZygoteCommandBuffer(JNIEnv *env);
 extern int register_com_android_internal_os_ZygoteInit(JNIEnv *env);
@@ -1588,6 +1589,7 @@
         REG_JNI(register_com_android_internal_net_NetworkUtilsInternal),
         REG_JNI(register_com_android_internal_os_ClassLoaderFactory),
         REG_JNI(register_com_android_internal_os_LongArrayMultiStateCounter),
+        REG_JNI(register_com_android_internal_os_LongMultiStateCounter),
         REG_JNI(register_com_android_internal_os_Zygote),
         REG_JNI(register_com_android_internal_os_ZygoteCommandBuffer),
         REG_JNI(register_com_android_internal_os_ZygoteInit),
diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp
index aadd320..0d33807 100644
--- a/core/jni/android_os_Parcel.cpp
+++ b/core/jni/android_os_Parcel.cpp
@@ -596,13 +596,10 @@
                                           jlong otherNativePtr)
 {
     Parcel* thisParcel = reinterpret_cast<Parcel*>(thisNativePtr);
-    if (thisParcel == NULL) {
-       return 0;
-    }
+    LOG_ALWAYS_FATAL_IF(thisParcel == nullptr, "Should not be null");
+
     Parcel* otherParcel = reinterpret_cast<Parcel*>(otherNativePtr);
-    if (otherParcel == NULL) {
-       return thisParcel->getOpenAshmemSize();
-    }
+    LOG_ALWAYS_FATAL_IF(otherParcel == nullptr, "Should not be null");
 
     return thisParcel->compareData(*otherParcel);
 }
@@ -638,6 +635,22 @@
     return ret;
 }
 
+static jboolean android_os_Parcel_hasFileDescriptorsInRange(JNIEnv* env, jclass clazz,
+                                                            jlong nativePtr, jint offset,
+                                                            jint length) {
+    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
+    if (parcel != NULL) {
+        bool result;
+        status_t err = parcel->hasFileDescriptorsInRange(offset, length, result);
+        if (err != NO_ERROR) {
+            signalExceptionForError(env, clazz, err);
+            return JNI_FALSE;
+        }
+        return result ? JNI_TRUE : JNI_FALSE;
+    }
+    return JNI_FALSE;
+}
+
 // String tries to allocate itself on the stack, within a known size, but will
 // make a heap allocation if not.
 template <size_t StackReserve>
@@ -727,11 +740,11 @@
     return Parcel::getGlobalAllocCount();
 }
 
-static jlong android_os_Parcel_getBlobAshmemSize(jlong nativePtr)
+static jlong android_os_Parcel_getOpenAshmemSize(jlong nativePtr)
 {
     Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
     if (parcel != NULL) {
-        return parcel->getBlobAshmemSize();
+        return parcel->getOpenAshmemSize();
     }
     return 0;
 }
@@ -831,6 +844,7 @@
     {"nativeAppendFrom",          "(JJII)V", (void*)android_os_Parcel_appendFrom},
     // @CriticalNative
     {"nativeHasFileDescriptors",  "(J)Z", (void*)android_os_Parcel_hasFileDescriptors},
+    {"nativeHasFileDescriptorsInRange",  "(JII)Z", (void*)android_os_Parcel_hasFileDescriptorsInRange},
     {"nativeWriteInterfaceToken", "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeInterfaceToken},
     {"nativeEnforceInterface",    "(JLjava/lang/String;)V", (void*)android_os_Parcel_enforceInterface},
 
@@ -838,7 +852,7 @@
     {"getGlobalAllocCount",       "()J", (void*)android_os_Parcel_getGlobalAllocCount},
 
     // @CriticalNative
-    {"nativeGetBlobAshmemSize",       "(J)J", (void*)android_os_Parcel_getBlobAshmemSize},
+    {"nativeGetOpenAshmemSize",       "(J)J", (void*)android_os_Parcel_getOpenAshmemSize},
 
     // @CriticalNative
     {"nativeReadCallingWorkSourceUid", "(J)I", (void*)android_os_Parcel_readCallingWorkSourceUid},
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index dae844f..3bc3336 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -1870,16 +1870,12 @@
     if (surface == nullptr) {
         return;
     }
-    surface->setTransformHint(
-            ui::Transform::toRotationFlags(static_cast<ui::Rotation>(transformHint)));
+    surface->setTransformHint(transformHint);
 }
 
 static jint nativeGetTransformHint(JNIEnv* env, jclass clazz, jlong nativeSurfaceControl) {
     sp<SurfaceControl> surface(reinterpret_cast<SurfaceControl*>(nativeSurfaceControl));
-    ui::Transform::RotationFlags transformHintRotationFlags =
-            static_cast<ui::Transform::RotationFlags>(surface->getTransformHint());
-
-    return toRotationInt(ui::Transform::toRotation((transformHintRotationFlags)));
+    return surface->getTransformHint();
 }
 
 static jint nativeGetLayerId(JNIEnv* env, jclass clazz, jlong nativeSurfaceControl) {
diff --git a/core/jni/com_android_internal_os_LongMultiStateCounter.cpp b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp
new file mode 100644
index 0000000..9ffc757
--- /dev/null
+++ b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <android/binder_parcel.h>
+#include <android/binder_parcel_jni.h>
+#include <android/binder_parcel_utils.h>
+#include <android_runtime/Log.h>
+
+#include <cstring>
+
+#include "MultiStateCounter.h"
+#include "core_jni_helpers.h"
+
+namespace android {
+
+namespace battery {
+
+typedef battery::MultiStateCounter<int64_t> LongMultiStateCounter;
+
+template <>
+bool LongMultiStateCounter::delta(const int64_t &previousValue, const int64_t &newValue,
+                                  int64_t *outValue) const {
+    *outValue = newValue - previousValue;
+    return *outValue >= 0;
+}
+
+template <>
+void LongMultiStateCounter::add(int64_t *value1, const int64_t &value2, const uint64_t numerator,
+                                const uint64_t denominator) const {
+    if (numerator != denominator) {
+        // The caller ensures that denominator != 0
+        *value1 += value2 * numerator / denominator;
+    } else {
+        *value1 += value2;
+    }
+}
+
+template <>
+std::string LongMultiStateCounter::valueToString(const int64_t &v) const {
+    return std::to_string(v);
+}
+
+} // namespace battery
+
+static inline battery::LongMultiStateCounter *asLongMultiStateCounter(const jlong nativePtr) {
+    return reinterpret_cast<battery::LongMultiStateCounter *>(nativePtr);
+}
+
+static jlong native_init(jint stateCount) {
+    battery::LongMultiStateCounter *counter = new battery::LongMultiStateCounter(stateCount, 0);
+    return reinterpret_cast<jlong>(counter);
+}
+
+static void native_dispose(void *nativePtr) {
+    delete reinterpret_cast<battery::LongMultiStateCounter *>(nativePtr);
+}
+
+static jlong native_getReleaseFunc() {
+    return reinterpret_cast<jlong>(native_dispose);
+}
+
+static void native_setEnabled(jlong nativePtr, jboolean enabled, jlong timestamp) {
+    asLongMultiStateCounter(nativePtr)->setEnabled(enabled, timestamp);
+}
+
+static void native_setState(jlong nativePtr, jint state, jlong timestamp) {
+    asLongMultiStateCounter(nativePtr)->setState(state, timestamp);
+}
+
+static void native_updateValue(jlong nativePtr, jlong value, jlong timestamp) {
+    asLongMultiStateCounter(nativePtr)->updateValue((int64_t)value, timestamp);
+}
+
+static void native_addCount(jlong nativePtr, jlong count) {
+    asLongMultiStateCounter(nativePtr)->addValue(count);
+}
+
+static void native_reset(jlong nativePtr) {
+    asLongMultiStateCounter(nativePtr)->reset();
+}
+
+static jlong native_getCount(jlong nativePtr, jint state) {
+    return asLongMultiStateCounter(nativePtr)->getCount(state);
+}
+
+static jobject native_toString(JNIEnv *env, jobject self, jlong nativePtr) {
+    return env->NewStringUTF(asLongMultiStateCounter(nativePtr)->toString().c_str());
+}
+
+static void throwWriteRE(JNIEnv *env, binder_status_t status) {
+    ALOGE("Could not write LongMultiStateCounter to Parcel, status = %d", status);
+    jniThrowRuntimeException(env, "Could not write LongMultiStateCounter to Parcel");
+}
+
+#define THROW_ON_WRITE_ERROR(expr)     \
+    {                                  \
+        binder_status_t status = expr; \
+        if (status != STATUS_OK) {     \
+            throwWriteRE(env, status); \
+        }                              \
+    }
+
+static void native_writeToParcel(JNIEnv *env, jobject self, jlong nativePtr, jobject jParcel,
+                                 jint flags) {
+    battery::LongMultiStateCounter *counter = asLongMultiStateCounter(nativePtr);
+    AParcel *parcel = AParcel_fromJavaParcel(env, jParcel);
+
+    uint16_t stateCount = counter->getStateCount();
+    THROW_ON_WRITE_ERROR(AParcel_writeInt32(parcel, stateCount));
+
+    for (battery::state_t state = 0; state < stateCount; state++) {
+        THROW_ON_WRITE_ERROR(AParcel_writeInt64(parcel, counter->getCount(state)));
+    }
+}
+
+static void throwReadRE(JNIEnv *env, binder_status_t status) {
+    ALOGE("Could not read LongMultiStateCounter from Parcel, status = %d", status);
+    jniThrowRuntimeException(env, "Could not read LongMultiStateCounter from Parcel");
+}
+
+#define THROW_ON_READ_ERROR(expr)      \
+    {                                  \
+        binder_status_t status = expr; \
+        if (status != STATUS_OK) {     \
+            throwReadRE(env, status);  \
+        }                              \
+    }
+
+static jlong native_initFromParcel(JNIEnv *env, jclass theClass, jobject jParcel) {
+    AParcel *parcel = AParcel_fromJavaParcel(env, jParcel);
+
+    int32_t stateCount;
+    THROW_ON_READ_ERROR(AParcel_readInt32(parcel, &stateCount));
+
+    battery::LongMultiStateCounter *counter = new battery::LongMultiStateCounter(stateCount, 0);
+
+    for (battery::state_t state = 0; state < stateCount; state++) {
+        int64_t value;
+        THROW_ON_READ_ERROR(AParcel_readInt64(parcel, &value));
+        counter->setValue(state, value);
+    }
+
+    return reinterpret_cast<jlong>(counter);
+}
+
+static jint native_getStateCount(jlong nativePtr) {
+    return asLongMultiStateCounter(nativePtr)->getStateCount();
+}
+
+static const JNINativeMethod g_methods[] = {
+        // @CriticalNative
+        {"native_init", "(I)J", (void *)native_init},
+        // @CriticalNative
+        {"native_getReleaseFunc", "()J", (void *)native_getReleaseFunc},
+        // @CriticalNative
+        {"native_setEnabled", "(JZJ)V", (void *)native_setEnabled},
+        // @CriticalNative
+        {"native_setState", "(JIJ)V", (void *)native_setState},
+        // @CriticalNative
+        {"native_updateValue", "(JJJ)V", (void *)native_updateValue},
+        // @CriticalNative
+        {"native_addCount", "(JJ)V", (void *)native_addCount},
+        // @CriticalNative
+        {"native_reset", "(J)V", (void *)native_reset},
+        // @CriticalNative
+        {"native_getCount", "(JI)J", (void *)native_getCount},
+        // @FastNative
+        {"native_toString", "(J)Ljava/lang/String;", (void *)native_toString},
+        // @FastNative
+        {"native_writeToParcel", "(JLandroid/os/Parcel;I)V", (void *)native_writeToParcel},
+        // @FastNative
+        {"native_initFromParcel", "(Landroid/os/Parcel;)J", (void *)native_initFromParcel},
+        // @CriticalNative
+        {"native_getStateCount", "(J)I", (void *)native_getStateCount},
+};
+
+int register_com_android_internal_os_LongMultiStateCounter(JNIEnv *env) {
+    return RegisterMethodsOrDie(env, "com/android/internal/os/LongMultiStateCounter", g_methods,
+                                NELEM(g_methods));
+}
+
+} // namespace android
diff --git a/core/proto/OWNERS b/core/proto/OWNERS
index 44ea23f..78650ed 100644
--- a/core/proto/OWNERS
+++ b/core/proto/OWNERS
@@ -8,9 +8,6 @@
 yro@google.com
 zhouwenjie@google.com
 
-# Settings UI
-per-file settings_enums.proto=tmfang@google.com
-
 # Frameworks
 ogunwale@google.com
 jjaggi@google.com
diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto
index 8e33561..bc6a090 100644
--- a/core/proto/android/server/windowmanagerservice.proto
+++ b/core/proto/android/server/windowmanagerservice.proto
@@ -69,6 +69,7 @@
     // know what activity types to check for when invoking splitscreen multi-window.
     optional bool is_home_recents_component = 6;
     repeated IdentifierProto pending_activities = 7 [deprecated=true];
+    optional int32 default_min_size_resizable_task = 8;
 }
 
 message BarControllerProto {
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c8d70a8..d0910fc 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -254,6 +254,8 @@
         android:name="com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY" />
     <protected-broadcast
         android:name="android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED" />
+    <protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED" />
+    <protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED" />
     <protected-broadcast
         android:name="android.bluetooth.action.TETHERING_STATE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.pbap.profile.action.CONNECTION_STATE_CHANGED" />
@@ -707,7 +709,7 @@
     <protected-broadcast android:name="android.scheduling.action.REBOOT_READY" />
     <protected-broadcast android:name="android.app.action.DEVICE_POLICY_CONSTANTS_CHANGED" />
     <protected-broadcast android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
-    <protected-broadcast android:name="android.app.action.ACTION_SHOW_NEW_USER_DISCLAIMER" />
+    <protected-broadcast android:name="android.app.action.SHOW_NEW_USER_DISCLAIMER" />
 
     <!-- ====================================================================== -->
     <!--                          RUNTIME PERMISSIONS                           -->
@@ -751,7 +753,16 @@
         android:permissionGroup="android.permission-group.UNDEFINED"
         android:label="@string/permlab_writeContacts"
         android:description="@string/permdesc_writeContacts"
-        android:protectionLevel="dangerous" />
+      android:protectionLevel="dangerous" />
+
+    <!-- Allows an application to set default account for new contacts.
+        <p> This permission is only granted to system applications fulfilling the Contacts app role.
+        <p>Protection level: internal|role
+        @SystemApi
+        @hide
+    -->
+    <permission android:name="android.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS"
+        android:protectionLevel="internal|role" />
 
     <!-- ====================================================================== -->
     <!-- Permissions for accessing user's calendar                              -->
@@ -2000,6 +2011,14 @@
         android:label="@string/permlab_uwb_ranging"
         android:protectionLevel="dangerous" />
 
+    <!-- Required to be able to advertise and connect to nearby devices via Wi-Fi.
+         <p>Protection level: dangerous -->
+    <permission android:name="android.permission.NEARBY_WIFI_DEVICES"
+                android:permissionGroup="android.permission-group.UNDEFINED"
+                android:description="@string/permdesc_nearby_wifi_devices"
+                android:label="@string/permlab_nearby_wifi_devices"
+                android:protectionLevel="dangerous" />
+
     <!-- @SystemApi @TestApi Allows an application to suspend other apps, which will prevent the
          user from using them until they are unsuspended.
          @hide
@@ -4483,6 +4502,12 @@
     <permission android:name="android.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE"
                 android:protectionLevel="signature" />
 
+    <!-- Allows an application to modify the user preferred display mode.
+         @hide
+         @TestApi -->
+    <permission android:name="android.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE"
+                android:protectionLevel="signature" />
+
     <!-- @SystemApi Allows an application to control VPN.
          <p>Not for use by third-party applications.</p>
          @hide -->
@@ -5381,6 +5406,12 @@
     <permission android:name="android.permission.VIEW_INSTANT_APPS"
                 android:protectionLevel="signature|preinstalled" />
 
+    <!-- Allows an application to interact with the currently active
+        {@link com.android.server.communal.CommunalManagerService}.
+        @hide -->
+    <permission android:name="android.permission.WRITE_COMMUNAL_STATE"
+                android:protectionLevel="signature" />
+
     <!-- Allows the holder to manage whether the system can bind to services
          provided by instant apps. This permission is intended to protect
          test/development fucntionality and should be used only in such cases.
@@ -6087,6 +6118,12 @@
                 android:process=":ui">
         </activity>
 
+        <activity android:name="com.android.internal.app.LaunchAfterAuthenticationActivity"
+                  android:theme="@style/Theme.Translucent.NoTitleBar"
+                  android:excludeFromRecents="true"
+                  android:process=":ui">
+        </activity>
+
         <activity android:name="com.android.settings.notification.NotificationAccessConfirmationActivity"
                   android:theme="@style/Theme.Dialog.Confirmation"
                   android:excludeFromRecents="true">
diff --git a/core/res/res/values-am/strings.xml b/core/res/res/values-am/strings.xml
index 5636137..21c21a9 100644
--- a/core/res/res/values-am/strings.xml
+++ b/core/res/res/values-am/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"የስልክ ጥሪዎች ያድርጉ እና ያስተዳድሩ"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"የሰውነት ዳሳሾች"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"ስለአስፈላጊ ምልክቶችዎ ያሉ የዳሳሽ ውሂብ ይድረሱ"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"ማሳወቂያዎች"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"ማሳወቂያዎች አሳይ"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"የመስኮት ይዘት ሰርስረው ያውጡ"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"መስተጋበር የሚፈጥሩት የመስኮት ይዘት ይመርምሩ።"</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"በመንካት ያስሱን ያብሩ"</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"መተግበሪያው መቆለፊያውና ማንኛውም የተጎዳኘ የይለፍ ቃል ደህንነት እንዲያሰናክል ይፈቅድለታል። ለምሳሌ ስልኩ ገቢ የስልክ ጥሪ በሚቀበልበት ጊዜ መቆለፊያውን ያሰናክልና ከዚያም ጥሪው ሲጠናቀቅ መቆለፊያውን በድጋሚ ያነቃዋል።"</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"የማያ ገጽ መቆለፊያ ውስብስብነትን ጠይቅ"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"መተግበሪያው የማያ ገጽ መቆለፊያው ውስብስብነት ደረጃ (ከፍተኛ፣ መካከለኛ፣ ዝቅተኛ ወይም ምንም) እንዲያውቅ ያስችለዋል፣ ይህም ሊሆኑ የሚችለው የማያ ገጽ መቆለፊያው ርዝመት እና ዓይነት ክልል ያመለክታል። መተግበሪያው እንዲሁም ለተጠቃሚዎች የማያ ገጽ መቆለፊያውን ወደተወሰነ ደረጃ እንዲያዘምኑት ሊጠቁማቸው ይችላል። የማያ ገጽ መቆለፊያው በስነጣ አልባ ጽሑፍ እንደማይከማች ልብ ይበሉ፣ በዚህም መተግበሪያው ትክክለኛውን የይለፍ ቃል አያውቅም።"</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"ማሳወቂያዎች አሳይ"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"መተግበሪያው ማሳወቂያዎችን እንዲያሳይ ያስችለዋል"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"ባዮሜትራዊ ሃርድዌርን መጠቀም"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"መተግበሪያው የባዮሜትራዊ ሃርድዌር ለማረጋገጥ ስራ እንዲጠቀም ያስችለዋል"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"የጣት አሻራ ሃርድዌርን አስተዳድር"</string>
diff --git a/core/res/res/values-ar/strings.xml b/core/res/res/values-ar/strings.xml
index 1aaa75e..1b67328 100644
--- a/core/res/res/values-ar/strings.xml
+++ b/core/res/res/values-ar/strings.xml
@@ -338,10 +338,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"إجراء مكالمات هاتفية وإدارتها"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"أجهزة استشعار الجسم"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"الوصول إلى بيانات المستشعر حول علاماتك الحيوية"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"الإشعارات"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"عرض الإشعارات"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"استرداد محتوى النافذة"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"فحص محتوى نافذة يتم التفاعل معها"</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"تفعيل الاستكشاف باللمس"</string>
@@ -565,10 +563,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"للسماح للتطبيق بإيقاف تأمين المفاتيح وأي أمان لكلمة مرور مرتبطة. على سبيل المثال، يعطل الهاتف تأمين المفاتيح عند استقبال مكالمة هاتفية واردة، ثم يعيد تفعيل تأمين المفاتيح عند انتهاء المكالمة."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"طلب معرفة مستوى صعوبة قفل الشاشة"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"للسماح للتطبيق بمعرفة مستوى صعوبة قفل الشاشة (عالي أو متوسط أو منخفض الصعوبة أو بدون)، والذي يحدّد النطاق المحتمل لطول ونوع قفل الشاشة. ويمكن أن يقترح التطبيق للمستخدمين أيضًا تعديل قفل الشاشة إلى مستوى معيّن، ولهم مطلق الحرية في تجاهل هذا الاقتراح ورفضه. وتجدر الإشارة إلى أنه لا يتم حفظ قفل الشاشة في نص عادي، لذا لا يعرف التطبيق كلمة المرور تحديدًا."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"عرض الإشعارات"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"السماح للتطبيق بعرض إشعارات"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"استخدام الأجهزة البيومترية"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"للسماح للتطبيق باستخدام الأجهزة البيومترية للمصادقة"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"لإدارة أجهزة بصمة الإصبع"</string>
diff --git a/core/res/res/values-as-television/strings.xml b/core/res/res/values-as-television/strings.xml
new file mode 100644
index 0000000..7585d9f
--- /dev/null
+++ b/core/res/res/values-as-television/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="sensor_privacy_start_use_mic_notification_content_title" msgid="7002619958660406548">"মাইক্ৰ’ফ’ন অৱৰোধ কৰি থোৱা আছে"</string>
+    <string name="sensor_privacy_start_use_camera_notification_content_title" msgid="2131954635322568179">"কেমেৰা অৱৰোধ কৰি থোৱা আছে"</string>
+</resources>
diff --git a/core/res/res/values-bg/strings.xml b/core/res/res/values-bg/strings.xml
index 2b057505..1b963f6 100644
--- a/core/res/res/values-bg/strings.xml
+++ b/core/res/res/values-bg/strings.xml
@@ -2102,7 +2102,7 @@
     <string name="notification_feedback_indicator_promoted" msgid="9030204303764698640">"Това известие бе класирано по-високо. Докоснете, за да изпратите отзиви."</string>
     <string name="notification_feedback_indicator_demoted" msgid="8880309924296450875">"Това известие бе класирано по-ниско. Докоснете, за да изпратите отзиви."</string>
     <string name="nas_upgrade_notification_title" msgid="8436359459300146555">"Подобрени известия"</string>
-    <string name="nas_upgrade_notification_content" msgid="5157550369837103337">"Предложените действия и отговори вече се предоставят от функцията за подобрени известия. Адаптивните известия за Android вече не се поддържат."</string>
+    <string name="nas_upgrade_notification_content" msgid="5157550369837103337">"Предложените действия и отговори вече се предоставят от функцията за подо­брени известия. Адаптивните известия за Android вече не се поддържат."</string>
     <string name="nas_upgrade_notification_enable_action" msgid="3046406808378726874">"OK"</string>
     <string name="nas_upgrade_notification_disable_action" msgid="3794833210043497982">"Изключване"</string>
     <string name="nas_upgrade_notification_learn_more_action" msgid="7011130656195423947">"Научете повече"</string>
diff --git a/core/res/res/values-bn-television/strings.xml b/core/res/res/values-bn-television/strings.xml
new file mode 100644
index 0000000..a2620e8
--- /dev/null
+++ b/core/res/res/values-bn-television/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="sensor_privacy_start_use_mic_notification_content_title" msgid="7002619958660406548">"মাইক্রোফোন ব্লক করা আছে"</string>
+    <string name="sensor_privacy_start_use_camera_notification_content_title" msgid="2131954635322568179">"ক্যামেরা ব্লক করা আছে"</string>
+</resources>
diff --git a/core/res/res/values-bn/strings.xml b/core/res/res/values-bn/strings.xml
index 04c8bd5..0f47e29 100644
--- a/core/res/res/values-bn/strings.xml
+++ b/core/res/res/values-bn/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"ফোন কলগুলি এবং পরিচালনা"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"বডি সেন্সর"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"আপনার অত্যাবশ্যক লক্ষণগুলির সম্পর্কে সেন্সর ডেটা অ্যাক্সেস করে"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"বিজ্ঞপ্তি"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"বিজ্ঞপ্তি দেখুন"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"উইন্ডোর কন্টেন্ট পুনরুদ্ধার করে"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"ব্যবহার করছেন এমন একটি উইন্ডোর কন্টেন্ট নিরীক্ষণ করে৷"</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"স্পর্শের মাধ্যমে অন্বেষণ করা চালু করুন"</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"কী-লক এবং যেকোনো সংশ্লিষ্ট পাসওয়ার্ড সুরক্ষা অক্ষম করতে অ্যাপ্লিকেশানটিকে মঞ্জুর করে৷ উদাহরণস্বরূপ, একটি ইনকামিং ফোন কল গ্রহণ করার সময়ে ফোনটি কী-লক অক্ষম করে, তারপরে কল শেষ হয়ে গেলে কী-লকটিকে আবার সক্ষম করে৷"</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"স্ক্রিন লকের জটিলতা জানার অনুরোধ করুন"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"এটি অ্যাপটিকে স্ক্রিন লকের জটিলতার লেভেল বুঝতে সাহায্য করে (খুব বেশি, মাঝারি, অল্প জটিল বা কোনও জটিলতা নেই), যা স্ক্রিন লকটি সম্ভবত কত দীর্ঘ ও সেটির ধরন কীরকম, তার ইঙ্গিত দেয়। এই অ্যাপটি একটি নির্দিষ্ট লেভেল পর্যন্ত স্ক্রিন লক আপডেট করার সাজেশনও দিতে পারে, তবে ব্যবহারকারী তা উপেক্ষা করে অন্য কোথাও চলে যেতে পারেন। মনে রাখবেন যে স্ক্রিন লক প্লেন টেক্সট হিসেবে সংরক্ষণ করা হয় না, তাই অ্যাপ কখনও আসল পাসওয়ার্ড জানতে পারে না।"</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"বিজ্ঞপ্তি দেখুন"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"অ্যাপকে বিজ্ঞপ্তি দেখানোর জন্য অনুমতি দেয়"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"বায়োমেট্রিক হার্ডওয়্যার ব্যবহার করুন"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"অ্যাপটিকে যাচাইকরণের জন্য বায়োমেট্রিক হার্ডওয়্যার ব্যবহার করার অনুমতি দেয়"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"আঙ্গুলের ছাপ নেওয়ার হার্ডওয়্যার পরিচালনা করুন"</string>
diff --git a/core/res/res/values-de/strings.xml b/core/res/res/values-de/strings.xml
index 1bd7970..368961b 100644
--- a/core/res/res/values-de/strings.xml
+++ b/core/res/res/values-de/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"Telefonanrufe tätigen und verwalten"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"Körpersensoren"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"auf Sensordaten zu deinen Vitaldaten zugreifen"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"Benachrichtigungen"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"Benachrichtigungen anzeigen"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"Fensterinhalte abrufen"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"Die Inhalte eines Fensters, mit dem du interagierst, werden abgerufen."</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"\"Tippen &amp; Entdecken\" aktivieren"</string>
@@ -340,7 +338,7 @@
     <string name="capability_desc_canControlMagnification" msgid="2206586716709254805">"Legt die Zoom-Stufe und -Position auf dem Display fest."</string>
     <string name="capability_title_canPerformGestures" msgid="9106545062106728987">"Touch-Gesten möglich"</string>
     <string name="capability_desc_canPerformGestures" msgid="6619457251067929726">"Tippen, Wischen, Zusammenziehen und andere Touch-Gesten möglich."</string>
-    <string name="capability_title_canCaptureFingerprintGestures" msgid="1189053104594608091">"Gesten auf dem Fingerabdrucksensor"</string>
+    <string name="capability_title_canCaptureFingerprintGestures" msgid="1189053104594608091">"Gesten auf dem Fin­gerabdrucksensor"</string>
     <string name="capability_desc_canCaptureFingerprintGestures" msgid="6861869337457461274">"Erfasst Touch-Gesten auf dem Fingerabdrucksensor des Geräts."</string>
     <string name="capability_title_canTakeScreenshot" msgid="3895812893130071930">"Screenshot erstellen"</string>
     <string name="capability_desc_canTakeScreenshot" msgid="7762297374317934052">"Es kann ein Screenshot des Displays erstellt werden."</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"Ermöglicht der App, die Tastensperre sowie den damit verbundenen Passwortschutz zu deaktivieren. Das Telefon deaktiviert die Tastensperre beispielsweise, wenn ein Anruf eingeht, und aktiviert sie wieder, nachdem das Gespräch beendet wurde."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"Komplexitätsstufe der Displaysperre anfragen"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"Ermöglicht es der App, die Komplexitätsstufe der Displaysperre (hoch, mittel, niedrig oder keine) zu ermitteln, was auf Art und mögliche Dauer der Displaysperre hinweist. Die App kann Nutzern auch vorschlagen, die Displaysperre auf eine bestimmte Stufe zu setzen. Nutzer können diesen Vorschlag jedoch einfach ignorieren und fortfahren. Beachten Sie, dass die Displaysperre nicht im Klartext gespeichert ist, sodass die App nicht das genaue Passwort kennt."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"Benachrichtigungen anzeigen"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"Ermöglicht der App, Benachrichtigungen anzuzeigen"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"Biometrische Hardware verwenden"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"Erlaubt der App, biometrische Hardware zur Authentifizierung zu verwenden"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"Fingerabdruckhardware verwalten"</string>
diff --git a/core/res/res/values-fr-rCA-television/strings.xml b/core/res/res/values-fr-rCA-television/strings.xml
new file mode 100644
index 0000000..3187a48
--- /dev/null
+++ b/core/res/res/values-fr-rCA-television/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="sensor_privacy_start_use_mic_notification_content_title" msgid="7002619958660406548">"Le microphone est bloqué"</string>
+    <string name="sensor_privacy_start_use_camera_notification_content_title" msgid="2131954635322568179">"La caméra est bloquée"</string>
+</resources>
diff --git a/core/res/res/values-fr-rCA/strings.xml b/core/res/res/values-fr-rCA/strings.xml
index a8023cc..43d6c09 100644
--- a/core/res/res/values-fr-rCA/strings.xml
+++ b/core/res/res/values-fr-rCA/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"faire et gérer des appels téléphoniques"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"Capteurs corporels"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"accéder aux données des capteurs sur vos signes vitaux"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"Notifications"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"afficher les notifications"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"Récupérer le contenu d\'une fenêtre"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"Inspecter le contenu d\'une fenêtre avec laquelle vous interagissez."</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"Activer la fonctionnalité Explorer au toucher"</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"Permet à l\'application de désactiver le verrouillage des touches et toute mesure de sécurité par mot de passe associée. Par exemple, votre téléphone désactive le verrouillage des touches lorsque vous recevez un appel, puis le réactive lorsque vous raccrochez."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"demander la complexité du verrouillage d\'écran"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"Autorise l\'application à apprendre le niveau de complexité de l\'écran de verrouillage (élevé, moyen, faible ou aucun), qui indique la gamme possible de longueur et de type de verrouillage d\'écran. L\'application peut aussi suggérer aux utilisateurs de mettre à jour l\'écran de verrouillage afin d\'utiliser un certain niveau de complexité, mais ils peuvent ignorer la suggestion. Notez que le verrouillage d\'écran n\'est pas stocké en texte brut pour de manière à ce que l\'application n\'ait pas accès au mot de passe exact."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"afficher les notifications"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"Permet à l\'application d\'afficher les notifications"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"utiliser le matériel biométrique"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"Permet à l\'application d\'utiliser du matériel biométrique pour l\'authentification"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"gérer le matériel d\'empreinte digitale"</string>
diff --git a/core/res/res/values-fr-television/strings.xml b/core/res/res/values-fr-television/strings.xml
new file mode 100644
index 0000000..2e357e6
--- /dev/null
+++ b/core/res/res/values-fr-television/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="sensor_privacy_start_use_mic_notification_content_title" msgid="7002619958660406548">"Le micro est bloqué"</string>
+    <string name="sensor_privacy_start_use_camera_notification_content_title" msgid="2131954635322568179">"La caméra est bloquée"</string>
+</resources>
diff --git a/core/res/res/values-gu-television/strings.xml b/core/res/res/values-gu-television/strings.xml
new file mode 100644
index 0000000..ff36196
--- /dev/null
+++ b/core/res/res/values-gu-television/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="sensor_privacy_start_use_mic_notification_content_title" msgid="7002619958660406548">"માઇક્રોફોનને બ્લૉક કરવામાં આવ્યો છે"</string>
+    <string name="sensor_privacy_start_use_camera_notification_content_title" msgid="2131954635322568179">"કૅમેરાને બ્લૉક કરવામાં આવ્યો છે"</string>
+</resources>
diff --git a/core/res/res/values-gu/strings.xml b/core/res/res/values-gu/strings.xml
index 02b1787..5b0682b 100644
--- a/core/res/res/values-gu/strings.xml
+++ b/core/res/res/values-gu/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"ફોન કૉલ કરો અને મેનેજ કરો"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"બૉડી સેન્સર"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"તમારા મહત્વપૂર્ણ ચિહ્નો વિશે સેન્સર ડેટા ઍક્સેસ કરો"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"નોટિફિકેશન"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"નોટિફિકેશન બતાવો"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"વિંડો કન્ટેન્ટ પુનઃપ્રાપ્ત કરો"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"તમે જેની સાથે ક્રિયા-પ્રતિક્રિયા કરી રહ્યાં છો તે વિંડોનું કન્ટેન્ટ તપાસો."</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"સ્પર્શ કરીને શોધખોળ કરવું ચાલુ કરો"</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"એપ્લિકેશનને કીલૉક અને કોઈપણ સંકળાયેલ પાસવર્ડ સુરક્ષા અક્ષમ કરવાની મંજૂરી આપે છે. ઉદાહરણ તરીકે, ઇનકમિંગ ફોન કૉલ પ્રાપ્ત કરતી વખતે ફોન, કીલૉકને અક્ષમ કરે છે, પછી કૉલ સમાપ્ત થઈ જવા પર કીલૉક ફરીથી સક્ષમ કરે છે."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"સ્ક્રીન લૉકની જટિલતા જાણવા માટે વિનંતી કરો"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"ઍપને સ્ક્રીન લૉકની જટિલતાનું લેવલ (ઊંચું, મધ્યમ, નીચું અથવા કોઈ નહીં) જાણવાની મંજૂરી આપે છે, જે સ્ક્રીન લૉકના પ્રકાર અને લંબાઈની સંભવિત શ્રેણી સૂચવે છે. ઍપ વપરાશકર્તાઓને સ્ક્રીન લૉકને ચોક્કસ લેવલ સુધી અપડેટ કરવાનું સૂચન પણ કરી શકે છે, પરંતુ વપરાશકર્તાઓ મુક્ત રીતે અવગણીને નૅવિગેટ કરી શકે છે. નોંધી લો કે સ્ક્રીન લૉકનો plaintextમાં સંગ્રહ કરવામાં આવતો નથી, તેથી ઍપને ચોક્કસ પાસવર્ડની જાણ હોતી નથી."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"નોટિફિકેશન બતાવો"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"ઍપને નોટિફિકેશન બતાવવાની મંજૂરી આપે છે"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"બાયોમેટ્રિક હાર્ડવેરનો ઉપયોગ કરો"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"ઍપને પ્રમાણીકરણ માટે બાયોમેટ્રિક હાર્ડવેરનો ઉપયોગ કરવાની મંજૂરી આપે છે"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"ફિંગરપ્રિન્ટ હાર્ડવેરને મેનેજ કરો"</string>
diff --git a/core/res/res/values-iw/strings.xml b/core/res/res/values-iw/strings.xml
index 3b55118..52539e0 100644
--- a/core/res/res/values-iw/strings.xml
+++ b/core/res/res/values-iw/strings.xml
@@ -332,10 +332,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"ביצוע וניהול של שיחות טלפון"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"חיישנים גופניים"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"גישה אל נתוני חיישנים של הסימנים החיוניים שלך"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"התראות"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"הצגת התראות"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"אחזור תוכן של חלון"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"בדיקת התוכן של חלון שאיתו מתבצעת אינטראקציה."</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"הפעלה של \'גילוי באמצעות מגע\'"</string>
@@ -559,10 +557,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"מאפשרת לאפליקציה להשבית את נעילת המקשים וכל אמצעי אבטחה משויך המבוסס על סיסמה. לדוגמה, הטלפון ישבית את נעילת המקשים במהלך שיחת טלפון נכנסת, ויפעיל מחדש את נעילת המקשים עם סיום השיחה."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"בקשת מידע לגבי מידת המורכבות של נעילת המסך"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"מאפשרת לאפליקציה ללמוד את רמת המורכבות של נעילת המסך (גבוהה, בינונית, נמוכה או לא מורכבת). הרמה הזו מציינת את הטווח האפשרי של אורך וסוג נעילת המסך. האפליקציה יכולה גם להציע למשתמשים לעדכן את נעילת המסך לרמה מסוימת, אבל המשתמשים יכולים להתעלם מההצעה ולנווט לפריט אחר. לתשומת ליבך, נעילת המסך לא מאוחסנת כטקסט פשוט, ולכן האפליקציה לא יודעת מה הסיסמה המדויקת."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"הצגת התראות"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"מאפשרת לאפליקציה להציג התראות"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"שימוש בחומרה ביומטרית"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"מאפשרת לאפליקציה להשתמש בחומרה ביומטרית לצורך אימות"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"ניהול חומרה של טביעות אצבעות"</string>
diff --git a/core/res/res/values-kn/strings.xml b/core/res/res/values-kn/strings.xml
index 2a9a59b..c8edc77 100644
--- a/core/res/res/values-kn/strings.xml
+++ b/core/res/res/values-kn/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"ಫೋನ್ ಕರೆ ಮಾಡಲು ಹಾಗೂ ನಿರ್ವಹಿಸಲು"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"ಬಾಡಿ ಸೆನ್ಸರ್‌"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"ನಿಮ್ಮ ಮುಖ್ಯ ಲಕ್ಷಣಗಳ ಕುರಿತು ಸೆನ್ಸಾರ್ ಡೇಟಾವನ್ನು ಪ್ರವೇಶಿಸಿ"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"ಅಧಿಸೂಚನೆಗಳು"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"ಅಧಿಸೂಚನೆಗಳನ್ನು ತೋರಿಸಿ"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"ವಿಂಡೋ ವಿಷಯವನ್ನು ಹಿಂಪಡೆಯುತ್ತದೆ"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"ನೀವು ಬಳಸುತ್ತಿರುವ ವಿಂಡೋದ ವಿಷಯ ಪರೀಕ್ಷಿಸುತ್ತದೆ."</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"ಸ್ಪರ್ಶ-ಎಕ್ಸ್‌ಪ್ಲೋರ್ ಆನ್ ಮಾಡುತ್ತದೆ"</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"ಕೀಲಾಕ್ ಮತ್ತು ಯಾವುದೇ ಸಂಬಂಧಿತ ಭದ್ರತಾ ಪಾಸ್‍‍ವರ್ಡ್ ಭದ್ರತೆಯನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲು ಅಪ್ಲಿಕೇಶನ್‍‍ಗೆ ಅನುಮತಿ ನೀಡುತ್ತದೆ. ಉದಾಹರಣೆಗೆ, ಒಳಬರುವ ಕರೆಯನ್ನು ಸ್ವೀಕರಿಸುವಾಗ ಕೀಲಾಕ್ ಅನ್ನು ಫೋನ್ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸುತ್ತದೆ, ನಂತರ ಕರೆಯು ಅಂತ್ಯಗೊಂಡಾಗ ಕೀಲಾಕ್ ಅನ್ನು ಮರು ಸಕ್ರಿಯಗೊಳಿಸುತ್ತದೆ."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"ಸ್ಕ್ರೀನ್ ಲಾಕ್ ಸಂಕೀರ್ಣತೆಯನ್ನು ವಿನಂತಿಸಿ"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"ಆ್ಯಪ್‌ಗೆ ಸ್ಕ್ರೀನ್ ಲಾಕ್ ಸಂಕೀರ್ಣತೆ ಮಟ್ಟವನ್ನು (ಅಧಿಕ, ಮಧ್ಯಮ, ಕಡಿಮೆ ಅಥವಾ ಯಾವುದೂ ಅಲ್ಲ) ತಿಳಿದುಕೊಳ್ಳಲು ಅನುಮತಿಸುತ್ತದೆ, ಅದು ಉದ್ದದ ಸಂಭವನೀಯ ಶ್ರೇಣಿ ಮತ್ತು ಸ್ಕ್ರೀನ್ ಲಾಕ್‌ನ ವಿಧವನ್ನು ಸೂಚಿಸುತ್ತದೆ. ಬಳಕೆದಾರರು ನಿರ್ದಿಷ್ಟ ಹಂತದವರೆಗೆ ಸ್ಕ್ರೀನ್ ಲಾಕ್ ಅನ್ನು ಅಪ್‌ಡೇಟ್ ಮಾಡಬಹುದು, ಆದರೆ ಅವರು ಅದನ್ನು ನಿರ್ಲಕ್ಷಿಸಬಹುದು ಮತ್ತು ಅದರಿಂದ ಹೊರಬರಬಹುದು ಎಂಬುದನ್ನು ಸಹ ಆ್ಯಪ್ ಬಳಕೆದಾರರಿಗೆ ಸೂಚಿಸುತ್ತದೆ ಸ್ಕ್ರೀನ್ ಲಾಕ್ ಅನ್ನು ಪಠ್ಯದ ರೂಪದಲ್ಲಿ ಸಂಗ್ರಹಿಸಲಾಗಿರುವುದಿಲ್ಲ, ಇದರಿಂದಾಗಿ ಆ್ಯಪ್‌ಗೆ ಸರಿಯಾದ ಪಾಸ್‌ವರ್ಡ್ ಗೊತ್ತಿರುವುದಿಲ್ಲ ಎಂಬುದನ್ನು ಗಮನಿಸಿ."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"ಅಧಿಸೂಚನೆಗಳನ್ನು ತೋರಿಸಿ"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"ಅಧಿಸೂಚನೆಗಳನ್ನು ತೋರಿಸಲು ಆ್ಯಪ್‌ಗೆ ಅನುಮತಿಸುತ್ತದೆ"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"ಬಯೋಮೆಟ್ರಿಕ್ ಹಾರ್ಡ್‌ವೇರ್‌ ಬಳಸಿ"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"ಪ್ರಮಾಣೀಕರಣಕ್ಕಾಗಿ ಬಯೋಮೆಟ್ರಿಕ್ ಹಾರ್ಡ್‌ವೇರ್ ಬಳಸಲು ಅಪ್ಲಿಕೇಶನ್‌ಗೆ ಅನುಮತಿಸುತ್ತದೆ"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"ಫಿಂಗರ್‌ಪ್ರಿಂಟ್ ಹಾರ್ಡ್‌ವೇರ್ ನಿರ್ವಹಿಸಿ"</string>
diff --git a/core/res/res/values-lo/strings.xml b/core/res/res/values-lo/strings.xml
index cd611f7..fe39b00 100644
--- a/core/res/res/values-lo/strings.xml
+++ b/core/res/res/values-lo/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"ໂທ ແລະ​ຈັດ​ການ​ການ​ໂທ​ລະ​ສັບ"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"ເຊັນເຊີຮ່າງກາຍ"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"ເຂົ້າ​ຫາ​ຂໍ້​ມູນ​ເຊັນ​ເຊີ​ກ່ຽວ​ກັບ​ສັນ​ຍານ​ຊີບ​ຂອງ​ທ່ານ"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"ການແຈ້ງເຕືອນ"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"ສະແດງການແຈ້ງເຕືອນ"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"ດຶງຂໍ້ມູນເນື້ອຫາໃນໜ້າຈໍ"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"ກວດກາເນື້ອຫາຂອງໜ້າຈໍທີ່ທ່ານກຳລັງມີປະຕິສຳພັນນຳ."</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"ເປີດໃຊ້ \"ການສຳຫຼວດໂດຍສຳຜັດ\""</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"ອະນຸຍາດໃຫ້ແອັບຯປິດການເຮັດວຽກຂອງປຸ່ມລັອກ ແລະລະບົບຄວາມປອດໄພຂອງລະຫັດຜ່ານທີ່ເຊື່ອມໂຍງກັນ. ໂຕຢ່າງ: ໂທລະສັບຈະປິດການເຮັດວຽກຂອງປຸ່ມລັອກເມື່ອມີສາຍໂທເຂົ້າ ຈາກນັ້ນຈຶ່ງເປີດໃຊ້ໄດ້ອີກເມື່ອວາງສາຍແລ້ວ."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"ຮ້ອງຂໍຄວາມຊັບຊ້ອນການລັອກໜ້າຈໍ"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"ອະນຸຍາດໃຫ້ແອັບສຶກສາລະດັບຄວາມຊັບຊ້ອນຂອງໜ້າຈໍລັອກ (ສູງ, ກາງ ຫຼື ບໍ່ມີ), ເຊິ່ງລະບຸຂອບເຂດຄວາມເປັນໄປໄດ້ຂອງຄວາມຍາວ ແລະ ປະເພດຂອງການລັອກໜ້າຈໍ. ແອັບນີ້ສາມາດແນະນຳຜູ້ໃຊ້ວ່າເຂົາເຈົ້າສາມາດອັບເດດໜ້າຈໍລັອກເປັນລະດັບໃດໜຶ່ງເປັນການສະເພາະໄດ້, ແຕ່ຜູ້ໃຊ້ສາມາດທີ່ຈະບໍ່ສົນໃຈ ຫຼື ເປີດໄປອັນອື່ນໄດ້. ກະລຸນາຮັບຊາບວ່າການລັອກໜ້າຈໍບໍ່ໄດ້ບັນທຶກໃນແບບຂໍ້ຄວາມທຳມະດາ, ດັ່ງນັ້ນແອັບຈະບໍ່ຮູ້ລະຫັດຜ່ານທີ່ແນ່ນອນ."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"ສະແດງການແຈ້ງເຕືອນ"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"ອະນຸຍາດໃຫ້ແອັບສະແດງການແຈ້ງເຕືອນ"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"ໃຊ້ຮາດແວຊີວະມິຕິ"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"ອະນຸຍາດໃຫ້ແອັບນຳໃຊ້ຮາດແວຊີວະມິຕິສຳລັບການພິສູດຢືນຢັນ"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"ຈັດ​ການ​ຮາດ​ແວ​ລາຍ​ນີ້ວ​ມື"</string>
diff --git a/core/res/res/values-mcc334-mnc020-as/strings.xml b/core/res/res/values-mcc334-mnc020-as/strings.xml
index 25b074e..43f5fb1 100644
--- a/core/res/res/values-mcc334-mnc020-as/strings.xml
+++ b/core/res/res/values-mcc334-mnc020-as/strings.xml
@@ -20,10 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="config_pdp_reject_dialog_title" msgid="41208110171880430"></string>
-    <!-- no translation found for config_pdp_reject_user_authentication_failed (4683454131283459978) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_service_not_subscribed (9021140729932308119) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_multi_conn_to_same_pdn_not_allowed (3838388706348367865) -->
-    <skip />
+    <string name="config_pdp_reject_user_authentication_failed" msgid="4683454131283459978">"বিশ্বাসযোগ্যতা প্ৰমাণীকৰণ বিফল হৈছে -29-"</string>
+    <string name="config_pdp_reject_service_not_subscribed" msgid="9021140729932308119">"সেৱাটো ছাবস্ক্ৰাইব কৰা নাই -33-"</string>
+    <string name="config_pdp_reject_multi_conn_to_same_pdn_not_allowed" msgid="3838388706348367865">"এটা APNৰ বাবে একাধিক PDN সংযোগৰ অনুমতি নাই -55-"</string>
 </resources>
diff --git a/core/res/res/values-mcc334-mnc020-bn/strings.xml b/core/res/res/values-mcc334-mnc020-bn/strings.xml
index 25b074e..f58aea7 100644
--- a/core/res/res/values-mcc334-mnc020-bn/strings.xml
+++ b/core/res/res/values-mcc334-mnc020-bn/strings.xml
@@ -20,10 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="config_pdp_reject_dialog_title" msgid="41208110171880430"></string>
-    <!-- no translation found for config_pdp_reject_user_authentication_failed (4683454131283459978) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_service_not_subscribed (9021140729932308119) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_multi_conn_to_same_pdn_not_allowed (3838388706348367865) -->
-    <skip />
+    <string name="config_pdp_reject_user_authentication_failed" msgid="4683454131283459978">"যাচাই করা যায়নি -29-"</string>
+    <string name="config_pdp_reject_service_not_subscribed" msgid="9021140729932308119">"পরিষেবা সাবস্ক্রাইব করা হয়নি -33-"</string>
+    <string name="config_pdp_reject_multi_conn_to_same_pdn_not_allowed" msgid="3838388706348367865">"নির্দিষ্ট কোনও APN-এর জন্য একাধিক PDN কানেকশন অনুমোদিত নয় -55-"</string>
 </resources>
diff --git a/core/res/res/values-mcc334-mnc020-fr-rCA/strings.xml b/core/res/res/values-mcc334-mnc020-fr-rCA/strings.xml
index 25b074e..19794da 100644
--- a/core/res/res/values-mcc334-mnc020-fr-rCA/strings.xml
+++ b/core/res/res/values-mcc334-mnc020-fr-rCA/strings.xml
@@ -20,10 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="config_pdp_reject_dialog_title" msgid="41208110171880430"></string>
-    <!-- no translation found for config_pdp_reject_user_authentication_failed (4683454131283459978) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_service_not_subscribed (9021140729932308119) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_multi_conn_to_same_pdn_not_allowed (3838388706348367865) -->
-    <skip />
+    <string name="config_pdp_reject_user_authentication_failed" msgid="4683454131283459978">"ÉCHEC DE L\'AUTHENTIFICATION -29-"</string>
+    <string name="config_pdp_reject_service_not_subscribed" msgid="9021140729932308119">"NON ABONNÉ AU SERVICE -33-"</string>
+    <string name="config_pdp_reject_multi_conn_to_same_pdn_not_allowed" msgid="3838388706348367865">"Connexions PDN multiples interdites pour un APN donné -55-"</string>
 </resources>
diff --git a/core/res/res/values-mcc334-mnc020-fr/strings.xml b/core/res/res/values-mcc334-mnc020-fr/strings.xml
index 25b074e..6eaa7cd 100644
--- a/core/res/res/values-mcc334-mnc020-fr/strings.xml
+++ b/core/res/res/values-mcc334-mnc020-fr/strings.xml
@@ -20,10 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="config_pdp_reject_dialog_title" msgid="41208110171880430"></string>
-    <!-- no translation found for config_pdp_reject_user_authentication_failed (4683454131283459978) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_service_not_subscribed (9021140729932308119) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_multi_conn_to_same_pdn_not_allowed (3838388706348367865) -->
-    <skip />
+    <string name="config_pdp_reject_user_authentication_failed" msgid="4683454131283459978">"ÉCHEC DE L\'AUTHENTIFICATION -29-"</string>
+    <string name="config_pdp_reject_service_not_subscribed" msgid="9021140729932308119">"NON INSCRIT AU SERVICE -33-"</string>
+    <string name="config_pdp_reject_multi_conn_to_same_pdn_not_allowed" msgid="3838388706348367865">"Vous n\'êtes pas autorisé à avoir plusieurs connexions PDN pour un APN donné -55-"</string>
 </resources>
diff --git a/core/res/res/values-mcc334-mnc020-gu/strings.xml b/core/res/res/values-mcc334-mnc020-gu/strings.xml
index 25b074e..5faef6f 100644
--- a/core/res/res/values-mcc334-mnc020-gu/strings.xml
+++ b/core/res/res/values-mcc334-mnc020-gu/strings.xml
@@ -20,10 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="config_pdp_reject_dialog_title" msgid="41208110171880430"></string>
-    <!-- no translation found for config_pdp_reject_user_authentication_failed (4683454131283459978) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_service_not_subscribed (9021140729932308119) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_multi_conn_to_same_pdn_not_allowed (3838388706348367865) -->
-    <skip />
+    <string name="config_pdp_reject_user_authentication_failed" msgid="4683454131283459978">"પ્રમાણીકરણ નિષ્ફળ થયું -29-"</string>
+    <string name="config_pdp_reject_service_not_subscribed" msgid="9021140729932308119">"સેવા સબ્સ્ક્રાઇબ કરી નથી -33-"</string>
+    <string name="config_pdp_reject_multi_conn_to_same_pdn_not_allowed" msgid="3838388706348367865">"આપેલા APN માટે એક કરતાં વધારે PDN કનેક્શનની મંજૂરી નથી -55-"</string>
 </resources>
diff --git a/core/res/res/values-mcc334-mnc020-ur/strings.xml b/core/res/res/values-mcc334-mnc020-ur/strings.xml
index 25b074e..bab5589 100644
--- a/core/res/res/values-mcc334-mnc020-ur/strings.xml
+++ b/core/res/res/values-mcc334-mnc020-ur/strings.xml
@@ -20,10 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="config_pdp_reject_dialog_title" msgid="41208110171880430"></string>
-    <!-- no translation found for config_pdp_reject_user_authentication_failed (4683454131283459978) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_service_not_subscribed (9021140729932308119) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_multi_conn_to_same_pdn_not_allowed (3838388706348367865) -->
-    <skip />
+    <string name="config_pdp_reject_user_authentication_failed" msgid="4683454131283459978">"توثیق کی ناکامی -29-"</string>
+    <string name="config_pdp_reject_service_not_subscribed" msgid="9021140729932308119">"سروس کو سبسکرائب نہیں کیا -33-"</string>
+    <string name="config_pdp_reject_multi_conn_to_same_pdn_not_allowed" msgid="3838388706348367865">"‏ایک دیئے گئے APN کے لیے متعدد PDN کنکشنز کی اجازت نہیں ہے -55-"</string>
 </resources>
diff --git a/core/res/res/values-mcc334-mnc020-zh-rCN/strings.xml b/core/res/res/values-mcc334-mnc020-zh-rCN/strings.xml
index 25b074e..a8fbe74c 100644
--- a/core/res/res/values-mcc334-mnc020-zh-rCN/strings.xml
+++ b/core/res/res/values-mcc334-mnc020-zh-rCN/strings.xml
@@ -20,10 +20,7 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="config_pdp_reject_dialog_title" msgid="41208110171880430"></string>
-    <!-- no translation found for config_pdp_reject_user_authentication_failed (4683454131283459978) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_service_not_subscribed (9021140729932308119) -->
-    <skip />
-    <!-- no translation found for config_pdp_reject_multi_conn_to_same_pdn_not_allowed (3838388706348367865) -->
-    <skip />
+    <string name="config_pdp_reject_user_authentication_failed" msgid="4683454131283459978">"身份验证失败 -29-"</string>
+    <string name="config_pdp_reject_service_not_subscribed" msgid="9021140729932308119">"未订阅服务 -33-"</string>
+    <string name="config_pdp_reject_multi_conn_to_same_pdn_not_allowed" msgid="3838388706348367865">"指定的 APN 不能有多个 PDN 连接 -55-"</string>
 </resources>
diff --git a/core/res/res/values-ml/strings.xml b/core/res/res/values-ml/strings.xml
index b63447a..1dfb50b 100644
--- a/core/res/res/values-ml/strings.xml
+++ b/core/res/res/values-ml/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"ഫോൺ വിളിക്കുകയും നിയന്ത്രിക്കുകയും ചെയ്യുക"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"ബോഡി സെൻസർ"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"നിങ്ങളുടെ ജീവാധാര ലക്ഷണങ്ങളെ കുറിച്ചുള്ള സെൻസർ വിവരങ്ങൾ ആക്സസ് ചെയ്യുക"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"അറിയിപ്പുകൾ"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"അറിയിപ്പുകൾ കാണിക്കുക"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"വിൻഡോ ഉള്ളടക്കം വീണ്ടെടുക്കുക"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"നിങ്ങൾ സംവദിക്കുന്ന ഒരു വിൻഡോയുടെ ഉള്ളടക്കം പരിശോധിക്കുക."</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"സ്‌പർശനം വഴി പര്യവേക്ഷണം ചെയ്യുക, ഓണാക്കുക"</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"കീലോക്കും ഏതെങ്കിലും അനുബന്ധ പാസ്‌വേഡ് സുരക്ഷയും പ്രവർത്തനരഹിതമാക്കാൻ അപ്ലിക്കേഷനെ അനുവദിക്കുന്നു. ഉദാഹരണത്തിന്, ഒരു ഇൻകമിംഗ് കോൾ സ്വീകരിക്കുമ്പോൾ ഫോൺ കീലോക്ക് പ്രവർത്തനരഹിതമാക്കുന്നു, കോൾ അവസാനിക്കുമ്പോൾ കീലോക്ക് വീണ്ടും പ്രവർത്തനക്ഷമമാകുന്നു."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"സ്‌ക്രീൻ ലോക്ക് സങ്കീർണ്ണത അഭ്യർത്ഥിക്കുക"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"സ്ക്രീൻ ലോക്കിന്റെ സാധ്യമായ നീളവും തരവും സൂചിപ്പിക്കുന്ന, അതിന്റെ സങ്കീർണ്ണത നില (ഉയർന്നത്, ഇടത്തരം, കുറഞ്ഞത് അല്ലെങ്കിൽ ഒന്നുമില്ല) മനസ്സിലാക്കാൻ ആപ്പിനെ അനുവദിക്കുന്നു. സ്‌ക്രീൻ ലോക്ക് ഒരു പ്രത്യേക തലത്തിലേക്ക് അപ്ഡേറ്റ് ചെയ്യാൻ ഉപയോക്താക്കളെ നിർദ്ദേശിക്കാനും ആപ്പിനാവും, പക്ഷെ ഉപയോക്താക്കൾക്ക് എളുപ്പത്തിൽ അവഗണിക്കാനും മറ്റൊന്നിലേക്ക് നാവിഗേറ്റ് ചെയ്യാനുമാവും. പ്ലെയിൻടെക്‌സ്‌റ്റിൽ സ്ക്രീൻ ലോക്ക് സംഭരിക്കപ്പെട്ടിട്ടില്ലെന്ന കാര്യം ശ്രദ്ധിക്കുക, അതിനാൽ ആപ്പിന് കൃത്യമായ പാസ്‌വേഡ് അറിയില്ല."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"അറിയിപ്പുകൾ കാണിക്കുക"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"അറിയിപ്പുകൾ കാണിക്കാൻ ആപ്പിനെ അനുവദിക്കുന്നു"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"ബയോമെട്രിക് ഹാർ‌ഡ്‌വെയർ ഉപയോഗിക്കുക"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"പരിശോധിച്ചുറപ്പിക്കുന്നതിനായി, ബയോമെട്രിക് ഹാർഡ്‌വെയർ ഉപയോഗിക്കാൻ ആപ്പിനെ അനുവദിക്കുക"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"ഫിംഗർപ്രിന്റ് ഹാർഡ്‌വെയർ നിയന്ത്രിക്കുക"</string>
diff --git a/core/res/res/values-mr/strings.xml b/core/res/res/values-mr/strings.xml
index 4da28dc..2e8bf70 100644
--- a/core/res/res/values-mr/strings.xml
+++ b/core/res/res/values-mr/strings.xml
@@ -100,7 +100,7 @@
     <string name="peerTtyModeHco" msgid="5626377160840915617">"समवयस्क व्यक्तीने TTY मोड HCO ची विनंती केली"</string>
     <string name="peerTtyModeVco" msgid="572208600818270944">"समवयस्क व्यक्तीने TTY मोड VCO ची विनंती केली"</string>
     <string name="peerTtyModeOff" msgid="2420380956369226583">"समवयस्क व्यक्तीने TTY मोड बंद ची विनंती केली"</string>
-    <string name="serviceClassVoice" msgid="2065556932043454987">"Voice"</string>
+    <string name="serviceClassVoice" msgid="2065556932043454987">"व्हॉइस"</string>
     <string name="serviceClassData" msgid="4148080018967300248">"डेटा"</string>
     <string name="serviceClassFAX" msgid="2561653371698904118">"फॅक्स"</string>
     <string name="serviceClassSMS" msgid="1547664561704509004">"SMS"</string>
@@ -306,7 +306,7 @@
     <string name="permgroupdesc_contacts" msgid="9163927941244182567">"आपल्या संपर्कांवर प्रवेश"</string>
     <string name="permgrouplab_location" msgid="1858277002233964394">"स्थान"</string>
     <string name="permgroupdesc_location" msgid="1995955142118450685">"या डिव्हाइसच्या स्थानावर प्रवेश"</string>
-    <string name="permgrouplab_calendar" msgid="6426860926123033230">"Calendar"</string>
+    <string name="permgrouplab_calendar" msgid="6426860926123033230">"कॅलेंडर"</string>
     <string name="permgroupdesc_calendar" msgid="6762751063361489379">"आपल्या कॅलेंडरवर प्रवेश"</string>
     <string name="permgrouplab_sms" msgid="795737735126084874">"SMS"</string>
     <string name="permgroupdesc_sms" msgid="5726462398070064542">"SMS मेसेज पाठवणे आणि पाहणे हे"</string>
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"फोन कॉल आणि व्यवस्थापित"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"शरीर सेन्सर"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"आपल्‍या महत्त्वाच्या मापनांविषयी सेन्सर डेटा अ‍ॅक्सेस करा"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"सूचना"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"सूचना दाखवा"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"विंडोमधील आशय पुन्हा मिळवा"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"तुम्ही वापरत असलेल्‍या विंडोमधील आशय तपासा."</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"स्पर्श करून अन्वेषण सुरू करा"</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"कीलॉक आणि कोणतीही संबद्ध पासवर्ड सुरक्षितता अक्षम करण्यासाठी अ‍ॅप ला अनुमती देते. उदाहरणार्थ, येणारा फोन कॉल प्राप्त करताना फोन कीलॉक अक्षम करतो, नंतर जेव्हा कॉल समाप्त होतो तेव्हा तो कीलॉक पुन्हा-सक्षम करतो."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"स्क्रीन लॉक क्लिष्टतेची विनंती करा"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"अ‍ॅपला स्क्रीन लॉक क्लिष्टता पातळी (उच्च, मध्यम, खालची किंवा काहीही नाही) जाणून घेऊ देते, जी लांबीची संभाव्य रेंज आणि स्क्रीन लॉकचा प्रकार सूचित करते. अ‍ॅप वापरकर्त्यांना असेदेखील सुचवू शकते की त्यांनी स्क्रीन लॉक ठराविक पातळीपर्यंत अपडेट करावे, परंतु वापरकर्ते त्याकडे मोकळेपणाने दुर्लक्ष करू शकतात आणि तेथून नेव्हिगेट करू शकतात. स्क्रीन लॉक प्लेनटेक्स्टमध्ये स्टोअर केले जात नसल्यामुळे अ‍ॅपला नेमका पासवर्ड माहीत नसतो याची नोंद घ्या."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"सूचना दाखवा"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"ॲपला सूचना दाखवू देते"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"बायोमेट्रिक हार्डवेअर वापरा"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"ऑथेंटिकेशनसाठी बायोमेट्रिक हार्डवेअरचा वापर करण्याची ॲपला अनुमती देते"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"फिंगरप्रिंट हार्डवेअर व्यवस्थापित करा"</string>
diff --git a/core/res/res/values-or/strings.xml b/core/res/res/values-or/strings.xml
index e5e78ee..edc206b 100644
--- a/core/res/res/values-or/strings.xml
+++ b/core/res/res/values-or/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"ଫୋନ୍‍ କଲ୍‍ କରେ ଏବଂ ପରିଚାଳନା କରେ"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"ବଡି ସେନ୍ସର୍"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"ଆପଣଙ୍କ ଗୁରୁତପୂର୍ଣ୍ଣ ସଂକେତଗୁଡ଼ିକ ବିଷୟରେ ସେନ୍ସର୍‍ ଡାଟା ଆକ୍ସେସ୍‍ କରେ"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"ବିଜ୍ଞପ୍ତିଗୁଡ଼ିକ"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"ବିଜ୍ଞପ୍ତିଗୁଡ଼ିକ ଦେଖାନ୍ତୁ"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"ୱିଣ୍ଡୋ କଣ୍ଟେଣ୍ଟ ହାସଲ କରନ୍ତୁ"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"ଆପଣ କାମ କରୁଥିବା ୱିଣ୍ଡୋର କଣ୍ଟେଣ୍ଟକୁ ଯାଞ୍ଚ କରନ୍ତୁ।"</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"ସ୍ପର୍ଶ ଦ୍ୱାରା ଏକ୍ସପ୍ଲୋର୍‍ ଅନ୍‍ କରନ୍ତୁ"</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"ଆପ୍‌କୁ କୀ\'ଲକ୍ କିମ୍ବା ସେଥିରେ ଥିବା କୌଣସି ପାସ୍‌ୱର୍ଡ ସୁରକ୍ଷାକୁ ଅକ୍ଷମ କରିବା ପାଇଁ ଅନୁମତି ଦିଏ, ଉଦାହରଣସ୍ୱରୂପ, ଇନ୍‌କମିଙ୍ଗ ଫୋନ୍‌ କଲ୍ ପ୍ରାପ୍ତ କରିବା ସମୟରେ ଫୋନ୍‌ଟି କୀ\'ଲକ୍‌କୁ ଅକ୍ଷମ କରିଦିଏ, ତା’ପରେ କଲ୍ ସମାପ୍ତ ହେବାପରେ ପୁଣି କୀ\'ଲକ୍‌କୁ ସକ୍ଷମ କରିଥାଏ।"</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"ସ୍କ୍ରିନ୍ ଲକ୍ ଜଟିଳତା ସଂକ୍ରାନ୍ତ ଅନୁରୋଧ"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"ସ୍କ୍ରିନ୍ ଲକ୍‌ର ଜଟିଳତା ସ୍ତର (ଉଚ୍ଚ, ମଧ୍ୟମ, ନିମ୍ନ କିମ୍ବା କିଛିନୁହେଁ), ଜାଣିବାକୁ ଆପ୍‌କୁ ଅନୁମତି ଦିଅନ୍ତୁ, ଯାହା ସ୍କ୍ରିନ୍ ଲକ୍‌ର ସମ୍ଭାବ୍ୟ ପରିସୀମାର ଲମ୍ବ ଏବଂ ପ୍ରକାର ସୂଚୀତ କରେ। ଆପ୍ ଏହା ମଧ୍ୟ ଉପଯୋଗକର୍ତ୍ତାମାନଙ୍କୁ ପରାମର୍ଶ ଦେଇପାରେ ଯେ ସେମାନେ ସ୍କ୍ରିନ୍ ଲକ୍‌କୁ ଏକ ନିର୍ଦ୍ଧିଷ୍ଟ ସ୍ତର ପର୍ଯ୍ୟନ୍ତ ଅପ୍‌ଡେଟ୍ କରିପାରନ୍ତି, କିନ୍ତୁ ଉପଯୋଗକର୍ତ୍ତାମାନେ ନିଜ ଇଚ୍ଛାରେ ଏହାକୁ ଉପେକ୍ଷା ଏବଂ ନାଭିଗେଟ୍ କରିପାରିବେ। ଧ୍ୟାନ ଦିଅନ୍ତୁ ଯେ, ସ୍କ୍ରିନ୍ ଲକ୍ ସରଳ ଟେକ୍ସଟ୍‌ରେ ଷ୍ଟୋର୍ କରାଯାଇନଥାଏ ତେଣୁ ଆପ୍ ସଠିକ୍ ପାସ୍‌‍ୱର୍ଡ ଜାଣିପାରି ନଥାଏ।"</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"ବିଜ୍ଞପ୍ତିଗୁଡ଼ିକ ଦେଖାନ୍ତୁ"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"ବିଜ୍ଞପ୍ତିଗୁଡ଼ିକ ଦେଖାଇବା ପାଇଁ ଆପକୁ ଅନୁମତି ଦିଏ"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"ବାୟୋମେଟ୍ରିକ୍‌ ହାର୍ଡୱେର୍‌ ବ୍ୟବହାର କରନ୍ତୁ"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"ସ୍ୱୀକୃତି ପାଇଁ ବାୟୋମେଟ୍ରିକ୍‌ ହାର୍ଡୱେର୍‌ ବ୍ୟବହାର କରିବାକୁ ଅନୁମତି ଦିଏ"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"ଟିପଚିହ୍ନ ହାର୍ଡୱେର୍‍ ପରିଚାଳନା କରନ୍ତୁ"</string>
diff --git a/core/res/res/values-sl/strings.xml b/core/res/res/values-sl/strings.xml
index 4426146..17e1588 100644
--- a/core/res/res/values-sl/strings.xml
+++ b/core/res/res/values-sl/strings.xml
@@ -332,10 +332,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"opravljanje in upravljanje telefonskih klicev"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"Tipala telesnih funkcij"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"dostop do podatkov tipala o vaših vitalnih znakih"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"Obvestila"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"prikaz obvestil"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"Pridobiti vsebino okna"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"Preverjanje vsebine okna, ki ga uporabljate."</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"Vklopiti raziskovanje z dotikom"</string>
@@ -559,10 +557,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"Aplikaciji dovoljuje, da onemogoči zaklep tipkovnice in morebitno povezano varnostno geslo. Telefon na primer onemogoči zaklep tipkovnice pri dohodnem klicu ter vnovič omogoči zaklep, ko je klic končan."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"zahteva zapletenost zaklepanja zaslona"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"Aplikaciji dovoljuje, da pridobi raven zapletenosti zaklepanja zaslona (visoka, srednja, nizka ali brez), ki označuje možen obseg dolžine in vrsto zaklepanja zaslona. Aplikacija lahko tudi predlaga uporabnikom, da posodobijo zaklepanje zaslona na določeno raven, vendar lahko uporabniki to opozorilo prezrejo in ga zaprejo tako, da se pomaknejo stran. Upoštevajte, da zaklepanje zaslona ni shranjeno v golem besedilu, tako da aplikacija ne pozna točnega gesla."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"prikaz obvestil"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"Aplikaciji dovoli prikaz obvestil."</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"uporaba strojne opreme za biometrične podatke"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"Aplikaciji omogoča uporabo strojne opreme za biometrične podatke za preverjanje pristnosti"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"upravljanje strojne opreme za prstne odtise"</string>
diff --git a/core/res/res/values-te/strings.xml b/core/res/res/values-te/strings.xml
index 0f6d47e..c216de3 100644
--- a/core/res/res/values-te/strings.xml
+++ b/core/res/res/values-te/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"ఫోన్ కాల్స్‌ చేయడం మరియు నిర్వహించడం"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"శరీర సెన్సార్‌లు"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"మీ అత్యంత కీలకమైన గుర్తుల గురించి సెన్సార్ డేటాని యాక్సెస్ చేస్తుంది"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"నోటిఫికేషన్‌లు"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"నోటిఫికేషన్‌లను చూపండి"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"విండో కంటెంట్‍ను తిరిగి పొందుతుంది"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"మీరు పరస్పర చర్య చేస్తున్న విండో కంటెంట్‌‍ను పరిశీలిస్తుంది."</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"తాకడం ద్వారా విశ్లేషణను ప్రారంభిస్తుంది"</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"కీలాక్ మరియు ఏదైనా అనుబంధించబడిన పాస్‌వర్డ్ భద్రతను నిలిపివేయడానికి యాప్‌ను అనుమతిస్తుంది. ఉదాహరణకు, ఇన్‌కమింగ్ ఫోన్ కాల్ వస్తున్నప్పుడు ఫోన్ కీలాక్‌ను నిలిపివేస్తుంది, ఆపై కాల్ ముగిసిన తర్వాత కీలాక్‌ను మళ్లీ ప్రారంభిస్తుంది."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"స్క్రీన్ లాక్ సంక్లిష్టత రిక్వెస్ట్‌"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"ఇది మీ స్క్రీన్ లాక్ పాస్‌వర్డ్‌ సంక్లిష్టత స్థాయి (తీవ్రంగా ఉండాలా, ఓ మోస్తరుగా ఉండాలా, తక్కువ తీవ్రంగా ఉండాలా లేదా అస్సలు తీవ్రత ఉండకూడదా) తెలుసుకోవడానికి యాప్‌ను అనుమతిస్తుంది, అంటే పొడుగు ఎంత ఉండాలి, ఏ రకమైన స్క్రీన్ లాక్ పధ్ధతి అనుసరించాలో సూచిస్తుంది. అలాగే, స్క్రీన్ లాక్‌ పాస్‌వర్డ్‌ సంక్లిష్టతను ఏ స్థాయికి సెట్ చేసుకుంటే బాగుంటుందో కూడా వినియోగదారులకు యాప్ సూచించగలదు, కానీ వినియోగదారులు నిరభ్యంతరంగా ఆ సూచనలను పట్టించుకోకుండా వారి ఇష్టం మేరకు చక్కగా సెట్ చేసుకోవచ్చు. ఇంకో ముఖ్య విషయం, స్క్రీన్ లాక్‌ అన్నది సాదా వచన రూపంలో నిల్వ చేయబడదు, కనుక ఖచ్చితమైన పాస్‌వర్డ్‌ ఏమిటనేది యాప్‌కు తెలియదు."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"నోటిఫికేషన్‌లను చూపండి"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"నోటిఫికేషన్‌లను చూపించడానికి యాప్‌ను అనుమతించండి"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"బయోమెట్రిక్ హార్డ్‌వేర్‌ని ఉపయోగించు"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"ప్రమాణీకరణ కోసం బయోమెట్రిక్ హార్డ్‌వేర్‌ను ఉపయోగించడానికి యాప్‌ని అనుమతిస్తుంది"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"వేలిముద్ర హార్డ్‌వేర్‌ని నిర్వహించడానికి అనుమతి"</string>
diff --git a/core/res/res/values-ur-television/strings.xml b/core/res/res/values-ur-television/strings.xml
new file mode 100644
index 0000000..42f2085
--- /dev/null
+++ b/core/res/res/values-ur-television/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="sensor_privacy_start_use_mic_notification_content_title" msgid="7002619958660406548">"مائیکروفون مسدود ہے"</string>
+    <string name="sensor_privacy_start_use_camera_notification_content_title" msgid="2131954635322568179">"کیمرا مسدود ہے"</string>
+</resources>
diff --git a/core/res/res/values-ur/strings.xml b/core/res/res/values-ur/strings.xml
index 5880de7..60f086b 100644
--- a/core/res/res/values-ur/strings.xml
+++ b/core/res/res/values-ur/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"فون کالز کریں اور ان کا نظم کریں"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"باڈی سینسرز"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"اپنی علامات حیات کے متعلق سنسر ڈیٹا تک رسائی حاصل کریں"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"اطلاعات"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"اطلاعات دکھائیں"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"ونڈو مواد بازیافت کرنے کی"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"کسی ایسی ونڈو کے مواد کا معائنہ کریں جس کے ساتھ آپ تعامل کر رہے ہیں۔"</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"ٹچ کے ذریعے دریافت کریں کو آن کرنے کی"</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"ایپ کو کلیدی لاک اور کسی بھی متعلقہ پاس ورڈ سیکیورٹی کو غیر فعال کرنے کی اجازت دیتا ہے۔ مثلاً، کوئی آنے والی فون کال موصول ہونے کے وقت فون کلیدی لاک کو غیر فعال کرتا ہے، پھر کال پوری ہوجانے پر کلیدی لاک کو دوبارہ فعال کردیتا ہے۔"</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"اسکرین لاک کی پیچیدگی کی درخواست کریں"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"اپپ کو اسکرین لاک کی پیچیدگی (بہت زیادہ، درمیانی، کم یا کوئی بھی نہیں) کو جاننے کی اجازت دیتی ہے، جو طوالت کی ممکنہ حد اور اسکرین لاک کی قسم کو بتاتی ہے۔ اپپ صارفین کو یہ مشوره بھی دے سکتی ہے کہ وہ اسکرین لاک کو مخصوص لیول تک اپ ڈیٹ کریں لیکن صارفین آزادانہ طور پر نظر انداز اور نیویگیٹ کر سکتے ہیں۔ نوٹ کریں کہ اسکرین لاک پلین ٹیکسٹ میں اسٹور نہیں کیا جاتا ہے اس لیے اپپ کو صحیح پاس ورڈ نہیں معلوم ہوتا ہے۔"</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"اطلاعات دکھائیں"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"ایپ کو اطلاعات دکھانے کی اجازت دیتا ہے"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"بایومیٹرک ہارڈ ویئر استعمال کریں"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"ایپ کو توثیق کے لیے بایومیٹرک ہارڈ ویئر استعمال کرنے کی اجازت دیتا ہے"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"فنگر پرنٹ ہارڈ ویئر کا نظم کریں"</string>
diff --git a/core/res/res/values-zh-rCN-television/strings.xml b/core/res/res/values-zh-rCN-television/strings.xml
new file mode 100644
index 0000000..b30c9df
--- /dev/null
+++ b/core/res/res/values-zh-rCN-television/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+   -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="sensor_privacy_start_use_mic_notification_content_title" msgid="7002619958660406548">"麦克风已被屏蔽"</string>
+    <string name="sensor_privacy_start_use_camera_notification_content_title" msgid="2131954635322568179">"摄像头已被屏蔽"</string>
+</resources>
diff --git a/core/res/res/values-zu/strings.xml b/core/res/res/values-zu/strings.xml
index ad9c34a..2e3c20b 100644
--- a/core/res/res/values-zu/strings.xml
+++ b/core/res/res/values-zu/strings.xml
@@ -326,10 +326,8 @@
     <string name="permgroupdesc_phone" msgid="270048070781478204">"yenza uphinde uphathe amakholi wefoni"</string>
     <string name="permgrouplab_sensors" msgid="9134046949784064495">"Izinzwa zomzimba"</string>
     <string name="permgroupdesc_sensors" msgid="2610631290633747752">"finyelela idatha yesizweli mayelana nezimpawu zakho ezibalulekile"</string>
-    <!-- no translation found for permgrouplab_notifications (5472972361980668884) -->
-    <skip />
-    <!-- no translation found for permgroupdesc_notifications (4608679556801506580) -->
-    <skip />
+    <string name="permgrouplab_notifications" msgid="5472972361980668884">"Izaziso"</string>
+    <string name="permgroupdesc_notifications" msgid="4608679556801506580">"bonisa izaziso"</string>
     <string name="capability_title_canRetrieveWindowContent" msgid="7554282892101587296">"Thola okuqukethwe kwewindi"</string>
     <string name="capability_desc_canRetrieveWindowContent" msgid="6195610527625237661">"Hlola okuqukethwe kwewindi ohlanganyela nalo."</string>
     <string name="capability_title_canRequestTouchExploration" msgid="327598364696316213">"Vula ukuhlola ngokuthinta"</string>
@@ -553,10 +551,8 @@
     <string name="permdesc_disableKeyguard" msgid="3223710003098573038">"Ivumela uhlelo lokusebenza ukukhubaza ukuvala ukhiye nanoma yikuphi ukuphepha kwephasiwedi okuhlobene. Isibonelo, ifoni ikhubaza ukuvala ukhiye lapho ithola ikholi yefoni engenayo, bese inike amandla kabusha ukuvala ukhiye lapho ikholi isiqedile."</string>
     <string name="permlab_requestPasswordComplexity" msgid="1808977190557794109">"cela ubunkimbinkimbi kokukhiya isikrini"</string>
     <string name="permdesc_requestPasswordComplexity" msgid="1130556896836258567">"Ivumela uhlelo lokusebenza ukuthi lifunde ileveli yobunkimbinkimbi bokukhiya isikrini (okuphezulu, okuphakathi nendawo, okuphansi noma lutho), obubonisa ibanga elingaba khona lobude nohlobo lokukhiya isikrini Uhlelo lokusebenza lungaphinda luphakamise kubasebenzisi ukuthi babuyekeze ukukhiya isikrini kuleveli elithile kodwa abasebenzisi bangaziba ngokukhululekile baphinde bazulazule nje. Qaphela ukuthi ukukhiya isikrini akugcinwa kumbhalo osobala ukuze uhlelo lokusebenza lungayazi iphasiwedi enembile."</string>
-    <!-- no translation found for permlab_postNotification (4875401198597803658) -->
-    <skip />
-    <!-- no translation found for permdesc_postNotification (5974977162462877075) -->
-    <skip />
+    <string name="permlab_postNotification" msgid="4875401198597803658">"bonisa izaziso"</string>
+    <string name="permdesc_postNotification" msgid="5974977162462877075">"Kuvumela i-app ibonise izaziso"</string>
     <string name="permlab_useBiometric" msgid="6314741124749633786">"sebenzisa izingxenyekazi zekhompyutha ze-biometric"</string>
     <string name="permdesc_useBiometric" msgid="7502858732677143410">"Ivumela uhlelo lokusebenza ukuthi lusebenzise izingxenyekazi zekhompyutha ze-biometric ukuze kuqinisekiswe"</string>
     <string name="permlab_manageFingerprint" msgid="7432667156322821178">"phatha izingxenyekazi zekhompyutha zezigxivizo zeminwe"</string>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 676546b..765495a 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -8372,7 +8372,7 @@
         <attr name="supportsAmbientMode" format="boolean" />
 
         <!-- Indicates that this wallpaper service should receive zoom transition updates when
-             changing the display state of the device (e.g. when folding or unfolding
+             changing the structural state of the device (e.g. when folding or unfolding
              a foldable device). When this value is set to true
              {@link android.service.wallpaper.WallpaperService.Engine} could receive zoom updates
              before or after changing the device state. Wallpapers receive zoom updates using
@@ -8381,8 +8381,8 @@
              {@link android.service.wallpaper.WallpaperService.Engine} is created and not destroyed.
              Default value is true.
              Corresponds to
-             {@link android.app.WallpaperInfo#shouldUseDefaultDisplayStateChangeTransition()} -->
-        <attr name="shouldUseDefaultDisplayStateChangeTransition" format="boolean" />
+             {@link android.app.WallpaperInfo#shouldUseDefaultUnfoldTransition()} -->
+        <attr name="shouldUseDefaultUnfoldTransition" format="boolean" />
 
         <!-- Uri that specifies a settings Slice for this wallpaper. -->
         <attr name="settingsSliceUri" format="string"/>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 40bd4ca..2eb32b0 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2790,6 +2790,9 @@
         <item>com.android.inputmethod.latin</item>
     </string-array>
 
+    <!-- Make the IME killable by the lowmemorykiller by raising its oom_score_adj. -->
+    <bool name="config_killableInputMethods">false</bool>
+
     <!-- The list of classes that should be added to the notification ranking pipeline.
      See {@link com.android.server.notification.NotificationSignalExtractor}
       If you add a new extractor to this list make sure to update
@@ -4062,7 +4065,7 @@
     <string translatable="false" name="config_inCallNotificationSound">/product/media/audio/ui/InCallNotification.ogg</string>
 
     <!-- URI for default ringtone sound file to be used for silent ringer vibration -->
-    <string translatable="false" name="config_defaultRingtoneVibrationSound">/product/media/audio/ui/AttentionalHaptics.ogg</string>
+    <string translatable="false" name="config_defaultRingtoneVibrationSound"></string>
 
     <!-- Default number of notifications from the same app before they are automatically grouped by the OS -->
     <integer translatable="false" name="config_autoGroupAtCount">4</integer>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index fea3a6e..a6e42bd 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -3222,7 +3222,7 @@
   <eat-comment />
 
   <staging-public-group type="attr" first-id="0x01ff0000">
-    <public name="shouldUseDefaultDisplayStateChangeTransition" />
+    <public name="shouldUseDefaultUnfoldTransition" />
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01fe0000">
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index dbb2b1a..6f4c7ab 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1493,6 +1493,11 @@
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=120]-->
     <string name="permdesc_uwb_ranging">Allow the app to determine relative position between nearby Ultra-Wideband devices</string>
 
+    <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=50]-->
+    <string name="permlab_nearby_wifi_devices">interact with nearby Wi\u2011Fi devices</string>
+    <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. [CHAR LIMIT=120]-->
+    <string name="permdesc_nearby_wifi_devices">Allows the app to advertise, connect, and determine the relative position of nearby Wi\u2011Fi devices</string>
+
     <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
     <string name="permlab_preferredPaymentInfo">Preferred NFC Payment Service Information</string>
     <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 86cf04e..253cd47 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2229,6 +2229,7 @@
   <java-symbol type="string" name="config_secondaryLocationTimeZoneProviderPackageName" />
   <java-symbol type="bool" name="config_autoResetAirplaneMode" />
   <java-symbol type="string" name="config_notificationAccessConfirmationActivity" />
+  <java-symbol type="bool" name="config_killableInputMethods" />
 
   <java-symbol type="layout" name="resolver_list" />
   <java-symbol type="id" name="resolver_list" />
diff --git a/core/res/res/xml/power_profile.xml b/core/res/res/xml/power_profile.xml
index 166edca..d310736 100644
--- a/core/res/res/xml/power_profile.xml
+++ b/core/res/res/xml/power_profile.xml
@@ -27,9 +27,33 @@
        are totally dependent on the platform and can vary
        significantly, so should be measured on the shipping platform
        with a power meter. -->
-  <item name="ambient.on">0.1</item>  <!-- ~100mA -->
-  <item name="screen.on">0.1</item>  <!-- ~100mA -->
-  <item name="screen.full">0.1</item>  <!-- ~100mA -->
+
+  <!-- Display related values. -->
+  <!-- Average battery current draw of display0 while in ambient mode, including backlight.
+       There must be one of these for each display, labeled:
+       ambient.on.display0, ambient.on.display1, etc...
+
+       Each display suffix number should match it's ordinal in its display device config.
+  -->
+  <item name="ambient.on.display0">0.1</item>  <!-- ~100mA -->
+  <!-- Average battery current draw of display0 while on without backlight.
+       There must be one of these for each display, labeled:
+       screen.on.display0, screen.on.display1, etc...
+
+       Each display suffix number should match it's ordinal in its display device config.
+  -->
+  <item name="screen.on.display0">0.1</item>  <!-- ~100mA -->
+  <!-- Average battery current draw of the backlight at full brightness.
+       The full current draw of display N at full brightness should be the sum of screen.on.displayN
+       and screen.full.displayN
+
+       There must be one of these for each display, labeled:
+       screen.full.display0, screen.full.display1, etc...
+
+       Each display suffix number should match it's ordinal in its display device config.
+  -->
+  <item name="screen.full.display0">0.1</item>  <!-- ~100mA -->
+
   <item name="bluetooth.active">0.1</item> <!-- Bluetooth data transfer, ~10mA -->
   <item name="bluetooth.on">0.1</item>  <!-- Bluetooth on & connectable, but not connected, ~0.1mA -->
   <item name="wifi.on">0.1</item>  <!-- ~3mA -->
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 93e4a29..bcd794e 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -54,7 +54,6 @@
         "print-test-util-lib",
         "testng",
         "servicestests-utils",
-        "AppSearchTestUtils",
     ],
 
     libs: [
diff --git a/core/tests/coretests/src/android/app/time/OWNERS b/core/tests/coretests/src/android/app/time/OWNERS
index 8f80897..292cb72 100644
--- a/core/tests/coretests/src/android/app/time/OWNERS
+++ b/core/tests/coretests/src/android/app/time/OWNERS
@@ -1,3 +1,2 @@
 # Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+include /core/java/android/app/time/OWNERS
diff --git a/core/tests/coretests/src/android/app/timedetector/OWNERS b/core/tests/coretests/src/android/app/timedetector/OWNERS
index 8f80897..c612473 100644
--- a/core/tests/coretests/src/android/app/timedetector/OWNERS
+++ b/core/tests/coretests/src/android/app/timedetector/OWNERS
@@ -1,3 +1,2 @@
 # Bug component: 847766
-mingaleev@google.com
 include /core/java/android/app/timedetector/OWNERS
diff --git a/core/tests/coretests/src/android/app/timezone/OWNERS b/core/tests/coretests/src/android/app/timezone/OWNERS
index 8f80897..381ecf1 100644
--- a/core/tests/coretests/src/android/app/timezone/OWNERS
+++ b/core/tests/coretests/src/android/app/timezone/OWNERS
@@ -1,3 +1,2 @@
-# Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+# Bug component: 24949
+include /core/java/android/app/timezone/OWNERS
diff --git a/core/tests/coretests/src/android/app/timezonedetector/OWNERS b/core/tests/coretests/src/android/app/timezonedetector/OWNERS
index 8f80897..2e9c324 100644
--- a/core/tests/coretests/src/android/app/timezonedetector/OWNERS
+++ b/core/tests/coretests/src/android/app/timezonedetector/OWNERS
@@ -1,3 +1,2 @@
 # Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+include /core/java/android/app/timezonedetector/OWNERS
diff --git a/core/tests/coretests/src/android/net/OWNERS b/core/tests/coretests/src/android/net/OWNERS
index aa87958..4e5136f 100644
--- a/core/tests/coretests/src/android/net/OWNERS
+++ b/core/tests/coretests/src/android/net/OWNERS
@@ -1 +1,3 @@
 include /services/core/java/com/android/server/net/OWNERS
+
+per-file SntpClient* = file:/services/core/java/com/android/server/timedetector/OWNERS
diff --git a/core/tests/coretests/src/android/net/SntpClientTest.java b/core/tests/coretests/src/android/net/SntpClientTest.java
index bf9978c..b400b9b 100644
--- a/core/tests/coretests/src/android/net/SntpClientTest.java
+++ b/core/tests/coretests/src/android/net/SntpClientTest.java
@@ -22,7 +22,10 @@
 
 import static org.mockito.Mockito.CALLS_REAL_METHODS;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
+import android.net.sntp.Duration64;
+import android.net.sntp.Timestamp64;
 import android.util.Log;
 
 import androidx.test.runner.AndroidJUnit4;
@@ -38,7 +41,13 @@
 import java.net.DatagramSocket;
 import java.net.InetAddress;
 import java.net.SocketException;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
 import java.util.Arrays;
+import java.util.Random;
+import java.util.function.Supplier;
 
 @RunWith(AndroidJUnit4.class)
 public class SntpClientTest {
@@ -54,41 +63,232 @@
     //
     // Server, Leap indicator:  (0), Stratum 2 (secondary reference), poll 6 (64s), precision -20
     // Root Delay: 0.005447, Root dispersion: 0.002716, Reference-ID: 221.253.71.41
-    //   Reference Timestamp:  3653932102.507969856 (2015/10/15 14:08:22)
-    //   Originator Timestamp: 3653932113.576327741 (2015/10/15 14:08:33)
-    //   Receive Timestamp:    3653932113.581012725 (2015/10/15 14:08:33)
-    //   Transmit Timestamp:   3653932113.581012725 (2015/10/15 14:08:33)
+    //   Reference Timestamp:
+    //     d9ca9446.820a5000 / ERA0: 2015-10-15 21:08:22 UTC / ERA1: 2151-11-22 03:36:38 UTC
+    //   Originator Timestamp:
+    //     d9ca9451.938a3771 / ERA0: 2015-10-15 21:08:33 UTC / ERA1: 2151-11-22 03:36:49 UTC
+    //   Receive Timestamp:
+    //     d9ca9451.94bd3fff / ERA0: 2015-10-15 21:08:33 UTC / ERA1: 2151-11-22 03:36:49 UTC
+    //   Transmit Timestamp:
+    //     d9ca9451.94bd4001 / ERA0: 2015-10-15 21:08:33 UTC / ERA1: 2151-11-22 03:36:49 UTC
+    //
     //     Originator - Receive Timestamp:  +0.004684958
     //     Originator - Transmit Timestamp: +0.004684958
-    private static final String WORKING_VERSION4 =
-            "240206ec" +
-            "00000165" +
-            "000000b2" +
-            "ddfd4729" +
-            "d9ca9446820a5000" +
-            "d9ca9451938a3771" +
-            "d9ca945194bd3fff" +
-            "d9ca945194bd4001";
+    private static final String LATE_ERA_RESPONSE =
+            "240206ec"
+                    + "00000165"
+                    + "000000b2"
+                    + "ddfd4729"
+                    + "d9ca9446820a5000"
+                    + "d9ca9451938a3771"
+                    + "d9ca945194bd3fff"
+                    + "d9ca945194bd4001";
+
+    /** This is the actual UTC time in the server if it is in ERA0 */
+    private static final Instant LATE_ERA0_SERVER_TIME =
+            calculateIdealServerTime("d9ca9451.94bd3fff", "d9ca9451.94bd4001", 0);
+
+    /**
+     * This is the Unix epoch time matches the originate timestamp from {@link #LATE_ERA_RESPONSE}
+     * when interpreted as an ERA0 timestamp.
+     */
+    private static final Instant LATE_ERA0_REQUEST_TIME =
+            Timestamp64.fromString("d9ca9451.938a3771").toInstant(0);
+
+    // A tweaked version of the ERA0 response to represent an ERA 1 response.
+    //
+    // Server, Leap indicator:  (0), Stratum 2 (secondary reference), poll 6 (64s), precision -20
+    // Root Delay: 0.005447, Root dispersion: 0.002716, Reference-ID: 221.253.71.41
+    //   Reference Timestamp:
+    //     1db2d246.820a5000 / ERA0: 1915-10-16 21:08:22 UTC / ERA1: 2051-11-22 03:36:38 UTC
+    //   Originate Timestamp:
+    //     1db2d251.938a3771 / ERA0: 1915-10-16 21:08:33 UTC / ERA1: 2051-11-22 03:36:49 UTC
+    //   Receive Timestamp:
+    //     1db2d251.94bd3fff / ERA0: 1915-10-16 21:08:33 UTC / ERA1: 2051-11-22 03:36:49 UTC
+    //   Transmit Timestamp:
+    //     1db2d251.94bd4001 / ERA0: 1915-10-16 21:08:33 UTC / ERA1: 2051-11-22 03:36:49 UTC
+    //
+    //     Originate - Receive Timestamp:  +0.004684958
+    //     Originate - Transmit Timestamp: +0.004684958
+    private static final String EARLY_ERA_RESPONSE =
+            "240206ec"
+                    + "00000165"
+                    + "000000b2"
+                    + "ddfd4729"
+                    + "1db2d246820a5000"
+                    + "1db2d251938a3771"
+                    + "1db2d25194bd3fff"
+                    + "1db2d25194bd4001";
+
+    /** This is the actual UTC time in the server if it is in ERA0 */
+    private static final Instant EARLY_ERA1_SERVER_TIME =
+            calculateIdealServerTime("1db2d251.94bd3fff", "1db2d251.94bd4001", 1);
+
+    /**
+     * This is the Unix epoch time matches the originate timestamp from {@link #EARLY_ERA_RESPONSE}
+     * when interpreted as an ERA1 timestamp.
+     */
+    private static final Instant EARLY_ERA1_REQUEST_TIME =
+            Timestamp64.fromString("1db2d251.938a3771").toInstant(1);
 
     private SntpTestServer mServer;
     private SntpClient mClient;
     private Network mNetwork;
+    private Supplier<Instant> mSystemTimeSupplier;
+    private Random mRandom;
 
+    @SuppressWarnings("unchecked")
     @Before
     public void setUp() throws Exception {
+        mServer = new SntpTestServer();
+
         // A mock network has NETID_UNSET, which allows the test to run, with a loopback server,
         // even w/o external networking.
         mNetwork = mock(Network.class, CALLS_REAL_METHODS);
-        mServer = new SntpTestServer();
-        mClient = new SntpClient();
+        mRandom = mock(Random.class);
+
+        mSystemTimeSupplier = mock(Supplier.class);
+        // Returning zero means the "randomized" bottom bits of the clients transmit timestamp /
+        // server's originate timestamp will be zeros.
+        when(mRandom.nextInt()).thenReturn(0);
+        mClient = new SntpClient(mSystemTimeSupplier, mRandom);
     }
 
+    /** Tests when the client and server are in ERA0. b/199481251. */
     @Test
-    public void testBasicWorkingSntpClientQuery() throws Exception {
-        mServer.setServerReply(HexEncoding.decode(WORKING_VERSION4.toCharArray(), false));
+    public void testRequestTime_era0ClientEra0RServer() throws Exception {
+        when(mSystemTimeSupplier.get()).thenReturn(LATE_ERA0_REQUEST_TIME);
+
+        mServer.setServerReply(HexEncoding.decode(LATE_ERA_RESPONSE.toCharArray(), false));
         assertTrue(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500, mNetwork));
         assertEquals(1, mServer.numRequestsReceived());
         assertEquals(1, mServer.numRepliesSent());
+
+        checkRequestTimeCalcs(LATE_ERA0_REQUEST_TIME, LATE_ERA0_SERVER_TIME, mClient);
+    }
+
+    /** Tests when the client is behind the server and in the previous ERA. b/199481251. */
+    @Test
+    public void testRequestTime_era0ClientEra1Server() throws Exception {
+        when(mSystemTimeSupplier.get()).thenReturn(LATE_ERA0_REQUEST_TIME);
+
+        mServer.setServerReply(HexEncoding.decode(EARLY_ERA_RESPONSE.toCharArray(), false));
+        assertTrue(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500, mNetwork));
+        assertEquals(1, mServer.numRequestsReceived());
+        assertEquals(1, mServer.numRepliesSent());
+
+        checkRequestTimeCalcs(LATE_ERA0_REQUEST_TIME, EARLY_ERA1_SERVER_TIME, mClient);
+
+    }
+
+    /** Tests when the client is ahead of the server and in the next ERA. b/199481251. */
+    @Test
+    public void testRequestTime_era1ClientEra0Server() throws Exception {
+        when(mSystemTimeSupplier.get()).thenReturn(EARLY_ERA1_REQUEST_TIME);
+
+        mServer.setServerReply(HexEncoding.decode(LATE_ERA_RESPONSE.toCharArray(), false));
+        assertTrue(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500, mNetwork));
+        assertEquals(1, mServer.numRequestsReceived());
+        assertEquals(1, mServer.numRepliesSent());
+
+        checkRequestTimeCalcs(EARLY_ERA1_REQUEST_TIME, LATE_ERA0_SERVER_TIME, mClient);
+    }
+
+    /** Tests when the client and server are in ERA1. b/199481251. */
+    @Test
+    public void testRequestTime_era1ClientEra1Server() throws Exception {
+        when(mSystemTimeSupplier.get()).thenReturn(EARLY_ERA1_REQUEST_TIME);
+
+        mServer.setServerReply(HexEncoding.decode(EARLY_ERA_RESPONSE.toCharArray(), false));
+        assertTrue(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500, mNetwork));
+        assertEquals(1, mServer.numRequestsReceived());
+        assertEquals(1, mServer.numRepliesSent());
+
+        checkRequestTimeCalcs(EARLY_ERA1_REQUEST_TIME, EARLY_ERA1_SERVER_TIME, mClient);
+    }
+
+    private static void checkRequestTimeCalcs(
+            Instant clientTime, Instant serverTime, SntpClient client) {
+        // The tests don't attempt to control the elapsed time tracking, which influences the
+        // round trip time (i.e. time spent in due to the network), but they control everything
+        // else, so assertions are allowed some slop and round trip time just has to be >= 0.
+        assertTrue("getRoundTripTime()=" + client.getRoundTripTime(),
+                client.getRoundTripTime() >= 0);
+
+        // Calculate the ideal offset if nothing took any time.
+        long expectedOffset = serverTime.toEpochMilli() - clientTime.toEpochMilli();
+        long allowedSlop = (client.getRoundTripTime() / 2) + 1; // +1 to allow for truncation loss.
+        assertNearlyEquals(expectedOffset, client.getClockOffset(), allowedSlop);
+        assertNearlyEquals(clientTime.toEpochMilli() + expectedOffset,
+                client.getNtpTime(), allowedSlop);
+    }
+
+    /**
+     * Unit tests for the low-level offset calculations. More targeted / easier to write than the
+     * end-to-end tests above that simulate the server. b/199481251.
+     */
+    @Test
+    public void testCalculateClockOffset() {
+        Instant era0Time1 = utcInstant(2021, 10, 5, 2, 2, 2, 2);
+        // Confirm what happens when the client and server are completely in sync.
+        checkCalculateClockOffset(era0Time1, era0Time1);
+
+        Instant era0Time2 = utcInstant(2021, 10, 6, 1, 1, 1, 1);
+        checkCalculateClockOffset(era0Time1, era0Time2);
+        checkCalculateClockOffset(era0Time2, era0Time1);
+
+        Instant era1Time1 = utcInstant(2061, 10, 5, 2, 2, 2, 2);
+        checkCalculateClockOffset(era1Time1, era1Time1);
+
+        Instant era1Time2 = utcInstant(2061, 10, 6, 1, 1, 1, 1);
+        checkCalculateClockOffset(era1Time1, era1Time2);
+        checkCalculateClockOffset(era1Time2, era1Time1);
+
+        // Cross-era calcs (requires they are still within 68 years of each other).
+        checkCalculateClockOffset(era0Time1, era1Time1);
+        checkCalculateClockOffset(era1Time1, era0Time1);
+    }
+
+    private void checkCalculateClockOffset(Instant clientTime, Instant serverTime) {
+        // The expected (ideal) offset is the difference between the client and server clocks. NTP
+        // assumes delays are symmetric, i.e. that the server time is between server
+        // receive/transmit time, client time is between request/response time, and send networking
+        // delay == receive networking delay.
+        Duration expectedOffset = Duration.between(clientTime, serverTime);
+
+        // Try simulating various round trip delays, including zero.
+        for (long totalElapsedTimeMillis : Arrays.asList(0, 20, 200, 2000, 20000)) {
+            // Simulate that a 10% of the elapsed time is due to time spent in the server, the rest
+            // is network / client processing time.
+            long simulatedServerElapsedTimeMillis = totalElapsedTimeMillis / 10;
+            long simulatedClientElapsedTimeMillis = totalElapsedTimeMillis;
+
+            // Create some symmetrical timestamps.
+            Timestamp64 clientRequestTimestamp = Timestamp64.fromInstant(
+                    clientTime.minusMillis(simulatedClientElapsedTimeMillis / 2));
+            Timestamp64 clientResponseTimestamp = Timestamp64.fromInstant(
+                    clientTime.plusMillis(simulatedClientElapsedTimeMillis / 2));
+            Timestamp64 serverReceiveTimestamp = Timestamp64.fromInstant(
+                    serverTime.minusMillis(simulatedServerElapsedTimeMillis / 2));
+            Timestamp64 serverTransmitTimestamp = Timestamp64.fromInstant(
+                    serverTime.plusMillis(simulatedServerElapsedTimeMillis / 2));
+
+            Duration actualOffset = SntpClient.calculateClockOffset(
+                    clientRequestTimestamp, serverReceiveTimestamp,
+                    serverTransmitTimestamp, clientResponseTimestamp);
+
+            // We allow up to 1ms variation because NTP types are lossy and the simulated elapsed
+            // time millis may not divide exactly.
+            int allowedSlopMillis = 1;
+            assertNearlyEquals(
+                    expectedOffset.toMillis(), actualOffset.toMillis(), allowedSlopMillis);
+        }
+    }
+
+    private static Instant utcInstant(
+            int year, int monthOfYear, int day, int hour, int minute, int second, int nanos) {
+        return LocalDateTime.of(year, monthOfYear, day, hour, minute, second, nanos)
+                .toInstant(ZoneOffset.UTC);
     }
 
     @Test
@@ -98,6 +298,8 @@
 
     @Test
     public void testTimeoutFailure() throws Exception {
+        when(mSystemTimeSupplier.get()).thenReturn(LATE_ERA0_REQUEST_TIME);
+
         mServer.clearServerReply();
         assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500, mNetwork));
         assertEquals(1, mServer.numRequestsReceived());
@@ -106,7 +308,9 @@
 
     @Test
     public void testIgnoreLeapNoSync() throws Exception {
-        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
+        when(mSystemTimeSupplier.get()).thenReturn(LATE_ERA0_REQUEST_TIME);
+
+        final byte[] reply = HexEncoding.decode(LATE_ERA_RESPONSE.toCharArray(), false);
         reply[0] |= (byte) 0xc0;
         mServer.setServerReply(reply);
         assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500, mNetwork));
@@ -116,7 +320,9 @@
 
     @Test
     public void testAcceptOnlyServerAndBroadcastModes() throws Exception {
-        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
+        when(mSystemTimeSupplier.get()).thenReturn(LATE_ERA0_REQUEST_TIME);
+
+        final byte[] reply = HexEncoding.decode(LATE_ERA_RESPONSE.toCharArray(), false);
         for (int i = 0; i <= 7; i++) {
             final String logMsg = "mode: " + i;
             reply[0] &= (byte) 0xf8;
@@ -140,10 +346,12 @@
 
     @Test
     public void testAcceptableStrataOnly() throws Exception {
+        when(mSystemTimeSupplier.get()).thenReturn(LATE_ERA0_REQUEST_TIME);
+
         final int STRATUM_MIN = 1;
         final int STRATUM_MAX = 15;
 
-        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
+        final byte[] reply = HexEncoding.decode(LATE_ERA_RESPONSE.toCharArray(), false);
         for (int i = 0; i < 256; i++) {
             final String logMsg = "stratum: " + i;
             reply[1] = (byte) i;
@@ -162,7 +370,9 @@
 
     @Test
     public void testZeroTransmitTime() throws Exception {
-        final byte[] reply = HexEncoding.decode(WORKING_VERSION4.toCharArray(), false);
+        when(mSystemTimeSupplier.get()).thenReturn(LATE_ERA0_REQUEST_TIME);
+
+        final byte[] reply = HexEncoding.decode(LATE_ERA_RESPONSE.toCharArray(), false);
         Arrays.fill(reply, TRANSMIT_TIME_OFFSET, TRANSMIT_TIME_OFFSET + 8, (byte) 0x00);
         mServer.setServerReply(reply);
         assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500, mNetwork));
@@ -170,6 +380,19 @@
         assertEquals(1, mServer.numRepliesSent());
     }
 
+    @Test
+    public void testNonMatchingOriginateTime() throws Exception {
+        when(mSystemTimeSupplier.get()).thenReturn(LATE_ERA0_REQUEST_TIME);
+
+        final byte[] reply = HexEncoding.decode(LATE_ERA_RESPONSE.toCharArray(), false);
+        mServer.setServerReply(reply);
+        mServer.setGenerateValidOriginateTimestamp(false);
+
+        assertFalse(mClient.requestTime(mServer.getAddress(), mServer.getPort(), 500, mNetwork));
+        assertEquals(1, mServer.numRequestsReceived());
+        assertEquals(1, mServer.numRepliesSent());
+    }
+
 
     private static class SntpTestServer {
         private final Object mLock = new Object();
@@ -177,6 +400,7 @@
         private final InetAddress mAddress;
         private final int mPort;
         private byte[] mReply;
+        private boolean mGenerateValidOriginateTimestamp = true;
         private int mRcvd;
         private int mSent;
         private Thread mListeningThread;
@@ -201,10 +425,16 @@
                         synchronized (mLock) {
                             mRcvd++;
                             if (mReply == null) { continue; }
-                            // Copy transmit timestamp into originate timestamp.
-                            // TODO: bounds checking.
-                            System.arraycopy(ntpMsg.getData(), TRANSMIT_TIME_OFFSET,
-                                             mReply, ORIGINATE_TIME_OFFSET, 8);
+                            if (mGenerateValidOriginateTimestamp) {
+                                // Copy the transmit timestamp into originate timestamp: This is
+                                // validated by well-behaved clients.
+                                System.arraycopy(ntpMsg.getData(), TRANSMIT_TIME_OFFSET,
+                                        mReply, ORIGINATE_TIME_OFFSET, 8);
+                            } else {
+                                // Fill it with junk instead.
+                                Arrays.fill(mReply, ORIGINATE_TIME_OFFSET,
+                                        ORIGINATE_TIME_OFFSET + 8, (byte) 0xFF);
+                            }
                             ntpMsg.setData(mReply);
                             ntpMsg.setLength(mReply.length);
                             try {
@@ -245,9 +475,38 @@
             }
         }
 
+        /**
+         * Controls the test server's behavior of copying the client's transmit timestamp into the
+         * response's originate timestamp (which is required of a real server).
+         */
+        public void setGenerateValidOriginateTimestamp(boolean enabled) {
+            synchronized (mLock) {
+                mGenerateValidOriginateTimestamp = enabled;
+            }
+        }
+
         public InetAddress getAddress() { return mAddress; }
         public int getPort() { return mPort; }
         public int numRequestsReceived() { synchronized (mLock) { return mRcvd; } }
         public int numRepliesSent() { synchronized (mLock) { return mSent; } }
     }
+
+    /**
+     * Generates the "real" server time assuming it is exactly between the receive and transmit
+     * timestamp and in the NTP era specified.
+     */
+    private static Instant calculateIdealServerTime(String receiveTimestampString,
+            String transmitTimestampString, int era) {
+        Timestamp64 receiveTimestamp = Timestamp64.fromString(receiveTimestampString);
+        Timestamp64 transmitTimestamp = Timestamp64.fromString(transmitTimestampString);
+        Duration serverProcessingTime =
+                Duration64.between(receiveTimestamp, transmitTimestamp).toDuration();
+        return receiveTimestamp.toInstant(era)
+                .plusMillis(serverProcessingTime.dividedBy(2).toMillis());
+    }
+
+    private static void assertNearlyEquals(long expected, long actual, long allowedSlop) {
+        assertTrue("expected=" + expected + ", actual=" + actual + ", allowedSlop=" + allowedSlop,
+                actual >= expected - allowedSlop && actual <= expected + allowedSlop);
+    }
 }
diff --git a/core/tests/coretests/src/android/net/sntp/Duration64Test.java b/core/tests/coretests/src/android/net/sntp/Duration64Test.java
new file mode 100644
index 0000000..933800f
--- /dev/null
+++ b/core/tests/coretests/src/android/net/sntp/Duration64Test.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.sntp;
+
+import static android.net.sntp.Timestamp64.NANOS_PER_SECOND;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+
+public class Duration64Test {
+
+    @Test
+    public void testBetween_rangeChecks() {
+        long maxDuration64Seconds = Timestamp64.MAX_SECONDS_IN_ERA / 2;
+
+        Timestamp64 zeroNoFrac = Timestamp64.fromComponents(0, 0);
+        assertEquals(Duration64.ZERO, Duration64.between(zeroNoFrac, zeroNoFrac));
+
+        {
+            Timestamp64 ceilNoFrac = Timestamp64.fromComponents(maxDuration64Seconds, 0);
+            assertEquals(Duration64.ZERO, Duration64.between(ceilNoFrac, ceilNoFrac));
+
+            long expectedNanos = maxDuration64Seconds * NANOS_PER_SECOND;
+            assertEquals(Duration.ofNanos(expectedNanos),
+                    Duration64.between(zeroNoFrac, ceilNoFrac).toDuration());
+            assertEquals(Duration.ofNanos(-expectedNanos),
+                    Duration64.between(ceilNoFrac, zeroNoFrac).toDuration());
+        }
+
+        {
+            // This value is the largest fraction of a second representable. It is 1-(1/2^32)), and
+            // so numerically larger than 999_999_999 nanos.
+            int fractionBits = 0xFFFF_FFFF;
+            Timestamp64 ceilWithFrac = Timestamp64
+                    .fromComponents(maxDuration64Seconds, fractionBits);
+            assertEquals(Duration64.ZERO, Duration64.between(ceilWithFrac, ceilWithFrac));
+
+            long expectedNanos = maxDuration64Seconds * NANOS_PER_SECOND + 999_999_999;
+            assertEquals(
+                    Duration.ofNanos(expectedNanos),
+                    Duration64.between(zeroNoFrac, ceilWithFrac).toDuration());
+            // The -1 nanos demonstrates asymmetry due to the way Duration64 has different
+            // precision / range of sub-second fractions.
+            assertEquals(
+                    Duration.ofNanos(-expectedNanos - 1),
+                    Duration64.between(ceilWithFrac, zeroNoFrac).toDuration());
+        }
+    }
+
+    @Test
+    public void testBetween_smallSecondsOnly() {
+        long expectedNanos = 5L * NANOS_PER_SECOND;
+        assertEquals(Duration.ofNanos(expectedNanos),
+                Duration64.between(Timestamp64.fromComponents(5, 0),
+                        Timestamp64.fromComponents(10, 0))
+                        .toDuration());
+        assertEquals(Duration.ofNanos(-expectedNanos),
+                Duration64.between(Timestamp64.fromComponents(10, 0),
+                        Timestamp64.fromComponents(5, 0))
+                        .toDuration());
+    }
+
+    @Test
+    public void testBetween_smallSecondsAndFraction() {
+        // Choose a nanos values we know can be represented exactly with fixed point binary (1/2
+        // second, 1/4 second, etc.).
+        {
+            long expectedNanos = 5L * NANOS_PER_SECOND + 500_000_000L;
+            int fractionHalfSecond = 0x8000_0000;
+            assertEquals(Duration.ofNanos(expectedNanos),
+                    Duration64.between(
+                            Timestamp64.fromComponents(5, 0),
+                            Timestamp64.fromComponents(10, fractionHalfSecond)).toDuration());
+            assertEquals(Duration.ofNanos(-expectedNanos),
+                    Duration64.between(
+                            Timestamp64.fromComponents(10, fractionHalfSecond),
+                            Timestamp64.fromComponents(5, 0)).toDuration());
+        }
+
+        {
+            long expectedNanos = 5L * NANOS_PER_SECOND + 250_000_000L;
+            int fractionHalfSecond = 0x8000_0000;
+            int fractionQuarterSecond = 0x4000_0000;
+
+            assertEquals(Duration.ofNanos(expectedNanos),
+                    Duration64.between(
+                            Timestamp64.fromComponents(5, fractionQuarterSecond),
+                            Timestamp64.fromComponents(10, fractionHalfSecond)).toDuration());
+            assertEquals(Duration.ofNanos(-expectedNanos),
+                    Duration64.between(
+                            Timestamp64.fromComponents(10, fractionHalfSecond),
+                            Timestamp64.fromComponents(5, fractionQuarterSecond)).toDuration());
+        }
+
+    }
+
+    @Test
+    public void testBetween_sameEra0() {
+        int arbitraryEra0Year = 2021;
+        Instant one = utcInstant(arbitraryEra0Year, 1, 1, 0, 0, 0, 500);
+        assertNtpEraOfInstant(one, 0);
+
+        checkDuration64Behavior(one, one);
+
+        Instant two = utcInstant(arbitraryEra0Year + 1, 1, 1, 0, 0, 0, 250);
+        assertNtpEraOfInstant(two, 0);
+
+        checkDuration64Behavior(one, two);
+        checkDuration64Behavior(two, one);
+    }
+
+    @Test
+    public void testBetween_sameEra1() {
+        int arbitraryEra1Year = 2037;
+        Instant one = utcInstant(arbitraryEra1Year, 1, 1, 0, 0, 0, 500);
+        assertNtpEraOfInstant(one, 1);
+
+        checkDuration64Behavior(one, one);
+
+        Instant two = utcInstant(arbitraryEra1Year + 1, 1, 1, 0, 0, 0, 250);
+        assertNtpEraOfInstant(two, 1);
+
+        checkDuration64Behavior(one, two);
+        checkDuration64Behavior(two, one);
+    }
+
+    /**
+     * Tests that two timestamps can originate from times in different eras, and the works
+     * calculation still works providing the two times aren't more than 68 years apart (half of the
+     * 136 years representable using an unsigned 32-bit seconds representation).
+     */
+    @Test
+    public void testBetween_adjacentEras() {
+        int yearsSeparation = 68;
+
+        // This year just needs to be < 68 years before the end of NTP timestamp era 0.
+        int arbitraryYearInEra0 = 2021;
+
+        Instant one = utcInstant(arbitraryYearInEra0, 1, 1, 0, 0, 0, 500);
+        assertNtpEraOfInstant(one, 0);
+
+        checkDuration64Behavior(one, one);
+
+        Instant two = utcInstant(arbitraryYearInEra0 + yearsSeparation, 1, 1, 0, 0, 0, 250);
+        assertNtpEraOfInstant(two, 1);
+
+        checkDuration64Behavior(one, two);
+        checkDuration64Behavior(two, one);
+    }
+
+    /**
+     * This test confirms that duration calculations fail in the expected fashion if two
+     * Timestamp64s are more than 2^31 seconds apart.
+     *
+     * <p>The types / math specified by NTP for timestamps deliberately takes place in 64-bit signed
+     * arithmetic for the bits used to represent timestamps (32-bit unsigned integer seconds,
+     * 32-bits fixed point for fraction of seconds). Timestamps can therefore represent ~136 years
+     * of seconds.
+     * When subtracting one timestamp from another, we end up with a signed 32-bit seconds value.
+     * This means the max duration representable is ~68 years before numbers will over or underflow.
+     * i.e. the client and server are in the same or adjacent NTP eras and the difference in their
+     * clocks isn't more than ~68 years. >= ~68 years and things break down.
+     */
+    @Test
+    public void testBetween_tooFarApart() {
+        int tooManyYearsSeparation = 68 + 1;
+
+        Instant one = utcInstant(2021, 1, 1, 0, 0, 0, 500);
+        assertNtpEraOfInstant(one, 0);
+        Instant two = utcInstant(2021 + tooManyYearsSeparation, 1, 1, 0, 0, 0, 250);
+        assertNtpEraOfInstant(two, 1);
+
+        checkDuration64OverflowBehavior(one, two);
+        checkDuration64OverflowBehavior(two, one);
+    }
+
+    private static void checkDuration64Behavior(Instant one, Instant two) {
+        // This is the answer if we perform the arithmetic in a lossless fashion.
+        Duration expectedDuration = Duration.between(one, two);
+        Duration64 expectedDuration64 = Duration64.fromDuration(expectedDuration);
+
+        // Sub-second precision is limited in Timestamp64, so we can lose 1ms.
+        assertEqualsOrSlightlyLessThan(
+                expectedDuration.toMillis(), expectedDuration64.toDuration().toMillis());
+
+        Timestamp64 one64 = Timestamp64.fromInstant(one);
+        Timestamp64 two64 = Timestamp64.fromInstant(two);
+
+        // This is the answer if we perform the arithmetic in a lossy fashion.
+        Duration64 actualDuration64 = Duration64.between(one64, two64);
+        assertEquals(expectedDuration64.getSeconds(), actualDuration64.getSeconds());
+        assertEqualsOrSlightlyLessThan(expectedDuration64.getNanos(), actualDuration64.getNanos());
+    }
+
+    private static void checkDuration64OverflowBehavior(Instant one, Instant two) {
+        // This is the answer if we perform the arithmetic in a lossless fashion.
+        Duration trueDuration = Duration.between(one, two);
+
+        // Confirm the maths is expected to overflow / underflow.
+        assertTrue(trueDuration.getSeconds() > Integer.MAX_VALUE / 2
+                || trueDuration.getSeconds() < Integer.MIN_VALUE / 2);
+
+        // Now perform the arithmetic as specified for NTP: do subtraction using the 64-bit
+        // timestamp.
+        Timestamp64 one64 = Timestamp64.fromInstant(one);
+        Timestamp64 two64 = Timestamp64.fromInstant(two);
+
+        Duration64 actualDuration64 = Duration64.between(one64, two64);
+        assertNotEquals(trueDuration.getSeconds(), actualDuration64.getSeconds());
+    }
+
+    /**
+     * Asserts the instant provided is in the specified NTP timestamp era. Used to confirm /
+     * document values picked for tests have the properties needed.
+     */
+    private static void assertNtpEraOfInstant(Instant one, int ntpEra) {
+        long expectedSeconds = one.getEpochSecond();
+
+        // The conversion to Timestamp64 is lossy (it loses the era). We then supply the expected
+        // era. If the era was correct, we will end up with the value we started with (modulo nano
+        // precision loss). If the era is wrong, we won't.
+        Instant roundtrippedInstant = Timestamp64.fromInstant(one).toInstant(ntpEra);
+
+        long actualSeconds = roundtrippedInstant.getEpochSecond();
+        assertEquals(expectedSeconds, actualSeconds);
+    }
+
+    /**
+     * Used to account for the fact that NTP types used 32-bit fixed point storage, so cannot store
+     * all values precisely. The value we get out will always be the value we put in, or one that is
+     * one unit smaller (due to truncation).
+     */
+    private static void assertEqualsOrSlightlyLessThan(long expected, long actual) {
+        assertTrue("expected=" + expected + ", actual=" + actual,
+                expected == actual || expected == actual - 1);
+    }
+
+    private static Instant utcInstant(
+            int year, int monthOfYear, int day, int hour, int minute, int second, int nanos) {
+        return LocalDateTime.of(year, monthOfYear, day, hour, minute, second, nanos)
+                .toInstant(ZoneOffset.UTC);
+    }
+}
diff --git a/core/tests/coretests/src/android/net/sntp/OWNERS b/core/tests/coretests/src/android/net/sntp/OWNERS
new file mode 100644
index 0000000..232c2eb
--- /dev/null
+++ b/core/tests/coretests/src/android/net/sntp/OWNERS
@@ -0,0 +1 @@
+include /core/java/android/net/sntp/OWNERS
diff --git a/core/tests/coretests/src/android/net/sntp/PredictableRandom.java b/core/tests/coretests/src/android/net/sntp/PredictableRandom.java
new file mode 100644
index 0000000..bb2922b
--- /dev/null
+++ b/core/tests/coretests/src/android/net/sntp/PredictableRandom.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.sntp;
+
+import java.util.Random;
+
+class PredictableRandom extends Random {
+    private int[] mIntSequence = new int[] { 1 };
+    private int mIntPos = 0;
+
+    public void setIntSequence(int[] intSequence) {
+        this.mIntSequence = intSequence;
+    }
+
+    @Override
+    public int nextInt() {
+        int value = mIntSequence[mIntPos++];
+        mIntPos %= mIntSequence.length;
+        return value;
+    }
+}
diff --git a/core/tests/coretests/src/android/net/sntp/Timestamp64Test.java b/core/tests/coretests/src/android/net/sntp/Timestamp64Test.java
new file mode 100644
index 0000000..c923812
--- /dev/null
+++ b/core/tests/coretests/src/android/net/sntp/Timestamp64Test.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.sntp;
+
+import static android.net.sntp.Timestamp64.NANOS_PER_SECOND;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.Random;
+import java.util.Set;
+
+public class Timestamp64Test {
+
+    @Test
+    public void testFromComponents() {
+        long minNtpEraSeconds = 0;
+        long maxNtpEraSeconds = 0xFFFFFFFFL;
+
+        expectIllegalArgumentException(() -> Timestamp64.fromComponents(minNtpEraSeconds - 1, 0));
+        expectIllegalArgumentException(() -> Timestamp64.fromComponents(maxNtpEraSeconds + 1, 0));
+
+        assertComponentCreation(minNtpEraSeconds, 0);
+        assertComponentCreation(maxNtpEraSeconds, 0);
+        assertComponentCreation(maxNtpEraSeconds, Integer.MIN_VALUE);
+        assertComponentCreation(maxNtpEraSeconds, Integer.MAX_VALUE);
+    }
+
+    private static void assertComponentCreation(long ntpEraSeconds, int fractionBits) {
+        Timestamp64 value = Timestamp64.fromComponents(ntpEraSeconds, fractionBits);
+        assertEquals(ntpEraSeconds, value.getEraSeconds());
+        assertEquals(fractionBits, value.getFractionBits());
+    }
+
+    @Test
+    public void testEqualsAndHashcode() {
+        assertEqualsAndHashcode(0, 0);
+        assertEqualsAndHashcode(1, 0);
+        assertEqualsAndHashcode(0, 1);
+    }
+
+    private static void assertEqualsAndHashcode(int eraSeconds, int fractionBits) {
+        Timestamp64 one = Timestamp64.fromComponents(eraSeconds, fractionBits);
+        Timestamp64 two = Timestamp64.fromComponents(eraSeconds, fractionBits);
+        assertEquals(one, two);
+        assertEquals(one.hashCode(), two.hashCode());
+    }
+
+    @Test
+    public void testStringForm() {
+        expectIllegalArgumentException(() -> Timestamp64.fromString(""));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("."));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("1234567812345678"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("12345678?12345678"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("12345678..12345678"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("1.12345678"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("12.12345678"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("123456.12345678"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("1234567.12345678"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("12345678.1"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("12345678.12"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("12345678.123456"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("12345678.1234567"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("X2345678.12345678"));
+        expectIllegalArgumentException(() -> Timestamp64.fromString("12345678.X2345678"));
+
+        assertStringCreation("00000000.00000000", 0, 0);
+        assertStringCreation("00000001.00000001", 1, 1);
+        assertStringCreation("ffffffff.ffffffff", 0xFFFFFFFFL, 0xFFFFFFFF);
+    }
+
+    private static void assertStringCreation(
+            String string, long expectedSeconds, int expectedFractionBits) {
+        Timestamp64 timestamp64 = Timestamp64.fromString(string);
+        assertEquals(string, timestamp64.toString());
+        assertEquals(expectedSeconds, timestamp64.getEraSeconds());
+        assertEquals(expectedFractionBits, timestamp64.getFractionBits());
+    }
+
+    @Test
+    public void testStringForm_lenientHexCasing() {
+        Timestamp64 mixedCaseValue = Timestamp64.fromString("AaBbCcDd.EeFf1234");
+        assertEquals(0xAABBCCDDL, mixedCaseValue.getEraSeconds());
+        assertEquals(0xEEFF1234, mixedCaseValue.getFractionBits());
+    }
+
+    @Test
+    public void testFromInstant_secondsHandling() {
+        final int era0 = 0;
+        final int eraNeg1 = -1;
+        final int eraNeg2 = -2;
+        final int era1 = 1;
+
+        assertInstantCreationOnlySeconds(-Timestamp64.OFFSET_1900_TO_1970, 0, era0);
+        assertInstantCreationOnlySeconds(
+                -Timestamp64.OFFSET_1900_TO_1970 - Timestamp64.SECONDS_IN_ERA, 0, eraNeg1);
+        assertInstantCreationOnlySeconds(
+                -Timestamp64.OFFSET_1900_TO_1970 + Timestamp64.SECONDS_IN_ERA, 0, era1);
+
+        assertInstantCreationOnlySeconds(
+                -Timestamp64.OFFSET_1900_TO_1970 - 1, Timestamp64.MAX_SECONDS_IN_ERA, -1);
+        assertInstantCreationOnlySeconds(
+                -Timestamp64.OFFSET_1900_TO_1970 - Timestamp64.SECONDS_IN_ERA - 1,
+                Timestamp64.MAX_SECONDS_IN_ERA, eraNeg2);
+        assertInstantCreationOnlySeconds(
+                -Timestamp64.OFFSET_1900_TO_1970 + Timestamp64.SECONDS_IN_ERA - 1,
+                Timestamp64.MAX_SECONDS_IN_ERA, era0);
+
+        assertInstantCreationOnlySeconds(-Timestamp64.OFFSET_1900_TO_1970 + 1, 1, era0);
+        assertInstantCreationOnlySeconds(
+                -Timestamp64.OFFSET_1900_TO_1970 - Timestamp64.SECONDS_IN_ERA + 1, 1, eraNeg1);
+        assertInstantCreationOnlySeconds(
+                -Timestamp64.OFFSET_1900_TO_1970 + Timestamp64.SECONDS_IN_ERA + 1, 1, era1);
+
+        assertInstantCreationOnlySeconds(0, Timestamp64.OFFSET_1900_TO_1970, era0);
+        assertInstantCreationOnlySeconds(
+                -Timestamp64.SECONDS_IN_ERA, Timestamp64.OFFSET_1900_TO_1970, eraNeg1);
+        assertInstantCreationOnlySeconds(
+                Timestamp64.SECONDS_IN_ERA, Timestamp64.OFFSET_1900_TO_1970, era1);
+
+        assertInstantCreationOnlySeconds(1, Timestamp64.OFFSET_1900_TO_1970 + 1, era0);
+        assertInstantCreationOnlySeconds(
+                -Timestamp64.SECONDS_IN_ERA + 1, Timestamp64.OFFSET_1900_TO_1970 + 1, eraNeg1);
+        assertInstantCreationOnlySeconds(
+                Timestamp64.SECONDS_IN_ERA + 1, Timestamp64.OFFSET_1900_TO_1970 + 1, era1);
+
+        assertInstantCreationOnlySeconds(-1, Timestamp64.OFFSET_1900_TO_1970 - 1, era0);
+        assertInstantCreationOnlySeconds(
+                -Timestamp64.SECONDS_IN_ERA - 1, Timestamp64.OFFSET_1900_TO_1970 - 1, eraNeg1);
+        assertInstantCreationOnlySeconds(
+                Timestamp64.SECONDS_IN_ERA - 1, Timestamp64.OFFSET_1900_TO_1970 - 1, era1);
+    }
+
+    private static void assertInstantCreationOnlySeconds(
+            long epochSeconds, long expectedNtpEraSeconds, int ntpEra) {
+        int nanosOfSecond = 0;
+        Instant instant = Instant.ofEpochSecond(epochSeconds, nanosOfSecond);
+        Timestamp64 timestamp = Timestamp64.fromInstant(instant);
+        assertEquals(expectedNtpEraSeconds, timestamp.getEraSeconds());
+
+        int expectedFractionBits = 0;
+        assertEquals(expectedFractionBits, timestamp.getFractionBits());
+
+        // Confirm the Instant can be round-tripped if we know the era. Also assumes the nanos can
+        // be stored precisely; 0 can be.
+        Instant roundTrip = timestamp.toInstant(ntpEra);
+        assertEquals(instant, roundTrip);
+    }
+
+    @Test
+    public void testFromInstant_fractionHandling() {
+        // Try some values we know can be represented exactly.
+        assertInstantCreationOnlyFractionExact(0x0, 0);
+        assertInstantCreationOnlyFractionExact(0x80000000, 500_000_000L);
+        assertInstantCreationOnlyFractionExact(0x40000000, 250_000_000L);
+
+        // Test the limits of precision.
+        assertInstantCreationOnlyFractionExact(0x00000006, 1L);
+        assertInstantCreationOnlyFractionExact(0x00000005, 1L);
+        assertInstantCreationOnlyFractionExact(0x00000004, 0L);
+        assertInstantCreationOnlyFractionExact(0x00000002, 0L);
+        assertInstantCreationOnlyFractionExact(0x00000001, 0L);
+
+        // Confirm nanosecond storage / precision is within 1ns.
+        final boolean exhaustive = false;
+        for (int i = 0; i < NANOS_PER_SECOND; i++) {
+            Instant instant = Instant.ofEpochSecond(0, i);
+            Instant roundTripped = Timestamp64.fromInstant(instant).toInstant(0);
+            assertNanosWithTruncationAllowed(i, roundTripped);
+            if (!exhaustive) {
+                i += 999_999;
+            }
+        }
+    }
+
+    private static void assertInstantCreationOnlyFractionExact(
+            int fractionBits, long expectedNanos) {
+        Timestamp64 timestamp64 = Timestamp64.fromComponents(0, fractionBits);
+
+        final int ntpEra = 0;
+        Instant instant = timestamp64.toInstant(ntpEra);
+
+        assertEquals(expectedNanos, instant.getNano());
+    }
+
+    private static void assertNanosWithTruncationAllowed(long expectedNanos, Instant instant) {
+        // Allow for < 1ns difference due to truncation.
+        long actualNanos = instant.getNano();
+        assertTrue("expectedNanos=" + expectedNanos + ",  actualNanos=" + actualNanos,
+                actualNanos == expectedNanos || actualNanos == expectedNanos - 1);
+    }
+
+    @Test
+    public void testMillisRandomizationConstant() {
+        // Mathematically, we can say that to represent 1000 different values, we need 10 binary
+        // digits (2^10 = 1024). The same is true whether we're dealing with integers or fractions.
+        // Unfortunately, for fractions those 1024 values do not correspond to discrete decimal
+        // values. Discrete millisecond values as fractions (e.g. 0.001 - 0.999) cannot be
+        // represented exactly except where the value can also be represented as some combination of
+        // powers of -2. When we convert back and forth, we truncate, so millisecond decimal
+        // fraction N represented as a binary fraction will always be equal to or lower than N. If
+        // we are truncating correctly it will never be as low as (N-0.001). N -> [N-0.001, N].
+
+        // We need to keep 10 bits to hold millis (inaccurately, since there are numbers that
+        // cannot be represented exactly), leaving us able to randomize the remaining 22 bits of the
+        // fraction part without significantly affecting the number represented.
+        assertEquals(22, Timestamp64.SUB_MILLIS_BITS_TO_RANDOMIZE);
+
+        // Brute force proof that randomization logic will keep the timestamp within the range
+        // [N-0.001, N] where x is in milliseconds.
+        int smallFractionRandomizedLow = 0;
+        int smallFractionRandomizedHigh = 0b00000000_00111111_11111111_11111111;
+        int largeFractionRandomizedLow = 0b11111111_11000000_00000000_00000000;
+        int largeFractionRandomizedHigh = 0b11111111_11111111_11111111_11111111;
+
+        long smallLowNanos = Timestamp64.fromComponents(
+                0, smallFractionRandomizedLow).toInstant(0).getNano();
+        long smallHighNanos = Timestamp64.fromComponents(
+                0, smallFractionRandomizedHigh).toInstant(0).getNano();
+        long smallDelta = smallHighNanos - smallLowNanos;
+        long millisInNanos = 1_000_000_000 / 1_000;
+        assertTrue(smallDelta >= 0 && smallDelta < millisInNanos);
+
+        long largeLowNanos = Timestamp64.fromComponents(
+                0, largeFractionRandomizedLow).toInstant(0).getNano();
+        long largeHighNanos = Timestamp64.fromComponents(
+                0, largeFractionRandomizedHigh).toInstant(0).getNano();
+        long largeDelta = largeHighNanos - largeLowNanos;
+        assertTrue(largeDelta >= 0 && largeDelta < millisInNanos);
+
+        PredictableRandom random = new PredictableRandom();
+        random.setIntSequence(new int[] { 0xFFFF_FFFF });
+        Timestamp64 zero = Timestamp64.fromComponents(0, 0);
+        Timestamp64 zeroWithFractionRandomized = zero.randomizeSubMillis(random);
+        assertEquals(zero.getEraSeconds(), zeroWithFractionRandomized.getEraSeconds());
+        assertEquals(smallFractionRandomizedHigh, zeroWithFractionRandomized.getFractionBits());
+    }
+
+    @Test
+    public void testRandomizeLowestBits() {
+        Random random = new Random(1);
+        {
+            int fractionBits = 0;
+            expectIllegalArgumentException(
+                    () -> Timestamp64.randomizeLowestBits(random, fractionBits, -1));
+            expectIllegalArgumentException(
+                    () -> Timestamp64.randomizeLowestBits(random, fractionBits, 0));
+            expectIllegalArgumentException(
+                    () -> Timestamp64.randomizeLowestBits(random, fractionBits, Integer.SIZE));
+            expectIllegalArgumentException(
+                    () -> Timestamp64.randomizeLowestBits(random, fractionBits, Integer.SIZE + 1));
+        }
+
+        // Check the behavior looks correct from a probabilistic point of view.
+        for (int input : new int[] { 0, 0xFFFFFFFF }) {
+            for (int bitCount = 1; bitCount < Integer.SIZE; bitCount++) {
+                int upperBitMask = 0xFFFFFFFF << bitCount;
+                int expectedUpperBits = input & upperBitMask;
+
+                Set<Integer> values = new HashSet<>();
+                values.add(input);
+
+                int trials = 100;
+                for (int i = 0; i < trials; i++) {
+                    int outputFractionBits =
+                            Timestamp64.randomizeLowestBits(random, input, bitCount);
+
+                    // Record the output value for later analysis.
+                    values.add(outputFractionBits);
+
+                    // Check upper bits did not change.
+                    assertEquals(expectedUpperBits, outputFractionBits & upperBitMask);
+                }
+
+                // It's possible to be more rigorous here, perhaps with a histogram. As bitCount
+                // rises, values.size() quickly trend towards the value of trials + 1. For now, this
+                // mostly just guards against a no-op implementation.
+                assertTrue(bitCount + ":" + values.size(), values.size() > 1);
+            }
+        }
+    }
+
+    private static void expectIllegalArgumentException(Runnable r) {
+        try {
+            r.run();
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java
index 9d2cab3..ddd0070 100644
--- a/core/tests/coretests/src/android/os/BundleTest.java
+++ b/core/tests/coretests/src/android/os/BundleTest.java
@@ -47,6 +47,7 @@
 
     @After
     public void tearDown() throws Exception {
+        BaseBundle.setShouldDefuse(false);
         if (mWtfHandler != null) {
             Log.setWtfHandler(mWtfHandler);
         }
@@ -355,6 +356,81 @@
         assertThat(e.getCause()).isInstanceOf(Log.TerribleFailure.class);
     }
 
+    @Test
+    public void getParcelable_whenThrowingAndNotDefusing_throws() throws Exception {
+        Bundle.setShouldDefuse(false);
+        Bundle bundle = new Bundle();
+        bundle.putParcelable("key", new CustomParcelable(13, "Tiramisu"));
+        bundle.readFromParcel(getParcelledBundle(bundle));
+
+        // Default class-loader is the bootpath class-loader, which doesn't contain
+        // CustomParcelable, so trying to read it will throw BadParcelableException.
+        assertThrows(BadParcelableException.class, () -> bundle.getParcelable("key"));
+    }
+
+    @Test
+    public void getParcelable_whenThrowingAndDefusing_returnsNull() throws Exception {
+        Bundle.setShouldDefuse(true);
+        Bundle bundle = new Bundle();
+        bundle.putParcelable("key", new CustomParcelable(13, "Tiramisu"));
+        bundle.putString("string", "value");
+        bundle.readFromParcel(getParcelledBundle(bundle));
+
+        // Default class-loader is the bootpath class-loader, which doesn't contain
+        // CustomParcelable, so trying to read it will throw BadParcelableException.
+        assertThat(bundle.<Parcelable>getParcelable("key")).isNull();
+        // Doesn't affect other items
+        assertThat(bundle.getString("string")).isEqualTo("value");
+    }
+
+    @Test
+    public void getParcelable_whenThrowingAndDefusing_leavesElement() throws Exception {
+        Bundle.setShouldDefuse(true);
+        Bundle bundle = new Bundle();
+        Parcelable parcelable = new CustomParcelable(13, "Tiramisu");
+        bundle.putParcelable("key", parcelable);
+        bundle.putString("string", "value");
+        bundle.readFromParcel(getParcelledBundle(bundle));
+        assertThat(bundle.<Parcelable>getParcelable("key")).isNull();
+
+        // Now, we simulate reserializing and assign the proper class loader to not throw anymore
+        bundle.readFromParcel(getParcelledBundle(bundle));
+        bundle.setClassLoader(getClass().getClassLoader());
+
+        // We're able to retrieve it even though we failed before
+        assertThat(bundle.<Parcelable>getParcelable("key")).isEqualTo(parcelable);
+    }
+
+    @Test
+    public void partialDeserialization_whenNotDefusing_throws() throws Exception {
+        Bundle.setShouldDefuse(false);
+        Bundle bundle = getMalformedBundle();
+        assertThrows(BadParcelableException.class, bundle::isEmpty);
+    }
+
+    @Test
+    public void partialDeserialization_whenDefusing_emptiesMap() throws Exception {
+        Bundle.setShouldDefuse(true);
+        Bundle bundle = getMalformedBundle();
+        bundle.isEmpty();
+        // Nothing thrown
+        assertThat(bundle.size()).isEqualTo(0);
+    }
+
+    private Bundle getMalformedBundle() {
+        Parcel p = Parcel.obtain();
+        p.writeInt(BaseBundle.BUNDLE_MAGIC);
+        int start = p.dataPosition();
+        p.writeInt(1); // Number of items
+        p.writeString("key");
+        p.writeInt(131313); // Invalid type
+        p.writeInt(0); // Anything, really
+        int end = p.dataPosition();
+        p.setDataPosition(0);
+        return new Bundle(p, end - start);
+    }
+
+
     private static class CustomParcelable implements Parcelable {
         public final int integer;
         public final String string;
diff --git a/core/tests/coretests/src/android/os/OWNERS b/core/tests/coretests/src/android/os/OWNERS
index 9a9b474..a42285e 100644
--- a/core/tests/coretests/src/android/os/OWNERS
+++ b/core/tests/coretests/src/android/os/OWNERS
@@ -2,11 +2,11 @@
 per-file BrightnessLimit.java = michaelwr@google.com, santoscordon@google.com
 
 # Haptics
-per-file CombinedVibrationEffectTest.java = michaelwr@google.com
-per-file ExternalVibrationTest.java = michaelwr@google.com
-per-file VibrationEffectTest.java = michaelwr@google.com
-per-file VibratorInfoTest.java = michaelwr@google.com
-per-file VibratorTest.java = michaelwr@google.com
+per-file CombinedVibrationEffectTest.java = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file ExternalVibrationTest.java = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file VibrationEffectTest.java = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file VibratorInfoTest.java = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file VibratorTest.java = file:/services/core/java/com/android/server/vibrator/OWNERS
 
 # Power
-per-file PowerManager*.java = michaelwr@google.com, santoscordon@google.com
+per-file PowerManager*.java = michaelwr@google.com, santoscordon@google.com
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/service/timezone/OWNERS b/core/tests/coretests/src/android/service/timezone/OWNERS
new file mode 100644
index 0000000..8116388
--- /dev/null
+++ b/core/tests/coretests/src/android/service/timezone/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 847766
+include /core/java/android/service/timezone/OWNERS
diff --git a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java
new file mode 100644
index 0000000..c8de190
--- /dev/null
+++ b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderEventTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.timezone;
+
+import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+public class TimeZoneProviderEventTest {
+
+    @Test
+    public void isEquivalentToAndEquals() {
+        TimeZoneProviderEvent fail1v1 =
+                TimeZoneProviderEvent.createPermanentFailureEvent(1111L, "one");
+        assertEquals(fail1v1, fail1v1);
+        assertIsEquivalentTo(fail1v1, fail1v1);
+        assertNotEquals(fail1v1, null);
+        assertNotEquivalentTo(fail1v1, null);
+
+        {
+            TimeZoneProviderEvent fail1v2 =
+                    TimeZoneProviderEvent.createPermanentFailureEvent(1111L, "one");
+            assertEquals(fail1v1, fail1v2);
+            assertIsEquivalentTo(fail1v1, fail1v2);
+
+            TimeZoneProviderEvent fail2 =
+                    TimeZoneProviderEvent.createPermanentFailureEvent(2222L, "two");
+            assertNotEquals(fail1v1, fail2);
+            assertIsEquivalentTo(fail1v1, fail2);
+        }
+
+        TimeZoneProviderEvent uncertain1v1 = TimeZoneProviderEvent.createUncertainEvent(1111L);
+        assertEquals(uncertain1v1, uncertain1v1);
+        assertIsEquivalentTo(uncertain1v1, uncertain1v1);
+        assertNotEquals(uncertain1v1, null);
+        assertNotEquivalentTo(uncertain1v1, null);
+
+        {
+            TimeZoneProviderEvent uncertain1v2 = TimeZoneProviderEvent.createUncertainEvent(1111L);
+            assertEquals(uncertain1v1, uncertain1v2);
+            assertIsEquivalentTo(uncertain1v1, uncertain1v2);
+
+            TimeZoneProviderEvent uncertain2 = TimeZoneProviderEvent.createUncertainEvent(2222L);
+            assertNotEquals(uncertain1v1, uncertain2);
+            assertIsEquivalentTo(uncertain1v1, uncertain2);
+        }
+
+        TimeZoneProviderSuggestion suggestion1 = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(1111L)
+                .setTimeZoneIds(Collections.singletonList("Europe/London"))
+                .build();
+        TimeZoneProviderEvent certain1v1 =
+                TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1);
+        assertEquals(certain1v1, certain1v1);
+        assertIsEquivalentTo(certain1v1, certain1v1);
+        assertNotEquals(certain1v1, null);
+        assertNotEquivalentTo(certain1v1, null);
+
+        {
+            // Same suggestion, same time.
+            TimeZoneProviderEvent certain1v2 =
+                    TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion1);
+            assertEquals(certain1v1, certain1v2);
+            assertIsEquivalentTo(certain1v1, certain1v2);
+
+            // Same suggestion, different time.
+            TimeZoneProviderEvent certain1v3 =
+                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion1);
+            assertNotEquals(certain1v1, certain1v3);
+            assertIsEquivalentTo(certain1v1, certain1v3);
+
+            // suggestion1 is equivalent to suggestion2, but not equal
+            TimeZoneProviderSuggestion suggestion2 = new TimeZoneProviderSuggestion.Builder()
+                    .setElapsedRealtimeMillis(2222L)
+                    .setTimeZoneIds(Collections.singletonList("Europe/London"))
+                    .build();
+            assertNotEquals(suggestion1, suggestion2);
+            TimeZoneProviderSuggestionTest.assertIsEquivalentTo(suggestion1, suggestion2);
+            TimeZoneProviderEvent certain2 =
+                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion2);
+            assertNotEquals(certain1v1, certain2);
+            assertIsEquivalentTo(certain1v1, certain2);
+
+            // suggestion3 is not equivalent to suggestion1
+            TimeZoneProviderSuggestion suggestion3 = new TimeZoneProviderSuggestion.Builder()
+                    .setTimeZoneIds(Collections.singletonList("Europe/Paris"))
+                    .build();
+            TimeZoneProviderEvent certain3 =
+                    TimeZoneProviderEvent.createSuggestionEvent(2222L, suggestion3);
+            assertNotEquals(certain1v1, certain3);
+            assertNotEquivalentTo(certain1v1, certain3);
+        }
+
+        assertNotEquals(fail1v1, uncertain1v1);
+        assertNotEquivalentTo(fail1v1, uncertain1v1);
+
+        assertNotEquals(fail1v1, certain1v1);
+        assertNotEquivalentTo(fail1v1, certain1v1);
+    }
+
+    @Test
+    public void testParcelable_failureEvent() {
+        TimeZoneProviderEvent event =
+                TimeZoneProviderEvent.createPermanentFailureEvent(1111L, "failure reason");
+        assertRoundTripParcelable(event);
+    }
+
+    @Test
+    public void testParcelable_uncertain() {
+        TimeZoneProviderEvent event = TimeZoneProviderEvent.createUncertainEvent(1111L);
+        assertRoundTripParcelable(event);
+    }
+
+    @Test
+    public void testParcelable_suggestion() {
+        TimeZoneProviderSuggestion suggestion = new TimeZoneProviderSuggestion.Builder()
+                .setTimeZoneIds(Arrays.asList("Europe/London", "Europe/Paris"))
+                .build();
+        TimeZoneProviderEvent event =
+                TimeZoneProviderEvent.createSuggestionEvent(1111L, suggestion);
+        assertRoundTripParcelable(event);
+    }
+
+    private static void assertNotEquivalentTo(
+            TimeZoneProviderEvent one, TimeZoneProviderEvent two) {
+        if (one == null && two == null) {
+            fail("null arguments");
+        }
+        if (one != null) {
+            assertFalse("one=" + one + ", two=" + two, one.isEquivalentTo(two));
+        }
+        if (two != null) {
+            assertFalse("one=" + one + ", two=" + two, two.isEquivalentTo(one));
+        }
+    }
+
+    private static void assertIsEquivalentTo(TimeZoneProviderEvent one, TimeZoneProviderEvent two) {
+        if (one == null || two == null) {
+            fail("null arguments");
+        }
+        assertTrue("one=" + one + ", two=" + two, one.isEquivalentTo(two));
+        assertTrue("one=" + one + ", two=" + two, two.isEquivalentTo(one));
+    }
+}
diff --git a/core/tests/coretests/src/android/service/timezone/TimeZoneProviderSuggestionTest.java b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderSuggestionTest.java
new file mode 100644
index 0000000..e800739
--- /dev/null
+++ b/core/tests/coretests/src/android/service/timezone/TimeZoneProviderSuggestionTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.service.timezone;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import java.util.Collections;
+
+/** Tests for non-API methods */
+public class TimeZoneProviderSuggestionTest {
+
+    @Test
+    public void isEquivalentToAndEquals() {
+        TimeZoneProviderSuggestion suggestion1 = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(1111L)
+                .setTimeZoneIds(Collections.singletonList("Europe/London"))
+                .build();
+        assertEquals(suggestion1, suggestion1);
+        assertIsEquivalentTo(suggestion1, suggestion1);
+        assertNotEquals(suggestion1, null);
+        assertNotEquivalentTo(suggestion1, null);
+
+        // Same time zone IDs, different time.
+        TimeZoneProviderSuggestion suggestion2 = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(2222L)
+                .setTimeZoneIds(Collections.singletonList("Europe/London"))
+                .build();
+        assertNotEquals(suggestion1, suggestion2);
+        assertIsEquivalentTo(suggestion1, suggestion2);
+
+        // Different time zone IDs.
+        TimeZoneProviderSuggestion suggestion3 = new TimeZoneProviderSuggestion.Builder()
+                .setElapsedRealtimeMillis(1111L)
+                .setTimeZoneIds(Collections.singletonList("Europe/Paris"))
+                .build();
+        assertNotEquals(suggestion1, suggestion3);
+        assertNotEquivalentTo(suggestion1, suggestion3);
+    }
+
+    static void assertNotEquivalentTo(
+            TimeZoneProviderSuggestion one, TimeZoneProviderSuggestion two) {
+        if (one == null && two == null) {
+            fail("null arguments");
+        }
+        if (one != null) {
+            assertFalse("one=" + one + ", two=" + two, one.isEquivalentTo(two));
+        }
+        if (two != null) {
+            assertFalse("one=" + one + ", two=" + two, two.isEquivalentTo(one));
+        }
+    }
+
+    static void assertIsEquivalentTo(
+            TimeZoneProviderSuggestion one, TimeZoneProviderSuggestion two) {
+        if (one == null || two == null) {
+            fail("null arguments");
+        }
+        assertTrue("one=" + one + ", two=" + two, one.isEquivalentTo(two));
+        assertTrue("one=" + one + ", two=" + two, two.isEquivalentTo(one));
+    }
+}
diff --git a/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java b/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java
index e7b88c8..469be4c 100644
--- a/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java
+++ b/core/tests/coretests/src/android/view/ScrollCaptureConnectionTest.java
@@ -21,16 +21,19 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.os.Binder;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.ICancellationSignal;
 import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
@@ -60,6 +63,7 @@
 
     private ScrollCaptureTarget mTarget;
     private ScrollCaptureConnection mConnection;
+    private IBinder mConnectionBinder = new Binder("ScrollCaptureConnection Test");
 
     private Handler mHandler;
 
@@ -76,6 +80,7 @@
         mHandler = new Handler(getTargetContext().getMainLooper());
         when(mSurface.isValid()).thenReturn(true);
         when(mView.getScrollCaptureHint()).thenReturn(View.SCROLL_CAPTURE_HINT_INCLUDE);
+        when(mRemote.asBinder()).thenReturn(mConnectionBinder);
 
         mTarget = new ScrollCaptureTarget(mView, mLocalVisibleRect, mPositionInWindow, mCallback);
         mTarget.setScrollBounds(mScrollBounds);
@@ -107,12 +112,13 @@
     @Test
     public void testStartCapture() throws Exception {
         mConnection.startCapture(mSurface, mRemote);
+        assertTrue(mConnection.isConnected());
+        assertFalse(mConnection.isActive());
 
         mCallback.completeStartRequest();
         assertTrue(mConnection.isActive());
 
         verify(mRemote, times(1)).onCaptureStarted();
-        verifyNoMoreInteractions(mRemote);
     }
 
     @Test
@@ -123,7 +129,7 @@
         mCallback.completeStartRequest();
         assertFalse(mConnection.isActive());
 
-        verifyNoMoreInteractions(mRemote);
+        verify(mRemote, never()).onCaptureStarted();
     }
 
     /** @see ScrollCaptureConnection#requestImage(Rect) */
@@ -131,42 +137,63 @@
     public void testRequestImage() throws Exception {
         mConnection.startCapture(mSurface, mRemote);
         mCallback.completeStartRequest();
-        reset(mRemote);
 
         mConnection.requestImage(new Rect(1, 2, 3, 4));
         mCallback.completeImageRequest(new Rect(1, 2, 3, 4));
 
         verify(mRemote, times(1))
                 .onImageRequestCompleted(eq(0), eq(new Rect(1, 2, 3, 4)));
-        verifyNoMoreInteractions(mRemote);
     }
 
     @Test
     public void testRequestImage_cancellation() throws Exception {
         mConnection.startCapture(mSurface, mRemote);
         mCallback.completeStartRequest();
-        reset(mRemote);
 
         ICancellationSignal signal = mConnection.requestImage(new Rect(1, 2, 3, 4));
         signal.cancel();
         mCallback.completeImageRequest(new Rect(1, 2, 3, 4));
 
-        verifyNoMoreInteractions(mRemote);
+        verify(mRemote, never()).onImageRequestCompleted(anyInt(), any(Rect.class));
     }
 
     /** @see ScrollCaptureConnection#endCapture() */
     @Test
     public void testEndCapture() throws Exception {
         mConnection.startCapture(mSurface, mRemote);
+        assertTrue(mConnection.isConnected());
+
         mCallback.completeStartRequest();
-        reset(mRemote);
+        assertTrue(mConnection.isActive());
 
         mConnection.endCapture();
         mCallback.completeEndRequest();
+        assertFalse(mConnection.isActive());
 
         // And the reply is sent
         verify(mRemote, times(1)).onCaptureEnded();
-        verifyNoMoreInteractions(mRemote);
+
+        assertFalse(mConnection.isConnected());
+    }
+
+    /** @see ScrollCaptureConnection#endCapture() */
+    @Test
+    public void testClose_withPendingOperation() throws Exception {
+        mConnection.startCapture(mSurface, mRemote);
+        mCallback.completeStartRequest();
+
+        mConnection.requestImage(new Rect(1, 2, 3, 4));
+        assertFalse(mCallback.getLastCancellationSignal().isCanceled());
+
+        mConnection.close();
+
+        // And the reply is sent
+        assertTrue(mCallback.onScrollCaptureEndCalled());
+        assertTrue(mCallback.getLastCancellationSignal().isCanceled());
+        assertFalse(mConnection.isActive());
+        assertFalse(mConnection.isConnected());
+        verify(mRemote, never()).onCaptureEnded();
+        verify(mRemote, never()).onImageRequestCompleted(anyInt(), any(Rect.class));
     }
 
     /** @see ScrollCaptureConnection#endCapture() */
@@ -174,20 +201,19 @@
     public void testEndCapture_cancellation() throws Exception {
         mConnection.startCapture(mSurface, mRemote);
         mCallback.completeStartRequest();
-        reset(mRemote);
 
         ICancellationSignal signal = mConnection.endCapture();
         signal.cancel();
         mCallback.completeEndRequest();
 
-        verifyNoMoreInteractions(mRemote);
+        verify(mRemote, never()).onCaptureEnded();
     }
 
     @Test
     public void testClose() {
         mConnection.close();
         assertFalse(mConnection.isActive());
-        verifyNoMoreInteractions(mRemote);
+        assertFalse(mConnection.isConnected());
     }
 
     @Test
@@ -196,9 +222,11 @@
 
         mCallback.completeStartRequest();
         assertTrue(mConnection.isActive());
+        assertTrue(mConnection.isConnected());
 
         mConnection.close();
         mCallback.completeEndRequest();
         assertFalse(mConnection.isActive());
+        assertFalse(mConnection.isConnected());
     }
 }
diff --git a/core/tests/coretests/src/android/view/TestScrollCaptureCallback.java b/core/tests/coretests/src/android/view/TestScrollCaptureCallback.java
index 1520c6e..02921e7 100644
--- a/core/tests/coretests/src/android/view/TestScrollCaptureCallback.java
+++ b/core/tests/coretests/src/android/view/TestScrollCaptureCallback.java
@@ -31,10 +31,13 @@
     private Consumer<Rect> mImageOnComplete;
     private Runnable mOnEndReady;
     private volatile int mModCount;
+    private boolean mOnScrollCaptureEndCalled;
+    private CancellationSignal mLastCancellationSignal;
 
     @Override
     public void onScrollCaptureSearch(@NonNull CancellationSignal signal,
             @NonNull Consumer<Rect> onReady) {
+        mLastCancellationSignal = signal;
         mSearchConsumer = onReady;
         mModCount++;
     }
@@ -42,6 +45,7 @@
     @Override
     public void onScrollCaptureStart(@NonNull ScrollCaptureSession session,
             @NonNull CancellationSignal signal, @NonNull Runnable onReady) {
+        mLastCancellationSignal = signal;
         mStartOnReady = onReady;
         mModCount++;
     }
@@ -50,6 +54,7 @@
     public void onScrollCaptureImageRequest(@NonNull ScrollCaptureSession session,
             @NonNull CancellationSignal signal, @NonNull Rect captureArea,
             @NonNull Consumer<Rect> onComplete) {
+        mLastCancellationSignal = signal;
         mImageOnComplete = onComplete;
         mModCount++;
     }
@@ -57,8 +62,12 @@
     @Override
     public void onScrollCaptureEnd(@NonNull Runnable onReady) {
         mOnEndReady = onReady;
+        mOnScrollCaptureEndCalled = true;
     }
 
+    public boolean onScrollCaptureEndCalled() {
+        return mOnScrollCaptureEndCalled;
+    }
     void completeSearchRequest(Rect scrollBounds) {
         assertNotNull("Did not receive search request", mSearchConsumer);
         mSearchConsumer.accept(scrollBounds);
@@ -71,16 +80,26 @@
 
     void completeStartRequest() {
         assertNotNull("Did not receive start request", mStartOnReady);
-        mStartOnReady.run();
+        if (mLastCancellationSignal != null && !mLastCancellationSignal.isCanceled()) {
+            mStartOnReady.run();
+        }
     }
 
     void completeImageRequest(Rect captured) {
         assertNotNull("Did not receive image request", mImageOnComplete);
-        mImageOnComplete.accept(captured);
+        if (mLastCancellationSignal != null && !mLastCancellationSignal.isCanceled()) {
+            mImageOnComplete.accept(captured);
+        }
     }
 
     void completeEndRequest() {
         assertNotNull("Did not receive end request", mOnEndReady);
-        mOnEndReady.run();
+        if (mLastCancellationSignal != null && !mLastCancellationSignal.isCanceled()) {
+            mOnEndReady.run();
+        }
+    }
+
+    public CancellationSignal getLastCancellationSignal() {
+        return mLastCancellationSignal;
     }
 }
diff --git a/core/tests/coretests/src/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java b/core/tests/coretests/src/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java
index 79f7a5c..e95f6c2 100644
--- a/core/tests/coretests/src/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.os;
 
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_AMBIENT;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.os.BatteryConsumer;
@@ -36,7 +38,7 @@
 
     @Rule
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
-            .setAveragePower(PowerProfile.POWER_AMBIENT_DISPLAY, 10.0);
+            .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, 0, 10.0);
 
     @Test
     public void testMeasuredEnergyBasedModel() {
@@ -45,13 +47,13 @@
 
         stats.updateDisplayMeasuredEnergyStatsLocked(300_000_000, Display.STATE_ON, 0);
 
-        stats.noteScreenStateLocked(Display.STATE_DOZE, 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS,
+        stats.noteScreenStateLocked(0, Display.STATE_DOZE, 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS,
                 30 * MINUTE_IN_MS);
 
         stats.updateDisplayMeasuredEnergyStatsLocked(200_000_000, Display.STATE_DOZE,
                 30 * MINUTE_IN_MS);
 
-        stats.noteScreenStateLocked(Display.STATE_OFF, 120 * MINUTE_IN_MS, 120 * MINUTE_IN_MS,
+        stats.noteScreenStateLocked(0, Display.STATE_OFF, 120 * MINUTE_IN_MS, 120 * MINUTE_IN_MS,
                 120 * MINUTE_IN_MS);
 
         stats.updateDisplayMeasuredEnergyStatsLocked(100_000_000, Display.STATE_OFF,
@@ -76,9 +78,9 @@
     public void testPowerProfileBasedModel() {
         BatteryStatsImpl stats = mStatsRule.getBatteryStats();
 
-        stats.noteScreenStateLocked(Display.STATE_DOZE, 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS,
+        stats.noteScreenStateLocked(0, Display.STATE_DOZE, 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS,
                 30 * MINUTE_IN_MS);
-        stats.noteScreenStateLocked(Display.STATE_OFF, 120 * MINUTE_IN_MS, 120 * MINUTE_IN_MS,
+        stats.noteScreenStateLocked(0, Display.STATE_OFF, 120 * MINUTE_IN_MS, 120 * MINUTE_IN_MS,
                 120 * MINUTE_IN_MS);
 
         AmbientDisplayPowerCalculator calculator =
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsHistoryIteratorTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsHistoryIteratorTest.java
index 263daf0..2262c05 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsHistoryIteratorTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsHistoryIteratorTest.java
@@ -25,39 +25,50 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import org.junit.Rule;
+import libcore.testing.io.TestIoUtils;
+
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.File;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class BatteryStatsHistoryIteratorTest {
     private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
 
-    @Rule
-    public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule();
+    private MockClock mMockClock = new MockClock();
+    private MockBatteryStatsImpl mBatteryStats;
+
+    @Before
+    public void setup() {
+        final File historyDir =
+                TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
+        mBatteryStats = new MockBatteryStatsImpl(mMockClock, historyDir);
+    }
 
     @Test
     public void testIterator() {
-        MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
-        batteryStats.setRecordAllHistoryLocked(true);
-        batteryStats.forceRecordAllHistory();
+        mBatteryStats.setRecordAllHistoryLocked(true);
+        mBatteryStats.forceRecordAllHistory();
 
-        mStatsRule.setTime(1000, 1000);
-        batteryStats.setNoAutoReset(true);
+        mMockClock.realtime = 1000;
+        mMockClock.uptime = 1000;
+        mBatteryStats.setNoAutoReset(true);
 
-        batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+        mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
                 /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0, 1_000_000,
                 1_000_000, 1_000_000);
-        batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+        mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
                 /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0, 2_000_000,
                 2_000_000, 2_000_000);
 
-        batteryStats.noteAlarmStartLocked("foo", null, APP_UID, 3_000_000, 2_000_000);
-        batteryStats.noteAlarmFinishLocked("foo", null, APP_UID, 3_001_000, 2_001_000);
+        mBatteryStats.noteAlarmStartLocked("foo", null, APP_UID, 3_000_000, 2_000_000);
+        mBatteryStats.noteAlarmFinishLocked("foo", null, APP_UID, 3_001_000, 2_001_000);
 
         final BatteryStatsHistoryIterator iterator =
-                batteryStats.createBatteryStatsHistoryIterator();
+                mBatteryStats.createBatteryStatsHistoryIterator();
 
         BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
 
@@ -96,6 +107,57 @@
         assertThat(iterator.next(item)).isFalse();
     }
 
+    // Test history that spans multiple buffers and uses more than 32k different strings.
+    @Test
+    public void tagsLongHistory() {
+        mBatteryStats.setRecordAllHistoryLocked(true);
+        mBatteryStats.forceRecordAllHistory();
+
+        mMockClock.realtime = 1000;
+        mMockClock.uptime = 1000;
+        mBatteryStats.setNoAutoReset(true);
+
+        mBatteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+                /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0, 1_000_000,
+                1_000_000, 1_000_000);
+
+        // More than 32k strings
+        final int eventCount = 0x7FFF + 100;
+        for (int i = 0; i < eventCount; i++) {
+            mBatteryStats.noteAlarmStartLocked("a" + i, null, APP_UID, 3_000_000, 2_000_000);
+        }
+
+        final BatteryStatsHistoryIterator iterator =
+                mBatteryStats.createBatteryStatsHistoryIterator();
+
+        BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+        assertThat(iterator.next(item)).isTrue();
+        assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_RESET);
+        assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_NONE);
+        assertThat(item.eventTag).isNull();
+
+        assertThat(iterator.next(item)).isTrue();
+        assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_UPDATE);
+        assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_NONE);
+        assertThat(item.eventTag).isNull();
+        assertThat(item.time).isEqualTo(1_000_000);
+
+        assertThat(iterator.next(item)).isTrue();
+        assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_UPDATE);
+        assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_NONE);
+        assertThat(item.eventTag).isNull();
+        assertThat(item.time).isEqualTo(2_000_000);
+
+        for (int i = 0; i < eventCount; i++) {
+            assertThat(iterator.next(item)).isTrue();
+            assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_ALARM
+                    | BatteryStats.HistoryItem.EVENT_FLAG_START);
+            assertThat(item.eventTag.string).isEqualTo("a" + i);
+        }
+
+        assertThat(iterator.next(item)).isFalse();
+    }
+
     private void assertHistoryItem(BatteryStats.HistoryItem item, int command, int eventCode,
             String tag, int uid, int batteryChargeUah, int batteryLevel,
             long elapsedTimeMs) {
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
index e8e4330..358885e 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsNoteTest.java
@@ -16,9 +16,13 @@
 
 package com.android.internal.os;
 
+import static android.os.BatteryStats.NUM_SCREEN_BRIGHTNESS_BINS;
 import static android.os.BatteryStats.STATS_SINCE_CHARGED;
 import static android.os.BatteryStats.WAKE_TYPE_PARTIAL;
 
+import static com.android.internal.os.BatteryStatsImpl.ExternalStatsSync.UPDATE_CPU;
+import static com.android.internal.os.BatteryStatsImpl.ExternalStatsSync.UPDATE_DISPLAY;
+
 import android.app.ActivityManager;
 import android.os.BatteryStats;
 import android.os.BatteryStats.HistoryItem;
@@ -37,8 +41,10 @@
 
 import junit.framework.TestCase;
 
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.IntConsumer;
 
 /**
  * Test various BatteryStatsImpl noteStart methods.
@@ -317,18 +323,130 @@
     public void testNoteScreenStateLocked() throws Exception {
         final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
         MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
+        bi.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
 
         bi.updateTimeBasesLocked(true, Display.STATE_ON, 0, 0);
-        bi.noteScreenStateLocked(Display.STATE_ON);
-        bi.noteScreenStateLocked(Display.STATE_DOZE);
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
         assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
-        assertEquals(bi.getScreenState(), Display.STATE_DOZE);
-        bi.noteScreenStateLocked(Display.STATE_ON);
+        assertEquals(Display.STATE_DOZE, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
         assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
-        assertEquals(bi.getScreenState(), Display.STATE_ON);
-        bi.noteScreenStateLocked(Display.STATE_OFF);
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
         assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
-        assertEquals(bi.getScreenState(), Display.STATE_OFF);
+        assertEquals(Display.STATE_OFF, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE_SUSPEND);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE_SUSPEND, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        // STATE_VR note should map to STATE_ON.
+        bi.noteScreenStateLocked(0, Display.STATE_VR);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        // STATE_ON_SUSPEND note should map to STATE_ON.
+        bi.noteScreenStateLocked(0, Display.STATE_ON_SUSPEND);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        // Transition from ON to ON state should not cause an External Sync
+        assertEquals(0, bi.getAndClearExternalStatsSyncFlags());
+    }
+
+    /**
+     * Test BatteryStatsImpl.noteScreenStateLocked sets timebases and screen states correctly for
+     * multi display devices
+     */
+    @SmallTest
+    public void testNoteScreenStateLocked_multiDisplay() throws Exception {
+        final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
+        MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
+        bi.setDisplayCountLocked(2);
+        bi.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
+
+        bi.updateTimeBasesLocked(true, Display.STATE_OFF, 0, 0);
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        bi.noteScreenStateLocked(1, Display.STATE_OFF);
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_OFF, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE_SUSPEND);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE_SUSPEND, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        // STATE_VR note should map to STATE_ON.
+        bi.noteScreenStateLocked(0, Display.STATE_VR);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        // STATE_ON_SUSPEND note should map to STATE_ON.
+        bi.noteScreenStateLocked(0, Display.STATE_ON_SUSPEND);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        // Transition from ON to ON state should not cause an External Sync
+        assertEquals(0, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(1, Display.STATE_DOZE);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        // Should remain STATE_ON since display0 is still on.
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        // Overall screen state did not change, so no need to sync CPU stats.
+        assertEquals(UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE_SUSPEND);
+        assertTrue(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_DOZE, bi.getScreenState());
+        // Overall screen state did not change, so no need to sync CPU stats.
+        assertEquals(UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_VR);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(UPDATE_CPU | UPDATE_DISPLAY, bi.getAndClearExternalStatsSyncFlags());
+
+        bi.noteScreenStateLocked(0, Display.STATE_ON_SUSPEND);
+        assertFalse(bi.getOnBatteryScreenOffTimeBase().isRunning());
+        assertEquals(Display.STATE_ON, bi.getScreenState());
+        assertEquals(0, bi.getAndClearExternalStatsSyncFlags());
     }
 
     /*
@@ -352,32 +470,317 @@
         bi.updateTimeBasesLocked(true, Display.STATE_UNKNOWN, 100_000, 100_000);
         // Turn on display at 200us
         clocks.realtime = clocks.uptime = 200;
-        bi.noteScreenStateLocked(Display.STATE_ON);
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
         assertEquals(150_000, bi.computeBatteryRealtime(250_000, STATS_SINCE_CHARGED));
         assertEquals(100_000, bi.computeBatteryScreenOffRealtime(250_000, STATS_SINCE_CHARGED));
         assertEquals(50_000, bi.getScreenOnTime(250_000, STATS_SINCE_CHARGED));
         assertEquals(0, bi.getScreenDozeTime(250_000, STATS_SINCE_CHARGED));
+        assertEquals(50_000, bi.getDisplayScreenOnTime(0, 250_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(0, 250_000));
 
         clocks.realtime = clocks.uptime = 310;
-        bi.noteScreenStateLocked(Display.STATE_OFF);
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
         assertEquals(250_000, bi.computeBatteryRealtime(350_000, STATS_SINCE_CHARGED));
         assertEquals(140_000, bi.computeBatteryScreenOffRealtime(350_000, STATS_SINCE_CHARGED));
         assertEquals(110_000, bi.getScreenOnTime(350_000, STATS_SINCE_CHARGED));
         assertEquals(0, bi.getScreenDozeTime(350_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 350_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(0, 350_000));
 
         clocks.realtime = clocks.uptime = 400;
-        bi.noteScreenStateLocked(Display.STATE_DOZE);
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
         assertEquals(400_000, bi.computeBatteryRealtime(500_000, STATS_SINCE_CHARGED));
         assertEquals(290_000, bi.computeBatteryScreenOffRealtime(500_000, STATS_SINCE_CHARGED));
         assertEquals(110_000, bi.getScreenOnTime(500_000, STATS_SINCE_CHARGED));
         assertEquals(100_000, bi.getScreenDozeTime(500_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 500_000));
+        assertEquals(100_000, bi.getDisplayScreenDozeTime(0, 500_000));
 
         clocks.realtime = clocks.uptime = 1000;
-        bi.noteScreenStateLocked(Display.STATE_OFF);
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
         assertEquals(1400_000, bi.computeBatteryRealtime(1500_000, STATS_SINCE_CHARGED));
         assertEquals(1290_000, bi.computeBatteryScreenOffRealtime(1500_000, STATS_SINCE_CHARGED));
         assertEquals(110_000, bi.getScreenOnTime(1500_000, STATS_SINCE_CHARGED));
         assertEquals(600_000, bi.getScreenDozeTime(1500_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 1500_000));
+        assertEquals(600_000, bi.getDisplayScreenDozeTime(0, 1500_000));
+    }
+
+    /*
+     * Test BatteryStatsImpl.noteScreenStateLocked updates timers correctly for multi display
+     * devices.
+     */
+    @SmallTest
+    public void testNoteScreenStateTimersLocked_multiDisplay() throws Exception {
+        final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
+        MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
+        bi.setDisplayCountLocked(2);
+
+        clocks.realtime = clocks.uptime = 100;
+        // Device startup, setOnBatteryLocked calls updateTimebases
+        bi.updateTimeBasesLocked(true, Display.STATE_UNKNOWN, 100_000, 100_000);
+        // Turn on display at 200us
+        clocks.realtime = clocks.uptime = 200;
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        bi.noteScreenStateLocked(1, Display.STATE_OFF);
+        assertEquals(150_000, bi.computeBatteryRealtime(250_000, STATS_SINCE_CHARGED));
+        assertEquals(100_000, bi.computeBatteryScreenOffRealtime(250_000, STATS_SINCE_CHARGED));
+        assertEquals(50_000, bi.getScreenOnTime(250_000, STATS_SINCE_CHARGED));
+        assertEquals(0, bi.getScreenDozeTime(250_000, STATS_SINCE_CHARGED));
+        assertEquals(50_000, bi.getDisplayScreenOnTime(0, 250_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(0, 250_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 250_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(1, 250_000));
+
+        clocks.realtime = clocks.uptime = 310;
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertEquals(250_000, bi.computeBatteryRealtime(350_000, STATS_SINCE_CHARGED));
+        assertEquals(140_000, bi.computeBatteryScreenOffRealtime(350_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getScreenOnTime(350_000, STATS_SINCE_CHARGED));
+        assertEquals(0, bi.getScreenDozeTime(350_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 350_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(0, 350_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 350_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(1, 350_000));
+
+        clocks.realtime = clocks.uptime = 400;
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        assertEquals(400_000, bi.computeBatteryRealtime(500_000, STATS_SINCE_CHARGED));
+        assertEquals(290_000, bi.computeBatteryScreenOffRealtime(500_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getScreenOnTime(500_000, STATS_SINCE_CHARGED));
+        assertEquals(100_000, bi.getScreenDozeTime(500_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 500_000));
+        assertEquals(100_000, bi.getDisplayScreenDozeTime(0, 500_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 500_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(1, 500_000));
+
+        clocks.realtime = clocks.uptime = 1000;
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertEquals(1000_000, bi.computeBatteryRealtime(1100_000, STATS_SINCE_CHARGED));
+        assertEquals(890_000, bi.computeBatteryScreenOffRealtime(1100_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getScreenOnTime(1100_000, STATS_SINCE_CHARGED));
+        assertEquals(600_000, bi.getScreenDozeTime(1100_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 1100_000));
+        assertEquals(600_000, bi.getDisplayScreenDozeTime(0, 1100_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 1100_000));
+        assertEquals(0, bi.getDisplayScreenDozeTime(1, 1100_000));
+
+        clocks.realtime = clocks.uptime = 1200;
+        // Change state of second display to doze
+        bi.noteScreenStateLocked(1, Display.STATE_DOZE);
+        assertEquals(1150_000, bi.computeBatteryRealtime(1250_000, STATS_SINCE_CHARGED));
+        assertEquals(1040_000, bi.computeBatteryScreenOffRealtime(1250_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getScreenOnTime(1250_000, STATS_SINCE_CHARGED));
+        assertEquals(650_000, bi.getScreenDozeTime(1250_000, STATS_SINCE_CHARGED));
+        assertEquals(110_000, bi.getDisplayScreenOnTime(0, 1250_000));
+        assertEquals(600_000, bi.getDisplayScreenDozeTime(0, 1250_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 1250_000));
+        assertEquals(50_000, bi.getDisplayScreenDozeTime(1, 1250_000));
+
+        clocks.realtime = clocks.uptime = 1310;
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        assertEquals(1250_000, bi.computeBatteryRealtime(1350_000, STATS_SINCE_CHARGED));
+        assertEquals(1100_000, bi.computeBatteryScreenOffRealtime(1350_000, STATS_SINCE_CHARGED));
+        assertEquals(150_000, bi.getScreenOnTime(1350_000, STATS_SINCE_CHARGED));
+        assertEquals(710_000, bi.getScreenDozeTime(1350_000, STATS_SINCE_CHARGED));
+        assertEquals(150_000, bi.getDisplayScreenOnTime(0, 1350_000));
+        assertEquals(600_000, bi.getDisplayScreenDozeTime(0, 1350_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 1350_000));
+        assertEquals(150_000, bi.getDisplayScreenDozeTime(1, 1350_000));
+
+        clocks.realtime = clocks.uptime = 1400;
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        assertEquals(1400_000, bi.computeBatteryRealtime(1500_000, STATS_SINCE_CHARGED));
+        assertEquals(1200_000, bi.computeBatteryScreenOffRealtime(1500_000, STATS_SINCE_CHARGED));
+        assertEquals(200_000, bi.getScreenOnTime(1500_000, STATS_SINCE_CHARGED));
+        assertEquals(810_000, bi.getScreenDozeTime(1500_000, STATS_SINCE_CHARGED));
+        assertEquals(200_000, bi.getDisplayScreenOnTime(0, 1500_000));
+        assertEquals(700_000, bi.getDisplayScreenDozeTime(0, 1500_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 1500_000));
+        assertEquals(300_000, bi.getDisplayScreenDozeTime(1, 1500_000));
+
+        clocks.realtime = clocks.uptime = 2000;
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertEquals(2000_000, bi.computeBatteryRealtime(2100_000, STATS_SINCE_CHARGED));
+        assertEquals(1800_000, bi.computeBatteryScreenOffRealtime(2100_000, STATS_SINCE_CHARGED));
+        assertEquals(200_000, bi.getScreenOnTime(2100_000, STATS_SINCE_CHARGED));
+        assertEquals(1410_000, bi.getScreenDozeTime(2100_000, STATS_SINCE_CHARGED));
+        assertEquals(200_000, bi.getDisplayScreenOnTime(0, 2100_000));
+        assertEquals(1200_000, bi.getDisplayScreenDozeTime(0, 2100_000));
+        assertEquals(0, bi.getDisplayScreenOnTime(1, 2100_000));
+        assertEquals(900_000, bi.getDisplayScreenDozeTime(1, 2100_000));
+
+
+        clocks.realtime = clocks.uptime = 2200;
+        // Change state of second display to on
+        bi.noteScreenStateLocked(1, Display.STATE_ON);
+        assertEquals(2150_000, bi.computeBatteryRealtime(2250_000, STATS_SINCE_CHARGED));
+        assertEquals(1900_000, bi.computeBatteryScreenOffRealtime(2250_000, STATS_SINCE_CHARGED));
+        assertEquals(250_000, bi.getScreenOnTime(2250_000, STATS_SINCE_CHARGED));
+        assertEquals(1510_000, bi.getScreenDozeTime(2250_000, STATS_SINCE_CHARGED));
+        assertEquals(200_000, bi.getDisplayScreenOnTime(0, 2250_000));
+        assertEquals(1200_000, bi.getDisplayScreenDozeTime(0, 2250_000));
+        assertEquals(50_000, bi.getDisplayScreenOnTime(1, 2250_000));
+        assertEquals(1000_000, bi.getDisplayScreenDozeTime(1, 2250_000));
+
+        clocks.realtime = clocks.uptime = 2310;
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        assertEquals(2250_000, bi.computeBatteryRealtime(2350_000, STATS_SINCE_CHARGED));
+        assertEquals(1900_000, bi.computeBatteryScreenOffRealtime(2350_000, STATS_SINCE_CHARGED));
+        assertEquals(350_000, bi.getScreenOnTime(2350_000, STATS_SINCE_CHARGED));
+        assertEquals(1510_000, bi.getScreenDozeTime(2350_000, STATS_SINCE_CHARGED));
+        assertEquals(240_000, bi.getDisplayScreenOnTime(0, 2350_000));
+        assertEquals(1200_000, bi.getDisplayScreenDozeTime(0, 2350_000));
+        assertEquals(150_000, bi.getDisplayScreenOnTime(1, 2350_000));
+        assertEquals(1000_000, bi.getDisplayScreenDozeTime(1, 2350_000));
+
+        clocks.realtime = clocks.uptime = 2400;
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        assertEquals(2400_000, bi.computeBatteryRealtime(2500_000, STATS_SINCE_CHARGED));
+        assertEquals(1900_000, bi.computeBatteryScreenOffRealtime(2500_000, STATS_SINCE_CHARGED));
+        assertEquals(500_000, bi.getScreenOnTime(2500_000, STATS_SINCE_CHARGED));
+        assertEquals(1510_000, bi.getScreenDozeTime(2500_000, STATS_SINCE_CHARGED));
+        assertEquals(290_000, bi.getDisplayScreenOnTime(0, 2500_000));
+        assertEquals(1300_000, bi.getDisplayScreenDozeTime(0, 2500_000));
+        assertEquals(300_000, bi.getDisplayScreenOnTime(1, 2500_000));
+        assertEquals(1000_000, bi.getDisplayScreenDozeTime(1, 2500_000));
+
+        clocks.realtime = clocks.uptime = 3000;
+        bi.noteScreenStateLocked(0, Display.STATE_OFF);
+        assertEquals(3000_000, bi.computeBatteryRealtime(3100_000, STATS_SINCE_CHARGED));
+        assertEquals(1900_000, bi.computeBatteryScreenOffRealtime(3100_000, STATS_SINCE_CHARGED));
+        assertEquals(1100_000, bi.getScreenOnTime(3100_000, STATS_SINCE_CHARGED));
+        assertEquals(1510_000, bi.getScreenDozeTime(3100_000, STATS_SINCE_CHARGED));
+        assertEquals(290_000, bi.getDisplayScreenOnTime(0, 3100_000));
+        assertEquals(1800_000, bi.getDisplayScreenDozeTime(0, 3100_000));
+        assertEquals(900_000, bi.getDisplayScreenOnTime(1, 3100_000));
+        assertEquals(1000_000, bi.getDisplayScreenDozeTime(1, 3100_000));
+    }
+
+
+    /**
+     * Test BatteryStatsImpl.noteScreenBrightnessLocked updates timers correctly.
+     */
+    @SmallTest
+    public void testScreenBrightnessLocked_multiDisplay() throws Exception {
+        final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
+        MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
+
+        final int numDisplay = 2;
+        bi.setDisplayCountLocked(numDisplay);
+
+
+        final long[] overallExpected = new long[NUM_SCREEN_BRIGHTNESS_BINS];
+        final long[][] perDisplayExpected = new long[numDisplay][NUM_SCREEN_BRIGHTNESS_BINS];
+        class Bookkeeper {
+            public long currentTimeMs = 100;
+            public int overallActiveBin = -1;
+            public int[] perDisplayActiveBin = new int[numDisplay];
+        }
+        final Bookkeeper bk = new Bookkeeper();
+        Arrays.fill(bk.perDisplayActiveBin, -1);
+
+        IntConsumer incrementTime = inc -> {
+            bk.currentTimeMs += inc;
+            if (bk.overallActiveBin >= 0) {
+                overallExpected[bk.overallActiveBin] += inc;
+            }
+            for (int i = 0; i < numDisplay; i++) {
+                final int bin = bk.perDisplayActiveBin[i];
+                if (bin >= 0) {
+                    perDisplayExpected[i][bin] += inc;
+                }
+            }
+            clocks.realtime = clocks.uptime = bk.currentTimeMs;
+        };
+
+        bi.updateTimeBasesLocked(true, Display.STATE_ON, 0, 0);
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        bi.noteScreenStateLocked(1, Display.STATE_ON);
+
+        incrementTime.accept(100);
+        bi.noteScreenBrightnessLocked(0, 25);
+        bi.noteScreenBrightnessLocked(1, 25);
+        // floor(25/256*5) = bin 0
+        bk.overallActiveBin = 0;
+        bk.perDisplayActiveBin[0] = 0;
+        bk.perDisplayActiveBin[1] = 0;
+
+        incrementTime.accept(50);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(13);
+        bi.noteScreenBrightnessLocked(0, 100);
+        // floor(25/256*5) = bin 1
+        bk.overallActiveBin = 1;
+        bk.perDisplayActiveBin[0] = 1;
+
+        incrementTime.accept(44);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(22);
+        bi.noteScreenBrightnessLocked(1, 200);
+        // floor(200/256*5) = bin 3
+        bk.overallActiveBin = 3;
+        bk.perDisplayActiveBin[1] = 3;
+
+        incrementTime.accept(33);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(77);
+        bi.noteScreenBrightnessLocked(0, 150);
+        // floor(150/256*5) = bin 2
+        // Overall active bin should not change
+        bk.perDisplayActiveBin[0] = 2;
+
+        incrementTime.accept(88);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(11);
+        bi.noteScreenStateLocked(1, Display.STATE_OFF);
+        // Display 1 should timers should stop incrementing
+        // Overall active bin should fallback to display 0's bin
+        bk.overallActiveBin = 2;
+        bk.perDisplayActiveBin[1] = -1;
+
+        incrementTime.accept(99);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(200);
+        bi.noteScreenBrightnessLocked(0, 255);
+        // floor(150/256*5) = bin 4
+        bk.overallActiveBin = 4;
+        bk.perDisplayActiveBin[0] = 4;
+
+        incrementTime.accept(300);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(200);
+        bi.noteScreenStateLocked(0, Display.STATE_DOZE);
+        // No displays are on. No brightness timers should be active.
+        bk.overallActiveBin = -1;
+        bk.perDisplayActiveBin[0] = -1;
+
+        incrementTime.accept(300);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(400);
+        bi.noteScreenStateLocked(1, Display.STATE_ON);
+        // Display 1 turned back on.
+        bk.overallActiveBin = 3;
+        bk.perDisplayActiveBin[1] = 3;
+
+        incrementTime.accept(500);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
+
+        incrementTime.accept(600);
+        bi.noteScreenStateLocked(0, Display.STATE_ON);
+        // Display 0 turned back on.
+        bk.overallActiveBin = 4;
+        bk.perDisplayActiveBin[0] = 4;
+
+        incrementTime.accept(700);
+        checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
     }
 
     @SmallTest
@@ -820,4 +1223,19 @@
 
         assertEquals("Wrong uid2 blame in bucket 1 for Case " + caseName, blame2B, actualUid2[1]);
     }
+
+    private void checkScreenBrightnesses(long[] overallExpected, long[][] perDisplayExpected,
+            BatteryStatsImpl bi, long currentTimeMs) {
+        final int numDisplay = bi.getDisplayCount();
+        for (int bin = 0; bin < NUM_SCREEN_BRIGHTNESS_BINS; bin++) {
+            for (int display = 0; display < numDisplay; display++) {
+                assertEquals("Failure for display " + display + " screen brightness bin " + bin,
+                        perDisplayExpected[display][bin] * 1000,
+                        bi.getDisplayScreenBrightnessTime(display, bin, currentTimeMs * 1000));
+            }
+            assertEquals("Failure for overall screen brightness bin " + bin,
+                    overallExpected[bin] * 1000,
+                    bi.getScreenBrightnessTime(bin, currentTimeMs * 1000, STATS_SINCE_CHARGED));
+        }
+    }
 }
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java b/core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java
index 90a9572..92c2d43 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryStatsTests.java
@@ -64,6 +64,8 @@
         KernelSingleProcessCpuThreadReaderTest.class,
         KernelSingleUidTimeReaderTest.class,
         KernelWakelockReaderTest.class,
+        LongArrayMultiStateCounterTest.class,
+        LongMultiStateCounterTest.class,
         LongSamplingCounterTest.class,
         LongSamplingCounterArrayTest.class,
         MobileRadioPowerCalculatorTest.class,
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java
index 74b6dbe..7db31fb 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsProviderTest.java
@@ -163,6 +163,73 @@
         assertThat(iterator.next(item)).isFalse();
     }
 
+    @Test
+    public void testWriteAndReadHistoryTags() {
+        MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+        batteryStats.setRecordAllHistoryLocked(true);
+        batteryStats.forceRecordAllHistory();
+
+        batteryStats.setNoAutoReset(true);
+
+        mStatsRule.setTime(1_000_000, 1_000_000);
+
+        batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+                /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0, 1_000_000,
+                1_000_000, 1_000_000);
+
+        // Add a large number of different history tags with strings of increasing length.
+        // These long strings will overflow the history buffer, at which point
+        // history will be written to disk and a new buffer started.
+        // As a result, we will only see a tail end of the sequence of events included
+        // in history.
+        for (int i = 1; i < 200; i++) {
+            StringBuilder sb = new StringBuilder().append(i).append(" ");
+            for (int j = 0; j <= i; j++) {
+                sb.append("word ");
+            }
+            batteryStats.noteJobStartLocked(sb.toString(), i);
+        }
+
+        Context context = InstrumentationRegistry.getContext();
+        BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, batteryStats);
+
+        final BatteryUsageStats batteryUsageStats =
+                provider.getBatteryUsageStats(
+                        new BatteryUsageStatsQuery.Builder().includeBatteryHistory().build());
+
+        Parcel parcel = Parcel.obtain();
+        batteryUsageStats.writeToParcel(parcel, 0);
+
+        assertThat(parcel.dataSize()).isAtMost(128_000);
+
+        parcel.setDataPosition(0);
+        BatteryUsageStats unparceled = BatteryUsageStats.CREATOR.createFromParcel(parcel);
+
+        BatteryStatsHistoryIterator iterator = unparceled.iterateBatteryStatsHistory();
+        BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+
+        assertThat(iterator.next(item)).isTrue();
+        assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_CURRENT_TIME);
+
+        int lastUid = 0;
+        while (iterator.next(item)) {
+            int uid = item.eventTag.uid;
+            // We expect the history buffer to have been reset in the middle of the run because
+            // there were many different history tags written, exceeding the limit of 128k.
+            assertThat(uid).isGreaterThan(150);
+            assertThat(item.eventCode).isEqualTo(
+                    BatteryStats.HistoryItem.EVENT_JOB | BatteryStats.HistoryItem.EVENT_FLAG_START);
+            assertThat(item.eventTag.string).startsWith(uid + " ");
+            assertThat(item.batteryChargeUah).isEqualTo(3_600_000);
+            assertThat(item.batteryLevel).isEqualTo(90);
+            assertThat(item.time).isEqualTo((long) 1_000_000);
+
+            lastUid = uid;
+        }
+
+        assertThat(lastUid).isEqualTo(199);
+    }
+
     private void assertHistoryItem(BatteryStats.HistoryItem item, int command, int eventCode,
             String tag, int uid, int batteryChargeUah, int batteryLevel, long elapsedTimeMs) {
         assertThat(item.cmd).isEqualTo(command);
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsRule.java b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsRule.java
index e7fa656..3102e21 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsRule.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsRule.java
@@ -110,6 +110,14 @@
         return this;
     }
 
+    public BatteryUsageStatsRule setAveragePowerForOrdinal(String group, int ordinal,
+            double value) {
+        when(mPowerProfile.getAveragePowerForOrdinal(group, ordinal)).thenReturn(value);
+        when(mPowerProfile.getAveragePowerForOrdinal(eq(group), eq(ordinal),
+                anyDouble())).thenReturn(value);
+        return this;
+    }
+
     /** Call only after setting the power profile information. */
     public BatteryUsageStatsRule initMeasuredEnergyStatsLocked() {
         return initMeasuredEnergyStatsLocked(new String[0]);
@@ -183,8 +191,10 @@
         final String[] customPowerComponentNames = mBatteryStats.getCustomEnergyConsumerNames();
         final boolean includePowerModels = (query.getFlags()
                 & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_MODELS) != 0;
+        final boolean includeProcessStateData = (query.getFlags()
+                & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_INCLUDE_PROCESS_STATE_DATA) != 0;
         BatteryUsageStats.Builder builder = new BatteryUsageStats.Builder(
-                customPowerComponentNames, includePowerModels);
+                customPowerComponentNames, includePowerModels, includeProcessStateData);
         SparseArray<? extends BatteryStats.Uid> uidStats = mBatteryStats.getUidStats();
         for (int i = 0; i < uidStats.size(); i++) {
             builder.getOrCreateUidBatteryConsumerBuilder(uidStats.valueAt(i));
diff --git a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsTest.java b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsTest.java
index b44de3b..4733f86 100644
--- a/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/BatteryUsageStatsTest.java
@@ -16,9 +16,12 @@
 
 package com.android.internal.os;
 
+import static android.os.BatteryConsumer.POWER_COMPONENT_ANY;
 import static android.os.BatteryConsumer.POWER_MODEL_MEASURED_ENERGY;
-import static android.os.BatteryConsumer.POWER_MODEL_POWER_PROFILE;
 import static android.os.BatteryConsumer.POWER_MODEL_UNDEFINED;
+import static android.os.BatteryConsumer.PROCESS_STATE_BACKGROUND;
+import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND;
+import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -27,6 +30,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import android.os.AggregateBatteryConsumer;
 import android.os.BatteryConsumer;
 import android.os.BatteryUsageStats;
 import android.os.Parcel;
@@ -68,6 +72,12 @@
     }
 
     @Test
+    public void testBuilder_noProcessStateData() {
+        BatteryUsageStats batteryUsageStats = buildBatteryUsageStats1(false).build();
+        assertBatteryUsageStats1(batteryUsageStats, false);
+    }
+
+    @Test
     public void testParcelability_smallNumberOfUids() {
         final BatteryUsageStats outBatteryUsageStats = buildBatteryUsageStats1(true).build();
         final Parcel parcel = Parcel.obtain();
@@ -142,9 +152,14 @@
         assertThat(dump).contains("Computed drain: 30000");
         assertThat(dump).contains("actual drain: 1000-2000");
         assertThat(dump).contains("cpu: 20100 apps: 10100 duration: 20s 300ms");
+        assertThat(dump).contains("cpu(fg): 2333 apps: 1333 duration: 3s 332ms");
+        assertThat(dump).contains("cpu(bg): 2444 apps: 1444 duration: 4s 442ms");
+        assertThat(dump).contains("cpu(fgs): 2555 apps: 1555 duration: 5s 552ms");
         assertThat(dump).contains("FOO: 20200 apps: 10200 duration: 20s 400ms");
-        assertThat(dump).contains("UID 271: 1200 ( screen=300 cpu=400 FOO=500 )");
-        assertThat(dump).contains("User 42: 30.0 ( cpu=10.0 FOO=20.0 )");
+        assertThat(dump).contains("UID 271: 1200 fg: 1777 bg: 1888 fgs: 1999 ( screen=300 "
+                + "cpu=400 (600ms) cpu:fg=1777 (7s 771ms) cpu:bg=1888 (8s 881ms) "
+                + "cpu:fgs=1999 (9s 991ms) FOO=500 )");
+        assertThat(dump).contains("User 42: 30.0 ( cpu=10.0 (30ms) FOO=20.0 )");
     }
 
     @Test
@@ -160,10 +175,10 @@
     @Test
     public void testAdd() {
         final BatteryUsageStats stats1 = buildBatteryUsageStats1(false).build();
-        final BatteryUsageStats stats2 = buildBatteryUsageStats2(new String[] {"FOO"}).build();
+        final BatteryUsageStats stats2 = buildBatteryUsageStats2(new String[]{"FOO"}, true).build();
 
         final BatteryUsageStats sum =
-                new BatteryUsageStats.Builder(new String[] {"FOO"}, true)
+                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true)
                         .add(stats1)
                         .add(stats2)
                         .build();
@@ -175,12 +190,16 @@
         for (UidBatteryConsumer uidBatteryConsumer : uidBatteryConsumers) {
             if (uidBatteryConsumer.getUid() == APP_UID1) {
                 assertUidBatteryConsumer(uidBatteryConsumer, 2124, null,
-                        5321, 7432, 423, POWER_MODEL_POWER_PROFILE, 745, POWER_MODEL_UNDEFINED,
-                        956, 1167, 1478);
+                        5321, 7432, 423, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 745,
+                        POWER_MODEL_UNDEFINED,
+                        956, 1167, 1478,
+                        true, 3554, 3776, 3998, 3554, 15542, 3776, 17762, 3998, 19982);
             } else if (uidBatteryConsumer.getUid() == APP_UID2) {
                 assertUidBatteryConsumer(uidBatteryConsumer, 1332, "bar",
-                        1111, 2222, 333, POWER_MODEL_POWER_PROFILE, 444, POWER_MODEL_POWER_PROFILE,
-                        555, 666, 777);
+                        1111, 2222, 333, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 444,
+                        BatteryConsumer.POWER_MODEL_POWER_PROFILE,
+                        555, 666, 777,
+                        true, 1777, 1888, 1999, 1777, 7771, 1888, 8881, 1999, 9991);
             } else {
                 fail("Unexpected UID " + uidBatteryConsumer.getUid());
             }
@@ -198,8 +217,17 @@
     @Test
     public void testAdd_customComponentMismatch() {
         final BatteryUsageStats.Builder builder =
-                new BatteryUsageStats.Builder(new String[] {"FOO"}, true);
-        final BatteryUsageStats stats = buildBatteryUsageStats2(new String[] {"BAR"}).build();
+                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true);
+        final BatteryUsageStats stats = buildBatteryUsageStats2(new String[]{"BAR"}, false).build();
+
+        assertThrows(IllegalArgumentException.class, () -> builder.add(stats));
+    }
+
+    @Test
+    public void testAdd_processStateDataMismatch() {
+        final BatteryUsageStats.Builder builder =
+                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true);
+        final BatteryUsageStats stats = buildBatteryUsageStats2(new String[]{"FOO"}, false).build();
 
         assertThrows(IllegalArgumentException.class, () -> builder.add(stats));
     }
@@ -227,7 +255,7 @@
         final MockBatteryStatsImpl batteryStats = new MockBatteryStatsImpl(clocks);
 
         final BatteryUsageStats.Builder builder =
-                new BatteryUsageStats.Builder(new String[] {"FOO"}, true)
+                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, true)
                         .setBatteryCapacity(4000)
                         .setDischargePercentage(20)
                         .setDischargedPowerRange(1000, 2000)
@@ -236,16 +264,19 @@
 
         addUidBatteryConsumer(builder, batteryStats, APP_UID1, "foo",
                 1000, 2000,
-                300, POWER_MODEL_POWER_PROFILE, 400, POWER_MODEL_POWER_PROFILE, 500, 600, 800);
+                300, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 400,
+                BatteryConsumer.POWER_MODEL_POWER_PROFILE, 500, 600, 800,
+                1777, 7771, 1888, 8881, 1999, 9991);
 
         addAggregateBatteryConsumer(builder,
                 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS, 0,
-                10100, 10200, 10300, 10400);
+                10100, 10200, 10300, 10400,
+                1333, 3331, 1444, 4441, 1555, 5551);
 
         addAggregateBatteryConsumer(builder,
                 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE, 30000,
-                20100, 20200, 20300, 20400);
-
+                20100, 20200, 20300, 20400,
+                2333, 3332, 2444, 4442, 2555, 5552);
 
         if (includeUserBatteryConsumer) {
             builder.getOrCreateUserBatteryConsumerBuilder(USER_ID)
@@ -261,43 +292,55 @@
         return builder;
     }
 
-    private BatteryUsageStats.Builder buildBatteryUsageStats2(String[] customPowerComponentNames) {
+    private BatteryUsageStats.Builder buildBatteryUsageStats2(String[] customPowerComponentNames,
+            boolean includeProcessStateData) {
         final MockClock clocks = new MockClock();
         final MockBatteryStatsImpl batteryStats = new MockBatteryStatsImpl(clocks);
 
         final BatteryUsageStats.Builder builder =
-                new BatteryUsageStats.Builder(customPowerComponentNames, true)
-                        .setDischargePercentage(30)
-                        .setDischargedPowerRange(1234, 2345)
-                        .setStatsStartTimestamp(2000)
-                        .setStatsEndTimestamp(5000);
+                new BatteryUsageStats.Builder(customPowerComponentNames, true,
+                        includeProcessStateData);
+        builder.setDischargePercentage(30)
+                .setDischargedPowerRange(1234, 2345)
+                .setStatsStartTimestamp(2000)
+                .setStatsEndTimestamp(5000);
 
         addUidBatteryConsumer(builder, batteryStats, APP_UID1, null,
                 4321, 5432,
-                123, POWER_MODEL_POWER_PROFILE, 345, POWER_MODEL_MEASURED_ENERGY, 456, 567, 678);
+                123, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 345, POWER_MODEL_MEASURED_ENERGY,
+                456, 567, 678,
+                1777, 7771, 1888, 8881, 1999, 9991);
 
         addUidBatteryConsumer(builder, batteryStats, APP_UID2, "bar",
                 1111, 2222,
-                333, POWER_MODEL_POWER_PROFILE, 444, POWER_MODEL_POWER_PROFILE, 555, 666, 777);
+                333, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 444,
+                BatteryConsumer.POWER_MODEL_POWER_PROFILE, 555, 666, 777,
+                1777, 7771, 1888, 8881, 1999, 9991);
 
         addAggregateBatteryConsumer(builder,
                 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS, 0,
-                10123, 10234, 10345, 10456);
+                10123, 10234, 10345, 10456,
+                4333, 3334, 5444, 4445, 6555, 5556);
 
         addAggregateBatteryConsumer(builder,
                 BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE, 12345,
-                20111, 20222, 20333, 20444);
+                20111, 20222, 20333, 20444,
+                7333, 3337, 8444, 4448, 9555, 5559);
 
         return builder;
     }
 
     private void addUidBatteryConsumer(BatteryUsageStats.Builder builder,
             MockBatteryStatsImpl batteryStats, int uid, String packageWithHighestDrain,
-            int timeInStateForeground, int timeInStateBackground, int screenPower,
-            int screenPowerModel, int cpuPower, int cpuPowerModel, int customComponentPower,
-            int cpuDuration, int customComponentDuration) {
+            int timeInStateForeground, int timeInStateBackground, double screenPower,
+            int screenPowerModel, double cpuPower, int cpuPowerModel, double customComponentPower,
+            int cpuDuration, int customComponentDuration, double cpuPowerForeground,
+            int cpuDurationForeground, double cpuPowerBackground, int cpuDurationBackground,
+            double cpuPowerFgs, int cpuDurationFgs) {
         final BatteryStatsImpl.Uid batteryStatsUid = batteryStats.getUidStatsLocked(uid);
-        builder.getOrCreateUidBatteryConsumerBuilder(batteryStatsUid)
+        final UidBatteryConsumer.Builder uidBuilder =
+                builder.getOrCreateUidBatteryConsumerBuilder(batteryStatsUid);
+        uidBuilder
                 .setPackageWithHighestDrain(packageWithHighestDrain)
                 .setTimeInStateMs(UidBatteryConsumer.STATE_FOREGROUND, timeInStateForeground)
                 .setTimeInStateMs(UidBatteryConsumer.STATE_BACKGROUND, timeInStateBackground)
@@ -311,21 +354,68 @@
                         BatteryConsumer.POWER_COMPONENT_CPU, cpuDuration)
                 .setUsageDurationForCustomComponentMillis(
                         BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, customComponentDuration);
+        if (builder.isProcessStateDataNeeded()) {
+            final BatteryConsumer.Key cpuFgKey = uidBuilder.getKey(
+                    BatteryConsumer.POWER_COMPONENT_CPU,
+                    BatteryConsumer.PROCESS_STATE_FOREGROUND);
+            final BatteryConsumer.Key cpuBgKey = uidBuilder.getKey(
+                    BatteryConsumer.POWER_COMPONENT_CPU,
+                    BatteryConsumer.PROCESS_STATE_BACKGROUND);
+            final BatteryConsumer.Key cpuFgsKey = uidBuilder.getKey(
+                    BatteryConsumer.POWER_COMPONENT_CPU,
+                    BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE);
+            uidBuilder
+                    .setConsumedPower(cpuFgKey, cpuPowerForeground,
+                            BatteryConsumer.POWER_MODEL_POWER_PROFILE)
+                    .setUsageDurationMillis(cpuFgKey, cpuDurationForeground)
+                    .setConsumedPower(cpuBgKey, cpuPowerBackground,
+                            BatteryConsumer.POWER_MODEL_POWER_PROFILE)
+                    .setUsageDurationMillis(cpuBgKey, cpuDurationBackground)
+                    .setConsumedPower(cpuFgsKey, cpuPowerFgs,
+                            BatteryConsumer.POWER_MODEL_POWER_PROFILE)
+                    .setUsageDurationMillis(cpuFgsKey, cpuDurationFgs);
+        }
     }
 
     private void addAggregateBatteryConsumer(BatteryUsageStats.Builder builder, int scope,
             double consumedPower, int cpuPower, int customComponentPower, int cpuDuration,
-            int customComponentDuration) {
-        builder.getAggregateBatteryConsumerBuilder(scope)
-                .setConsumedPower(consumedPower)
-                .setConsumedPower(
-                        BatteryConsumer.POWER_COMPONENT_CPU, cpuPower)
-                .setConsumedPowerForCustomComponent(
-                        BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, customComponentPower)
-                .setUsageDurationMillis(
-                        BatteryConsumer.POWER_COMPONENT_CPU, cpuDuration)
-                .setUsageDurationForCustomComponentMillis(
-                        BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, customComponentDuration);
+            int customComponentDuration, double cpuPowerForeground, long cpuDurationForeground,
+            double cpuPowerBackground, long cpuDurationBackground, double cpuPowerFgs,
+            long cpuDurationFgs) {
+        final AggregateBatteryConsumer.Builder aggBuilder =
+                builder.getAggregateBatteryConsumerBuilder(scope)
+                        .setConsumedPower(consumedPower)
+                        .setConsumedPower(
+                                BatteryConsumer.POWER_COMPONENT_CPU, cpuPower)
+                        .setConsumedPowerForCustomComponent(
+                                BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID,
+                                customComponentPower)
+                        .setUsageDurationMillis(
+                                BatteryConsumer.POWER_COMPONENT_CPU, cpuDuration)
+                        .setUsageDurationForCustomComponentMillis(
+                                BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID,
+                                customComponentDuration);
+        if (builder.isProcessStateDataNeeded()) {
+            final BatteryConsumer.Key cpuFgKey = aggBuilder.getKey(
+                    BatteryConsumer.POWER_COMPONENT_CPU,
+                    BatteryConsumer.PROCESS_STATE_FOREGROUND);
+            final BatteryConsumer.Key cpuBgKey = aggBuilder.getKey(
+                    BatteryConsumer.POWER_COMPONENT_CPU,
+                    BatteryConsumer.PROCESS_STATE_BACKGROUND);
+            final BatteryConsumer.Key cpuFgsKey = aggBuilder.getKey(
+                    BatteryConsumer.POWER_COMPONENT_CPU,
+                    BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE);
+            aggBuilder
+                    .setConsumedPower(cpuFgKey, cpuPowerForeground,
+                            BatteryConsumer.POWER_MODEL_POWER_PROFILE)
+                    .setUsageDurationMillis(cpuFgKey, cpuDurationForeground)
+                    .setConsumedPower(cpuBgKey, cpuPowerBackground,
+                            BatteryConsumer.POWER_MODEL_POWER_PROFILE)
+                    .setUsageDurationMillis(cpuBgKey, cpuDurationBackground)
+                    .setConsumedPower(cpuFgsKey, cpuPowerFgs,
+                            BatteryConsumer.POWER_MODEL_POWER_PROFILE)
+                    .setUsageDurationMillis(cpuFgsKey, cpuDurationFgs);
+        }
     }
 
     public void assertBatteryUsageStats1(BatteryUsageStats batteryUsageStats,
@@ -338,8 +428,10 @@
         for (UidBatteryConsumer uidBatteryConsumer : uidBatteryConsumers) {
             if (uidBatteryConsumer.getUid() == APP_UID1) {
                 assertUidBatteryConsumer(uidBatteryConsumer, 1200, "foo",
-                        1000, 2000, 300, POWER_MODEL_POWER_PROFILE, 400, POWER_MODEL_POWER_PROFILE,
-                        500, 600, 800);
+                        1000, 2000, 300, BatteryConsumer.POWER_MODEL_POWER_PROFILE, 400,
+                        BatteryConsumer.POWER_MODEL_POWER_PROFILE,
+                        500, 600, 800,
+                        true, 1777, 1888, 1999, 1777, 7771, 1888, 8881, 1999, 9991);
             } else {
                 fail("Unexpected UID " + uidBatteryConsumer.getUid());
             }
@@ -384,10 +476,13 @@
     }
 
     private void assertUidBatteryConsumer(UidBatteryConsumer uidBatteryConsumer,
-            int consumedPower, String packageWithHighestDrain, int timeInStateForeground,
-            int timeInStateBackground, int screenPower, int screenPowerModel, int cpuPower,
-            int cpuPowerModel, int customComponentPower, int cpuDuration,
-            int customComponentDuration) {
+            double consumedPower, String packageWithHighestDrain, int timeInStateForeground,
+            int timeInStateBackground, int screenPower, int screenPowerModel, double cpuPower,
+            int cpuPowerModel, double customComponentPower, int cpuDuration,
+            int customComponentDuration, boolean processStateDataIncluded,
+            double totalPowerForeground, double totalPowerBackground, double totalPowerFgs,
+            double cpuPowerForeground, int cpuDurationForeground, double cpuPowerBackground,
+            int cpuDurationBackground, double cpuPowerFgs, int cpuDurationFgs) {
         assertThat(uidBatteryConsumer.getConsumedPower()).isEqualTo(consumedPower);
         assertThat(uidBatteryConsumer.getPackageWithHighestDrain()).isEqualTo(
                 packageWithHighestDrain);
@@ -413,6 +508,58 @@
         assertThat(uidBatteryConsumer.getCustomPowerComponentCount()).isEqualTo(1);
         assertThat(uidBatteryConsumer.getCustomPowerComponentName(
                 BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID)).isEqualTo("FOO");
+
+        if (processStateDataIncluded) {
+            assertThat(uidBatteryConsumer.getConsumedPower(
+                    new BatteryConsumer.Dimensions(POWER_COMPONENT_ANY,
+                            PROCESS_STATE_FOREGROUND)))
+                    .isEqualTo(totalPowerForeground);
+            assertThat(uidBatteryConsumer.getConsumedPower(
+                    new BatteryConsumer.Dimensions(POWER_COMPONENT_ANY,
+                            PROCESS_STATE_BACKGROUND)))
+                    .isEqualTo(totalPowerBackground);
+            assertThat(uidBatteryConsumer.getConsumedPower(
+                    new BatteryConsumer.Dimensions(POWER_COMPONENT_ANY,
+                            PROCESS_STATE_FOREGROUND_SERVICE)))
+                    .isEqualTo(totalPowerFgs);
+        }
+
+        final BatteryConsumer.Key cpuFgKey = uidBatteryConsumer.getKey(
+                BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_FOREGROUND);
+        if (processStateDataIncluded) {
+            assertThat(cpuFgKey).isNotNull();
+            assertThat(uidBatteryConsumer.getConsumedPower(cpuFgKey))
+                    .isEqualTo(cpuPowerForeground);
+            assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFgKey))
+                    .isEqualTo(cpuDurationForeground);
+        } else {
+            assertThat(cpuFgKey).isNull();
+        }
+
+        final BatteryConsumer.Key cpuBgKey = uidBatteryConsumer.getKey(
+                BatteryConsumer.POWER_COMPONENT_CPU, BatteryConsumer.PROCESS_STATE_BACKGROUND);
+        if (processStateDataIncluded) {
+            assertThat(cpuBgKey).isNotNull();
+            assertThat(uidBatteryConsumer.getConsumedPower(cpuBgKey))
+                    .isEqualTo(cpuPowerBackground);
+            assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuBgKey))
+                    .isEqualTo(cpuDurationBackground);
+        } else {
+            assertThat(cpuBgKey).isNull();
+        }
+
+        final BatteryConsumer.Key cpuFgsKey = uidBatteryConsumer.getKey(
+                BatteryConsumer.POWER_COMPONENT_CPU,
+                BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE);
+        if (processStateDataIncluded) {
+            assertThat(cpuFgsKey).isNotNull();
+            assertThat(uidBatteryConsumer.getConsumedPower(cpuFgsKey))
+                    .isEqualTo(cpuPowerFgs);
+            assertThat(uidBatteryConsumer.getUsageDurationMillis(cpuFgsKey))
+                    .isEqualTo(cpuDurationFgs);
+        } else {
+            assertThat(cpuFgsKey).isNotNull();
+        }
     }
 
     private void assertUserBatteryConsumer(UserBatteryConsumer userBatteryConsumer,
diff --git a/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java
new file mode 100644
index 0000000..8540adb
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.os;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class LongMultiStateCounterTest {
+
+    @Test
+    public void setStateAndUpdateValue() {
+        LongMultiStateCounter counter = new LongMultiStateCounter(2);
+
+        counter.updateValue(0, 1000);
+        counter.setState(0, 1000);
+        counter.setState(1, 2000);
+        counter.setState(0, 4000);
+        counter.updateValue(100, 9000);
+
+        assertThat(counter.getCount(0)).isEqualTo(75);
+        assertThat(counter.getCount(1)).isEqualTo(25);
+
+        assertThat(counter.toString()).isEqualTo("[0: 75, 1: 25] updated: 9000 currentState: 0");
+    }
+
+    @Test
+    public void setEnabled() {
+        LongMultiStateCounter counter = new LongMultiStateCounter(2);
+        counter.setState(0, 1000);
+        counter.updateValue(0, 1000);
+        counter.updateValue(100, 2000);
+
+        assertThat(counter.getCount(0)).isEqualTo(100);
+
+        counter.setEnabled(false, 3000);
+
+        // Partially included, because the counter is disabled after the previous update
+        counter.updateValue(200, 4000);
+
+        // Count only 50%, because the counter was disabled for 50% of the time
+        assertThat(counter.getCount(0)).isEqualTo(150);
+
+        // Not counted because the counter is disabled
+        counter.updateValue(250, 5000);
+
+        counter.setEnabled(true, 6000);
+
+        counter.updateValue(300, 7000);
+
+        // Again, take 50% of the delta
+        assertThat(counter.getCount(0)).isEqualTo(175);
+    }
+
+    @Test
+    public void reset() {
+        LongMultiStateCounter counter = new LongMultiStateCounter(2);
+        counter.setState(0, 1000);
+        counter.updateValue(0, 1000);
+        counter.updateValue(100, 2000);
+
+        assertThat(counter.getCount(0)).isEqualTo(100);
+
+        counter.reset();
+
+        assertThat(counter.getCount(0)).isEqualTo(0);
+
+        counter.updateValue(200, 3000);
+        counter.updateValue(300, 4000);
+
+        assertThat(counter.getCount(0)).isEqualTo(100);
+    }
+
+    @Test
+    public void parceling() {
+        LongMultiStateCounter counter = new LongMultiStateCounter(2);
+        counter.updateValue(0, 1000);
+        counter.setState(0, 1000);
+        counter.updateValue(100, 2000);
+        counter.setState(1, 2000);
+        counter.updateValue(101, 3000);
+
+        assertThat(counter.getCount(0)).isEqualTo(100);
+        assertThat(counter.getCount(1)).isEqualTo(1);
+
+        Parcel parcel = Parcel.obtain();
+        counter.writeToParcel(parcel, 0);
+        byte[] bytes = parcel.marshall();
+        parcel.recycle();
+
+        parcel = Parcel.obtain();
+        parcel.unmarshall(bytes, 0, bytes.length);
+        parcel.setDataPosition(0);
+
+        LongMultiStateCounter newCounter = LongMultiStateCounter.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+
+        assertThat(newCounter.getCount(0)).isEqualTo(100);
+        assertThat(newCounter.getCount(1)).isEqualTo(1);
+
+        // ==== Verify that the counter keeps accumulating after unparceling.
+
+        // State, last update timestamp and current counts are undefined at this point.
+        newCounter.setState(0, 100);
+        newCounter.updateValue(300, 100);
+
+        // A new base state and counters are established; we can continue accumulating deltas
+        newCounter.updateValue(316, 200);
+
+        assertThat(newCounter.getCount(0)).isEqualTo(116);
+    }
+}
diff --git a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
index d57eb65a..c24dc67 100644
--- a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
+++ b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
@@ -27,6 +27,7 @@
 import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidUserSysTimeReader;
 import com.android.internal.power.MeasuredEnergyStats;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Queue;
@@ -36,16 +37,23 @@
  * Mocks a BatteryStatsImpl object.
  */
 public class MockBatteryStatsImpl extends BatteryStatsImpl {
-    public Clock mClock;
     public boolean mForceOnBattery;
     private NetworkStats mNetworkStats;
+    private DummyExternalStatsSync mExternalStatsSync = new DummyExternalStatsSync();
+
+    MockBatteryStatsImpl() {
+        this(new MockClock());
+    }
 
     MockBatteryStatsImpl(Clock clock) {
-        super(clock);
-        this.mClock = mClock;
+        this(clock, null);
+    }
+
+    MockBatteryStatsImpl(Clock clock, File historyDirectory) {
+        super(clock, historyDirectory);
         initTimersAndCounters();
 
-        setExternalStatsSyncLocked(new DummyExternalStatsSync());
+        setExternalStatsSyncLocked(mExternalStatsSync);
         informThatAllExternalStatsAreFlushed();
 
         // A no-op handler.
@@ -53,10 +61,6 @@
         };
     }
 
-    MockBatteryStatsImpl() {
-        this(new MockClock());
-    }
-
     public void initMeasuredEnergyStats(String[] customBucketNames) {
         final boolean[] supportedStandardBuckets =
                 new boolean[MeasuredEnergyStats.NUMBER_STANDARD_POWER_BUCKETS];
@@ -182,7 +186,15 @@
         return mPendingUids;
     }
 
+    public int getAndClearExternalStatsSyncFlags() {
+        final int flags = mExternalStatsSync.flags;
+        mExternalStatsSync.flags = 0;
+        return flags;
+    }
+
     private class DummyExternalStatsSync implements ExternalStatsSync {
+        public int flags = 0;
+
         @Override
         public Future<?> scheduleSync(String reason, int flags) {
             return null;
@@ -216,8 +228,9 @@
         }
 
         @Override
-        public Future<?> scheduleSyncDueToScreenStateChange(
-                int flag, boolean onBattery, boolean onBatteryScreenOff, int screenState) {
+        public Future<?> scheduleSyncDueToScreenStateChange(int flag, boolean onBattery,
+                boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates) {
+            flags |= flag;
             return null;
         }
 
diff --git a/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java b/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java
index 5862368..88ee405 100644
--- a/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/PowerProfileTest.java
@@ -17,6 +17,10 @@
 
 package com.android.internal.os;
 
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_AMBIENT;
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL;
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_ON;
+
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
@@ -53,7 +57,12 @@
         assertEquals(4, mProfile.getNumSpeedStepsInCpuCluster(1));
         assertEquals(60.0, mProfile.getAveragePowerForCpuCore(1, 3));
         assertEquals(3000.0, mProfile.getBatteryCapacity());
-        assertEquals(0.5, mProfile.getAveragePower(PowerProfile.POWER_AMBIENT_DISPLAY));
+        assertEquals(0.5,
+                mProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, 0));
+        assertEquals(100.0,
+                mProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, 0));
+        assertEquals(800.0,
+                mProfile.getAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, 0));
         assertEquals(100.0, mProfile.getAveragePower(PowerProfile.POWER_AUDIO));
         assertEquals(150.0, mProfile.getAveragePower(PowerProfile.POWER_VIDEO));
     }
diff --git a/core/tests/coretests/src/com/android/internal/os/ScreenPowerCalculatorTest.java b/core/tests/coretests/src/com/android/internal/os/ScreenPowerCalculatorTest.java
index c695fc9..73f4eb2 100644
--- a/core/tests/coretests/src/com/android/internal/os/ScreenPowerCalculatorTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/ScreenPowerCalculatorTest.java
@@ -16,6 +16,9 @@
 
 package com.android.internal.os;
 
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_FULL;
+import static com.android.internal.os.PowerProfile.POWER_GROUP_DISPLAY_SCREEN_ON;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.ActivityManager;
@@ -42,15 +45,15 @@
 
     @Rule
     public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
-            .setAveragePower(PowerProfile.POWER_SCREEN_ON, 36.0)
-            .setAveragePower(PowerProfile.POWER_SCREEN_FULL, 48.0);
+            .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, 0, 36.0)
+            .setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, 0, 48.0);
 
     @Test
     public void testMeasuredEnergyBasedModel() {
         mStatsRule.initMeasuredEnergyStatsLocked();
         BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
 
-        batteryStats.noteScreenStateLocked(Display.STATE_ON, 0, 0, 0);
+        batteryStats.noteScreenStateLocked(0, Display.STATE_ON, 0, 0, 0);
         batteryStats.updateDisplayMeasuredEnergyStatsLocked(0, Display.STATE_ON, 0);
         setProcState(APP_UID1, ActivityManager.PROCESS_STATE_TOP, true,
                 0, 0);
@@ -67,7 +70,7 @@
         batteryStats.updateDisplayMeasuredEnergyStatsLocked(300_000_000, Display.STATE_ON,
                 60 * MINUTE_IN_MS);
 
-        batteryStats.noteScreenStateLocked(Display.STATE_OFF,
+        batteryStats.noteScreenStateLocked(0, Display.STATE_OFF,
                 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
         setProcState(APP_UID2, ActivityManager.PROCESS_STATE_TOP_SLEEPING, false,
                 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
@@ -130,20 +133,20 @@
     public void testPowerProfileBasedModel() {
         BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
 
-        batteryStats.noteScreenStateLocked(Display.STATE_ON, 0, 0, 0);
-        batteryStats.noteScreenBrightnessLocked(255, 0, 0);
+        batteryStats.noteScreenStateLocked(0, Display.STATE_ON, 0, 0, 0);
+        batteryStats.noteScreenBrightnessLocked(0, 255, 0, 0);
         setProcState(APP_UID1, ActivityManager.PROCESS_STATE_TOP, true,
                 0, 0);
 
-        batteryStats.noteScreenBrightnessLocked(100, 5 * MINUTE_IN_MS, 5 * MINUTE_IN_MS);
-        batteryStats.noteScreenBrightnessLocked(200, 10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
+        batteryStats.noteScreenBrightnessLocked(0, 100, 5 * MINUTE_IN_MS, 5 * MINUTE_IN_MS);
+        batteryStats.noteScreenBrightnessLocked(0, 200, 10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
 
         setProcState(APP_UID1, ActivityManager.PROCESS_STATE_CACHED_EMPTY, false,
                 20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
         setProcState(APP_UID2, ActivityManager.PROCESS_STATE_TOP, true,
                 20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
 
-        batteryStats.noteScreenStateLocked(Display.STATE_OFF,
+        batteryStats.noteScreenStateLocked(0, Display.STATE_OFF,
                 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
         setProcState(APP_UID2, ActivityManager.PROCESS_STATE_TOP_SLEEPING, false,
                 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
diff --git a/core/tests/overlaytests/host/Android.bp b/core/tests/overlaytests/host/Android.bp
index 08761f6..6340980 100644
--- a/core/tests/overlaytests/host/Android.bp
+++ b/core/tests/overlaytests/host/Android.bp
@@ -27,7 +27,6 @@
     libs: ["tradefed"],
     test_suites: [
         "device-tests",
-        "general-tests",
     ],
     target_required: [
         "OverlayHostTests_NonPlatformSignatureOverlay",
diff --git a/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.mk b/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.mk
index 15fb76d..b453cde9 100644
--- a/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.mk
+++ b/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.mk
@@ -23,7 +23,7 @@
 LOCAL_LICENSE_CONDITIONS := notice
 LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../../NOTICE
 LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests general-tests
+LOCAL_COMPATIBILITY_SUITE := device-tests
 LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_bad
 include $(BUILD_PACKAGE)
 
@@ -34,7 +34,7 @@
 LOCAL_LICENSE_CONDITIONS := notice
 LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../../NOTICE
 LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests general-tests
+LOCAL_COMPATIBILITY_SUITE := device-tests
 LOCAL_CERTIFICATE := platform
 LOCAL_MANIFEST_FILE := static/AndroidManifest.xml
 LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_static
@@ -47,7 +47,7 @@
 LOCAL_LICENSE_CONDITIONS := notice
 LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../../NOTICE
 LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests general-tests
+LOCAL_COMPATIBILITY_SUITE := device-tests
 LOCAL_CERTIFICATE := platform
 LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v1
 LOCAL_AAPT_FLAGS += --version-code 1 --version-name v1
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.mk b/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.mk
index 3921251..77fc887 100644
--- a/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.mk
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.mk
@@ -22,7 +22,7 @@
 LOCAL_LICENSE_CONDITIONS := notice
 LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../../NOTICE
 LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests general-tests
+LOCAL_COMPATIBILITY_SUITE := device-tests
 LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules
 LOCAL_USE_AAPT2 := true
 LOCAL_AAPT_FLAGS := --no-resource-removal
@@ -37,7 +37,7 @@
 LOCAL_LICENSE_CONDITIONS := notice
 LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../../NOTICE
 LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests general-tests
+LOCAL_COMPATIBILITY_SUITE := device-tests
 LOCAL_CERTIFICATE := platform
 LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v1
 LOCAL_AAPT_FLAGS += --version-code 1 --version-name v1
@@ -52,7 +52,7 @@
 LOCAL_LICENSE_CONDITIONS := notice
 LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../../NOTICE
 LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests general-tests
+LOCAL_COMPATIBILITY_SUITE := device-tests
 LOCAL_CERTIFICATE := platform
 LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v2
 LOCAL_AAPT_FLAGS += --version-code 2 --version-name v2
@@ -69,7 +69,7 @@
 LOCAL_LICENSE_CONDITIONS := notice
 LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../../NOTICE
 LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests general-tests
+LOCAL_COMPATIBILITY_SUITE := device-tests
 LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v1
 LOCAL_AAPT_FLAGS += --version-code 1 --version-name v1
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/app/v1/res
@@ -83,7 +83,7 @@
 LOCAL_LICENSE_CONDITIONS := notice
 LOCAL_NOTICE_FILE  := $(LOCAL_PATH)/../../../../../../NOTICE
 LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests general-tests
+LOCAL_COMPATIBILITY_SUITE := device-tests
 LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v2
 LOCAL_AAPT_FLAGS += --version-code 2 --version-name v2
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/app/v2/res
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 909ca39..9573607 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -595,6 +595,12 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "-1478175541": {
+      "message": "No longer animating wallpaper targets!",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "-1474602871": {
       "message": "Launch on display check: disallow launch on virtual display for not-embedded activity.",
       "level": "DEBUG",
@@ -1621,6 +1627,12 @@
       "group": "WM_DEBUG_TASKS",
       "at": "com\/android\/server\/wm\/RootWindowContainer.java"
     },
+    "-360208282": {
+      "message": "Animating wallpapers: old: %s hidden=%b new: %s hidden=%b",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "-354571697": {
       "message": "Existence Changed in transition %d: %s",
       "level": "VERBOSE",
@@ -1675,6 +1687,12 @@
       "group": "WM_DEBUG_LAYER_MIRRORING",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "-304728471": {
+      "message": "New wallpaper: target=%s prev=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "-302468788": {
       "message": "Expected target rootTask=%s to be top most but found rootTask=%s",
       "level": "WARN",
@@ -1693,6 +1711,12 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "-275077723": {
+      "message": "New animation: %s old animation: %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "-262984451": {
       "message": "Relaunch failed %s",
       "level": "INFO",
@@ -1747,6 +1771,12 @@
       "group": "WM_DEBUG_LAYER_MIRRORING",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "-182877285": {
+      "message": "Wallpaper layer changed: assigning layers + relayout",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "-177040661": {
       "message": "Start rotation animation. customAnim=%s, mCurRotation=%s, mOriginalRotation=%s",
       "level": "DEBUG",
@@ -2005,6 +2035,12 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "114070759": {
+      "message": "New wallpaper target: %s prevTarget: %s caller=%s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "115358443": {
       "message": "Focus changing: %s -> %s",
       "level": "INFO",
@@ -2347,6 +2383,12 @@
       "group": "WM_DEBUG_RESIZE",
       "at": "com\/android\/server\/wm\/WindowState.java"
     },
+    "422634333": {
+      "message": "First draw done in potential wallpaper target %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "424524729": {
       "message": "Attempted to add wallpaper window with unknown token %s.  Aborting.",
       "level": "WARN",
@@ -2371,12 +2413,6 @@
       "group": "WM_ERROR",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
-    "457951957": {
-      "message": "\tNot visible=%s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_REMOTE_ANIMATIONS",
-      "at": "com\/android\/server\/wm\/WallpaperAnimationAdapter.java"
-    },
     "463993897": {
       "message": "Aborted waiting for drawn: %s",
       "level": "WARN",
@@ -2425,6 +2461,12 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/WindowContainerThumbnail.java"
     },
+    "535103992": {
+      "message": "Wallpaper may change!  Adjusting",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/RootWindowContainer.java"
+    },
     "539077569": {
       "message": "Clear freezing of %s force=%b",
       "level": "VERBOSE",
@@ -2653,6 +2695,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
+    "733466617": {
+      "message": "Wallpaper token %s visible=%b",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperWindowToken.java"
+    },
     "736692676": {
       "message": "Config is relaunching %s",
       "level": "VERBOSE",
@@ -2989,6 +3037,12 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/DisplayContent.java"
     },
+    "1178653181": {
+      "message": "Old wallpaper still the target.",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "1186730970": {
       "message": "          no common mode yet, so set it",
       "level": "VERBOSE",
@@ -3667,6 +3721,12 @@
       "group": "WM_SHOW_TRANSACTIONS",
       "at": "com\/android\/server\/wm\/WindowAnimator.java"
     },
+    "1984843251": {
+      "message": "Hiding wallpaper %s from %s target=%s prev=%s callers=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_WALLPAPER",
+      "at": "com\/android\/server\/wm\/WallpaperController.java"
+    },
     "1995093920": {
       "message": "Checking to restart %s: changed=0x%s, handles=0x%s, mLastReportedConfiguration=%s",
       "level": "VERBOSE",
@@ -3697,6 +3757,12 @@
       "group": "WM_DEBUG_REMOTE_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RemoteAnimationController.java"
     },
+    "2024493888": {
+      "message": "\tWallpaper of display=%s is not visible",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_REMOTE_ANIMATIONS",
+      "at": "com\/android\/server\/wm\/WallpaperAnimationAdapter.java"
+    },
     "2028163120": {
       "message": "applyAnimation: anim=%s nextAppTransition=ANIM_SCALE_UP transit=%s isEntrance=%s Callers=%s",
       "level": "VERBOSE",
@@ -3721,12 +3787,6 @@
       "group": "WM_DEBUG_APP_TRANSITIONS",
       "at": "com\/android\/server\/wm\/AppTransitionController.java"
     },
-    "2057434754": {
-      "message": "\tvisible=%s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_REMOTE_ANIMATIONS",
-      "at": "com\/android\/server\/wm\/WallpaperAnimationAdapter.java"
-    },
     "2060978050": {
       "message": "moveWindowTokenToDisplay: Attempted to move token: %s to non-exiting displayId=%d",
       "level": "WARN",
@@ -3867,6 +3927,9 @@
     "WM_DEBUG_TASKS": {
       "tag": "WindowManager"
     },
+    "WM_DEBUG_WALLPAPER": {
+      "tag": "WindowManager"
+    },
     "WM_DEBUG_WINDOW_INSETS": {
       "tag": "WindowManager"
     },
diff --git a/graphics/java/android/graphics/Outline.java b/graphics/java/android/graphics/Outline.java
index 5858e39..b77865f 100644
--- a/graphics/java/android/graphics/Outline.java
+++ b/graphics/java/android/graphics/Outline.java
@@ -312,7 +312,10 @@
     }
 
     /**
-     * Offsets the Outline by (dx,dy)
+     * Offsets the Outline by (dx,dy). Offsetting is cumulative, so additional calls to
+     * offset() will add to previous offset values. Offset only applies to the current
+     * geometry (setRect(), setPath(), etc.); setting new geometry resets any existing
+     * offset.
      */
     public void offset(int dx, int dy) {
         if (mMode == MODE_ROUND_RECT) {
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java
index b24a22d..16f732f 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java
@@ -22,6 +22,8 @@
 import android.system.keystore2.Domain;
 import android.system.keystore2.KeyDescriptor;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.security.Key;
 
 /**
@@ -46,7 +48,11 @@
     // We do not include this member in comparisons.
     private final KeyStoreSecurityLevel mSecurityLevel;
 
-    AndroidKeyStoreKey(@NonNull KeyDescriptor descriptor,
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public AndroidKeyStoreKey(@NonNull KeyDescriptor descriptor,
             long keyId,
             @NonNull Authorization[] authorizations,
             @NonNull String algorithm,
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java
index 4842984..0b3be32 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStorePublicKey.java
@@ -23,7 +23,7 @@
 import android.system.keystore2.KeyMetadata;
 
 import java.security.PublicKey;
-import java.util.Objects;
+import java.util.Arrays;
 
 /**
  * {@link PublicKey} backed by Android Keystore.
@@ -62,8 +62,8 @@
         int result = 1;
 
         result = prime * result + super.hashCode();
-        result = prime * result + ((mCertificate == null) ? 0 : mCertificate.hashCode());
-        result = prime * result + ((mCertificateChain == null) ? 0 : mCertificateChain.hashCode());
+        result = prime * result + Arrays.hashCode(mCertificate);
+        result = prime * result + Arrays.hashCode(mCertificateChain);
 
         return result;
     }
@@ -83,7 +83,7 @@
          */
         final AndroidKeyStorePublicKey other = (AndroidKeyStorePublicKey) obj;
 
-        return Objects.equals(mCertificate, other.mCertificate) && Objects.equals(mCertificateChain,
+        return Arrays.equals(mCertificate, other.mCertificate) && Arrays.equals(mCertificateChain,
                 other.mCertificateChain);
     }
 }
diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
index 67358c4..33411e1 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
@@ -601,8 +601,6 @@
         }
         KeyProtection params = (KeyProtection) param;
 
-        @SecurityLevel int securityLevel = params.isStrongBoxBacked() ? SecurityLevel.STRONGBOX :
-                SecurityLevel.TRUSTED_ENVIRONMENT;
         @Domain int targetDomain = (getTargetDomain());
 
         if (key instanceof AndroidKeyStoreSecretKey) {
@@ -794,6 +792,9 @@
             flags |= IKeystoreSecurityLevel.KEY_FLAG_AUTH_BOUND_WITHOUT_CRYPTOGRAPHIC_LSKF_BINDING;
         }
 
+        @SecurityLevel int securityLevel = params.isStrongBoxBacked() ? SecurityLevel.STRONGBOX :
+                SecurityLevel.TRUSTED_ENVIRONMENT;
+
         try {
             KeyStoreSecurityLevel securityLevelInterface = mKeyStore.getSecurityLevel(
                     securityLevel);
diff --git a/keystore/tests/src/android/security/keystore2/AndroidKeyStoreSpiTest.java b/keystore/tests/src/android/security/keystore2/AndroidKeyStoreSpiTest.java
index 1bd3069..f96c39c8 100644
--- a/keystore/tests/src/android/security/keystore2/AndroidKeyStoreSpiTest.java
+++ b/keystore/tests/src/android/security/keystore2/AndroidKeyStoreSpiTest.java
@@ -24,7 +24,13 @@
 
 import android.security.KeyStore2;
 import android.security.KeyStoreException;
+import android.security.KeyStoreSecurityLevel;
+import android.system.keystore2.Authorization;
+import android.system.keystore2.Domain;
+import android.system.keystore2.KeyDescriptor;
+import android.system.keystore2.KeyMetadata;
 
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
@@ -52,4 +58,112 @@
         verify(mKeystore2).list(anyInt(), anyLong());
     }
 
+    @Mock
+    private KeyStoreSecurityLevel mKeystoreSecurityLevel;
+
+    private static KeyDescriptor descriptor() {
+        final KeyDescriptor keyDescriptor = new KeyDescriptor();
+        keyDescriptor.alias = "key";
+        keyDescriptor.blob = null;
+        keyDescriptor.domain = Domain.APP;
+        keyDescriptor.nspace = -1;
+        return keyDescriptor;
+    }
+
+    private static KeyMetadata metadata(byte[] cert, byte[] certChain) {
+        KeyMetadata metadata = new KeyMetadata();
+        metadata.authorizations = new Authorization[0];
+        metadata.certificate = cert;
+        metadata.certificateChain = certChain;
+        metadata.key = descriptor();
+        metadata.modificationTimeMs = 0;
+        metadata.keySecurityLevel = 1;
+        return metadata;
+    }
+
+    private static byte[] bytes(String string) {
+        return string.getBytes();
+    }
+
+    class MyPublicKey extends AndroidKeyStorePublicKey {
+        MyPublicKey(String cert, String chain, KeyStoreSecurityLevel securityLevel) {
+            super(descriptor(), metadata(cert.getBytes(), chain.getBytes()), "N/A".getBytes(),
+                    "RSA", securityLevel);
+        }
+
+        @Override
+        AndroidKeyStorePrivateKey getPrivateKey() {
+            return null;
+        }
+    }
+
+    private AndroidKeyStorePublicKey makePrivateKeyObject(String cert, String chain) {
+        return new MyPublicKey(cert, chain, mKeystoreSecurityLevel);
+    }
+
+    @Test
+    public void testKeystoreKeysAdhereToContractBetweenEqualsAndHashCode() throws Exception {
+        AndroidKeyStoreKey key1 = new AndroidKeyStoreKey(descriptor(), 1, new Authorization[0],
+                "RSA", mKeystoreSecurityLevel);
+        AndroidKeyStoreKey key2 = new AndroidKeyStoreKey(descriptor(), 2, new Authorization[0],
+                "RSA", mKeystoreSecurityLevel);
+        AndroidKeyStoreKey key1_clone = new AndroidKeyStoreKey(descriptor(), 1,
+                new Authorization[0], "RSA", mKeystoreSecurityLevel);
+
+        assertThat("Identity should yield true", key1.equals(key1));
+        Assert.assertEquals("Identity should yield same hash codes",
+                key1.hashCode(), key1.hashCode());
+        assertThat("Identity should yield true", key2.equals(key2));
+        Assert.assertEquals("Identity should yield same hash codes",
+                key2.hashCode(), key2.hashCode());
+        assertThat("Different keys should differ", !key1.equals(key2));
+        Assert.assertNotEquals("Different keys should have different hash codes",
+                key1.hashCode(), key2.hashCode());
+
+        assertThat("Same keys should yield true", key1.equals(key1_clone));
+        assertThat("Same keys should yield true", key1_clone.equals(key1));
+        Assert.assertEquals("Same keys should yield same hash codes",
+                key1.hashCode(), key1_clone.hashCode());
+
+        assertThat("anything.equal(null) should yield false", !key1.equals(null));
+        assertThat("anything.equal(null) should yield false", !key2.equals(null));
+        assertThat("anything.equal(null) should yield false", !key1_clone.equals(null));
+    }
+
+    @Test
+    public void testKeystorePublicKeysAdhereToContractBetweenEqualsAndHashCode() throws Exception {
+        AndroidKeyStorePublicKey key1 = makePrivateKeyObject("myCert1", "myChain1");
+        AndroidKeyStorePublicKey key2 = makePrivateKeyObject("myCert2", "myChain1");
+        AndroidKeyStorePublicKey key3 = makePrivateKeyObject("myCert1", "myChain3");
+        AndroidKeyStorePublicKey key1_clone = makePrivateKeyObject("myCert1", "myChain1");
+
+        assertThat("Identity should yield true", key1.equals(key1));
+        Assert.assertEquals("Identity should yield same hash codes",
+                key1.hashCode(), key1.hashCode());
+        assertThat("Identity should yield true", key2.equals(key2));
+        Assert.assertEquals("Identity should yield same hash codes",
+                key2.hashCode(), key2.hashCode());
+        assertThat("Identity should yield true", key3.equals(key3));
+        Assert.assertEquals("Identity should yield same hash codes",
+                key3.hashCode(), key3.hashCode());
+        assertThat("Different keys should differ", !key1.equals(key2));
+        Assert.assertNotEquals("Different keys should have different hash codes",
+                key1.hashCode(), key2.hashCode());
+        assertThat("Different keys should differ", !key1.equals(key3));
+        Assert.assertNotEquals("Different keys should have different hash codes",
+                key1.hashCode(), key3.hashCode());
+        assertThat("Different keys should differ", !key2.equals(key3));
+        Assert.assertNotEquals("Different keys should have different hash codes",
+                key2.hashCode(), key3.hashCode());
+
+        assertThat("Same keys should yield true", key1.equals(key1_clone));
+        assertThat("Same keys should yield true", key1_clone.equals(key1));
+        Assert.assertEquals("Same keys should yield same hash codes",
+                key1.hashCode(), key1_clone.hashCode());
+
+        assertThat("anything.equal(null) should yield false", !key1.equals(null));
+        assertThat("anything.equal(null) should yield false", !key2.equals(null));
+        assertThat("anything.equal(null) should yield false", !key3.equals(null));
+        assertThat("anything.equal(null) should yield false", !key1_clone.equals(null));
+    }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 990d7b6..bdf703c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -39,11 +39,12 @@
         return 1;
     }
 
-    @Override
-    public boolean isWindowLayoutComponentAvailable() {
-        return true;
-    }
-
+    /**
+     * Returns a reference implementation of {@link WindowLayoutComponent} if available,
+     * {@code null} otherwise. The implementation must match the API level reported in
+     * {@link WindowExtensions#getWindowLayoutComponent()}.
+     * @return {@link WindowLayoutComponent} OEM implementation
+     */
     @Override
     public WindowLayoutComponent getWindowLayoutComponent() {
         if (mWindowLayoutComponent == null) {
@@ -58,24 +59,10 @@
     }
 
     /**
-     * Returns {@code true} if {@link ActivityEmbeddingComponent} is present on the device,
-     * {@code false} otherwise. If the component is not available the developer will receive a
-     * single callback with empty data or default values where possible.
-     */
-    @Override
-    public boolean isEmbeddingComponentAvailable() {
-        return true;
-    }
-
-    /**
-     * Returns the OEM implementation of {@link ActivityEmbeddingComponent} if it is supported on
-     * the device. The implementation must match the API level reported in
-     * {@link androidx.window.extensions.WindowExtensions}. An
-     * {@link UnsupportedOperationException} will be thrown if the device does not support
-     * Activity Embedding. Use
-     * {@link WindowExtensions#isEmbeddingComponentAvailable()} to determine if
-     * {@link ActivityEmbeddingComponent} is present.
-     * @return the OEM implementation of {@link ActivityEmbeddingComponent}
+     * Returns a reference implementation of {@link ActivityEmbeddingComponent} if available,
+     * {@code null} otherwise. The implementation must match the API level reported in
+     * {@link WindowExtensions#getWindowLayoutComponent()}.
+     * @return {@link ActivityEmbeddingComponent} OEM implementation.
      */
     @NonNull
     public ActivityEmbeddingComponent getActivityEmbeddingComponent() {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
index 06f6228..194b633 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java
@@ -16,7 +16,8 @@
 
 package androidx.window.extensions.embedding;
 
-import android.graphics.Point;
+import static android.graphics.Matrix.MSCALE_X;
+
 import android.graphics.Rect;
 import android.view.Choreographer;
 import android.view.RemoteAnimationTarget;
@@ -25,58 +26,151 @@
 import android.view.animation.Transformation;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 
 /**
  * Wrapper to handle the TaskFragment animation update in one {@link SurfaceControl.Transaction}.
+ *
+ * The base adapter can be used for {@link RemoteAnimationTarget} that is simple open/close.
  */
 class TaskFragmentAnimationAdapter {
-    private final Animation mAnimation;
-    private final RemoteAnimationTarget mTarget;
-    private final SurfaceControl mLeash;
-    private final boolean mSizeChanged;
-    private final Point mPosition;
-    private final Transformation mTransformation = new Transformation();
-    private final float[] mMatrix = new float[9];
-    private final float[] mVecs = new float[4];
-    private final Rect mRect = new Rect();
+    final Animation mAnimation;
+    final RemoteAnimationTarget mTarget;
+    final SurfaceControl mLeash;
+
+    final Transformation mTransformation = new Transformation();
+    final float[] mMatrix = new float[9];
     private boolean mIsFirstFrame = true;
 
     TaskFragmentAnimationAdapter(@NonNull Animation animation,
             @NonNull RemoteAnimationTarget target) {
-        this(animation, target, target.leash, false /* sizeChanged */, null /* position */);
+        this(animation, target, target.leash);
     }
 
     /**
-     * @param sizeChanged whether the surface size needs to be changed.
+     * @param leash the surface to animate.
      */
     TaskFragmentAnimationAdapter(@NonNull Animation animation,
-            @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash,
-            boolean sizeChanged, @Nullable Point position) {
+            @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash) {
         mAnimation = animation;
         mTarget = target;
         mLeash = leash;
-        mSizeChanged = sizeChanged;
-        mPosition = position != null
-                ? position
-                : new Point(target.localBounds.left, target.localBounds.top);
     }
 
     /** Called on frame update. */
-    void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) {
+    final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) {
         if (mIsFirstFrame) {
             t.show(mLeash);
             mIsFirstFrame = false;
         }
 
-        currentPlayTime = Math.min(currentPlayTime, mAnimation.getDuration());
-        mAnimation.getTransformation(currentPlayTime, mTransformation);
-        mTransformation.getMatrix().postTranslate(mPosition.x, mPosition.y);
+        // Extract the transformation to the current time.
+        mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()),
+                mTransformation);
+        t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+        onAnimationUpdateInner(t);
+    }
+
+    /** To be overridden by subclasses to adjust the animation surface change. */
+    void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+        mTransformation.getMatrix().postTranslate(
+                mTarget.localBounds.left, mTarget.localBounds.top);
         t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
         t.setAlpha(mLeash, mTransformation.getAlpha());
-        t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+    }
 
-        if (mSizeChanged) {
+    /** Called after animation finished. */
+    final void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
+        onAnimationUpdate(t, mAnimation.getDuration());
+    }
+
+    final long getDurationHint() {
+        return mAnimation.computeDurationHint();
+    }
+
+    /**
+     * Should be used when the {@link RemoteAnimationTarget} is in split with others, and want to
+     * animate together as one. This adapter will offset the animation leash to make the animate of
+     * two windows look like a single window.
+     */
+    static class SplitAdapter extends TaskFragmentAnimationAdapter {
+        private final boolean mIsLeftHalf;
+        private final int mWholeAnimationWidth;
+
+        /**
+         * @param isLeftHalf whether this is the left half of the animation.
+         * @param wholeAnimationWidth the whole animation windows width.
+         */
+        SplitAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target,
+                boolean isLeftHalf, int wholeAnimationWidth) {
+            super(animation, target);
+            mIsLeftHalf = isLeftHalf;
+            mWholeAnimationWidth = wholeAnimationWidth;
+            if (wholeAnimationWidth == 0) {
+                throw new IllegalArgumentException("SplitAdapter must provide wholeAnimationWidth");
+            }
+        }
+
+        @Override
+        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+            float posX = mTarget.localBounds.left;
+            final float posY = mTarget.localBounds.top;
+            // This window is half of the whole animation window. Offset left/right to make it
+            // look as one with the other half.
+            mTransformation.getMatrix().getValues(mMatrix);
+            final int targetWidth = mTarget.localBounds.width();
+            final float scaleX = mMatrix[MSCALE_X];
+            final float totalOffset = mWholeAnimationWidth * (1 - scaleX) / 2;
+            final float curOffset = targetWidth * (1 - scaleX) / 2;
+            final float offsetDiff = totalOffset - curOffset;
+            if (mIsLeftHalf) {
+                posX += offsetDiff;
+            } else {
+                posX -= offsetDiff;
+            }
+            mTransformation.getMatrix().postTranslate(posX, posY);
+            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+            t.setAlpha(mLeash, mTransformation.getAlpha());
+        }
+    }
+
+    /**
+     * Should be used for the animation of the snapshot of a {@link RemoteAnimationTarget} that has
+     * size change.
+     */
+    static class SnapshotAdapter extends TaskFragmentAnimationAdapter {
+
+        SnapshotAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) {
+            // Start leash is the snapshot of the starting surface.
+            super(animation, target, target.startLeash);
+        }
+
+        @Override
+        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+            // Snapshot should always be placed at the top left of the animation leash.
+            mTransformation.getMatrix().postTranslate(0, 0);
+            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+            t.setAlpha(mLeash, mTransformation.getAlpha());
+        }
+    }
+
+    /**
+     * Should be used for the animation of the {@link RemoteAnimationTarget} that has size change.
+     */
+    static class BoundsChangeAdapter extends TaskFragmentAnimationAdapter {
+        private final float[] mVecs = new float[4];
+        private final Rect mRect = new Rect();
+
+        BoundsChangeAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) {
+            super(animation, target);
+        }
+
+        @Override
+        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
+            mTransformation.getMatrix().postTranslate(
+                    mTarget.localBounds.left, mTarget.localBounds.top);
+            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
+            t.setAlpha(mLeash, mTransformation.getAlpha());
+
             // The following applies an inverse scale to the clip-rect so that it crops "after" the
             // scale instead of before.
             mVecs[1] = mVecs[2] = 0;
@@ -92,13 +186,4 @@
             t.setWindowCrop(mLeash, mRect);
         }
     }
-
-    /** Called after animation finished. */
-    void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
-        onAnimationUpdate(t, mAnimation.getDuration());
-    }
-
-    long getDurationHint() {
-        return mAnimation.computeDurationHint();
-    }
 }
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 6579766..535dac1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
@@ -29,7 +29,6 @@
 class TaskFragmentAnimationController {
 
     private static final String TAG = "TaskFragAnimationCtrl";
-    // TODO(b/196173550) turn off when finalize
     static final boolean DEBUG = false;
 
     private final TaskFragmentOrganizer mOrganizer;
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 3980d07..412559e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -23,7 +23,7 @@
 
 import android.animation.Animator;
 import android.animation.ValueAnimator;
-import android.graphics.Point;
+import android.graphics.Rect;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.RemoteException;
@@ -40,6 +40,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.BiFunction;
 
 /** To run the TaskFragment animations. */
 class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub {
@@ -167,42 +168,86 @@
 
     private List<TaskFragmentAnimationAdapter> createOpenAnimationAdapters(
             @NonNull RemoteAnimationTarget[] targets) {
-        final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
-        for (RemoteAnimationTarget target : targets) {
-            final Animation animation =
-                    mAnimationSpec.loadOpenAnimation(target.mode != MODE_CLOSING /* isEnter */);
-            adapters.add(new TaskFragmentAnimationAdapter(animation, target));
-        }
-        return adapters;
+        return createOpenCloseAnimationAdapters(targets,
+                mAnimationSpec::loadOpenAnimation);
     }
 
     private List<TaskFragmentAnimationAdapter> createCloseAnimationAdapters(
             @NonNull RemoteAnimationTarget[] targets) {
-        final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
+        return createOpenCloseAnimationAdapters(targets,
+                mAnimationSpec::loadCloseAnimation);
+    }
+
+    private List<TaskFragmentAnimationAdapter> createOpenCloseAnimationAdapters(
+            @NonNull RemoteAnimationTarget[] targets,
+            @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider) {
+        // We need to know if the target window is only a partial of the whole animation screen.
+        // If so, we will need to adjust it to make the whole animation screen looks like one.
+        final List<RemoteAnimationTarget> openingTargets = new ArrayList<>();
+        final List<RemoteAnimationTarget> closingTargets = new ArrayList<>();
+        final Rect openingWholeScreenBounds = new Rect();
+        final Rect closingWholeScreenBounds = new Rect();
         for (RemoteAnimationTarget target : targets) {
-            final Animation animation =
-                    mAnimationSpec.loadCloseAnimation(target.mode != MODE_CLOSING /* isEnter */);
-            adapters.add(new TaskFragmentAnimationAdapter(animation, target));
+            if (target.mode != MODE_CLOSING) {
+                openingTargets.add(target);
+                openingWholeScreenBounds.union(target.localBounds);
+            } else {
+                closingTargets.add(target);
+                closingWholeScreenBounds.union(target.localBounds);
+            }
+        }
+
+        final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
+        for (RemoteAnimationTarget target : openingTargets) {
+            adapters.add(createOpenCloseAnimationAdapter(target, animationProvider,
+                    openingWholeScreenBounds));
+        }
+        for (RemoteAnimationTarget target : closingTargets) {
+            adapters.add(createOpenCloseAnimationAdapter(target, animationProvider,
+                    closingWholeScreenBounds));
         }
         return adapters;
     }
 
+    private TaskFragmentAnimationAdapter createOpenCloseAnimationAdapter(
+            @NonNull RemoteAnimationTarget target,
+            @NonNull BiFunction<RemoteAnimationTarget, Rect, Animation> animationProvider,
+            @NonNull Rect wholeAnimationBounds) {
+        final Animation animation = animationProvider.apply(target, wholeAnimationBounds);
+        final Rect targetBounds = target.localBounds;
+        if (targetBounds.left == wholeAnimationBounds.left
+                && targetBounds.right != wholeAnimationBounds.right) {
+            // This is the left split of the whole animation window.
+            return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target,
+                    true /* isLeftHalf */, wholeAnimationBounds.width());
+        } else if (targetBounds.left != wholeAnimationBounds.left
+                && targetBounds.right == wholeAnimationBounds.right) {
+            // This is the right split of the whole animation window.
+            return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target,
+                    false /* isLeftHalf */, wholeAnimationBounds.width());
+        }
+        // Open/close window that fills the whole animation.
+        return new TaskFragmentAnimationAdapter(animation, target);
+    }
+
     private List<TaskFragmentAnimationAdapter> createChangeAnimationAdapters(
             @NonNull RemoteAnimationTarget[] targets) {
         final List<TaskFragmentAnimationAdapter> adapters = new ArrayList<>();
         for (RemoteAnimationTarget target : targets) {
             if (target.startBounds != null) {
+                // This is the target with bounds change.
                 final Animation[] animations =
                         mAnimationSpec.createChangeBoundsChangeAnimations(target);
-                // The snapshot surface will always be at (0, 0) of its parent.
-                adapters.add(new TaskFragmentAnimationAdapter(animations[0], target,
-                        target.startLeash, false /* sizeChanged */, new Point(0, 0)));
-                // The end surface will have size change for scaling.
-                adapters.add(new TaskFragmentAnimationAdapter(animations[1], target,
-                        target.leash, true /* sizeChanged */, null /* position */));
+                // Adapter for the starting snapshot leash.
+                adapters.add(new TaskFragmentAnimationAdapter.SnapshotAdapter(
+                        animations[0], target));
+                // Adapter for the ending bounds changed leash.
+                adapters.add(new TaskFragmentAnimationAdapter.BoundsChangeAdapter(
+                        animations[1], target));
                 continue;
             }
 
+            // These are the other targets that don't have bounds change in the same transition.
             final Animation animation;
             if (target.hasAnimatingParent) {
                 // No-op if it will be covered by the changing parent window.
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 11a79b2..c0908a5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
@@ -176,18 +176,28 @@
         return new Animation[]{startSet, endSet};
     }
 
-    Animation loadOpenAnimation(boolean isEnter) {
-        // TODO(b/196173550) We need to customize the animation to handle two open window as one.
-        return mTransitionAnimation.loadDefaultAnimationAttr(isEnter
+    Animation loadOpenAnimation(@NonNull RemoteAnimationTarget target,
+            @NonNull Rect wholeAnimationBounds) {
+        final boolean isEnter = target.mode != MODE_CLOSING;
+        final Animation animation = mTransitionAnimation.loadDefaultAnimationAttr(isEnter
                 ? R.styleable.WindowAnimation_activityOpenEnterAnimation
                 : R.styleable.WindowAnimation_activityOpenExitAnimation);
+        animation.initialize(target.localBounds.width(), target.localBounds.height(),
+                wholeAnimationBounds.width(), wholeAnimationBounds.height());
+        animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        return animation;
     }
 
-    Animation loadCloseAnimation(boolean isEnter) {
-        // TODO(b/196173550) We need to customize the animation to handle two open window as one.
-        return mTransitionAnimation.loadDefaultAnimationAttr(isEnter
+    Animation loadCloseAnimation(@NonNull RemoteAnimationTarget target,
+            @NonNull Rect wholeAnimationBounds) {
+        final boolean isEnter = target.mode != MODE_CLOSING;
+        final Animation animation = mTransitionAnimation.loadDefaultAnimationAttr(isEnter
                 ? R.styleable.WindowAnimation_activityCloseEnterAnimation
                 : R.styleable.WindowAnimation_activityCloseExitAnimation);
+        animation.initialize(target.localBounds.width(), target.localBounds.height(),
+                wholeAnimationBounds.width(), wholeAnimationBounds.height());
+        animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
+        return animation;
     }
 
     private class SettingsObserver extends ContentObserver {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java
index e6f8388..224db02 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java
@@ -36,6 +36,6 @@
      * @return API version string in MAJOR.MINOR.PATCH-description format.
      */
     public static String getApiVersion() {
-        return "0.1.0-settings_sample";
+        return "1.0.0-reference";
     }
 }
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index 42e829e..4f36c9c 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/Android.bp b/libs/WindowManager/Shell/Android.bp
index 9aaef3b..3ba1a34 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -39,6 +39,14 @@
 }
 
 filegroup {
+    name: "wm_shell_util-sources",
+    srcs: [
+        "src/com/android/wm/shell/util/**/*.java",
+    ],
+    path: "src",
+}
+
+filegroup {
     name: "wm_shell-aidls",
     srcs: [
         "src/**/*.aidl",
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 d925a92..020ecb7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -42,6 +42,7 @@
 import android.view.SurfaceControl;
 import android.window.ITaskOrganizerController;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskAppearedInfo;
 import android.window.TaskOrganizer;
 
@@ -322,10 +323,9 @@
     }
 
     @Override
-    public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-            boolean playRevealAnimation) {
+    public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
         if (mStartingWindow != null) {
-            mStartingWindow.removeStartingWindow(taskId, leash, frame, playRevealAnimation);
+            mStartingWindow.removeStartingWindow(removalInfo);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index b48bda3..bef26bf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -558,6 +558,8 @@
         }
         Bubble bubbleToRemove = mBubbles.get(indexToRemove);
         bubbleToRemove.stopInflation();
+        overflowBubble(reason, bubbleToRemove);
+
         if (mBubbles.size() == 1) {
             if (hasOverflowBubbles() && (mPositioner.showingInTaskbar() || isExpanded())) {
                 // No more active bubbles but we have stuff in the overflow -- select that view
@@ -581,8 +583,6 @@
             mStateChange.orderChanged |= repackAll();
         }
 
-        overflowBubble(reason, bubbleToRemove);
-
         // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null.
         if (Objects.equals(mSelectedBubble, bubbleToRemove)) {
             // Move selection to the new bubble at the same position.
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 97c89d0..33ce383 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
@@ -21,7 +21,6 @@
 import android.annotation.NonNull;
 import android.content.Context;
 import android.content.res.Configuration;
-import android.graphics.Point;
 import android.graphics.Region;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -335,9 +334,6 @@
                 boolean alwaysConsumeSystemBars, int displayId) {}
 
         @Override
-        public void locationInParentDisplayChanged(Point offset) {}
-
-        @Override
         public void insetsChanged(InsetsState insetsState, boolean willMove, boolean willResize) {}
 
         @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java
index 08ab85c..fc1b704 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java
@@ -16,9 +16,6 @@
 
 package com.android.wm.shell.fullscreen;
 
-import static android.graphics.Color.blue;
-import static android.graphics.Color.green;
-import static android.graphics.Color.red;
 import static android.util.MathUtils.lerp;
 import static android.view.Display.DEFAULT_DISPLAY;
 
@@ -36,12 +33,11 @@
 import android.view.SurfaceControl;
 
 import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.wm.shell.R;
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
+import com.android.wm.shell.unfold.UnfoldBackgroundController;
 
 import java.util.concurrent.Executor;
 
@@ -59,21 +55,17 @@
     private static final float VERTICAL_START_MARGIN = 0.03f;
     private static final float END_SCALE = 1f;
     private static final float START_SCALE = END_SCALE - VERTICAL_START_MARGIN * 2;
-    private static final int BACKGROUND_LAYER_Z_INDEX = -1;
 
-    private final Context mContext;
     private final Executor mExecutor;
     private final ShellUnfoldProgressProvider mProgressProvider;
-    private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
     private final DisplayInsetsController mDisplayInsetsController;
 
     private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
+    private final UnfoldBackgroundController mBackgroundController;
 
-    private SurfaceControl mBackgroundLayer;
     private InsetsSource mTaskbarInsetsSource;
 
     private final float mWindowCornerRadiusPx;
-    private final float[] mBackgroundColor;
     private final float mExpandedTaskBarHeight;
 
     private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
@@ -81,19 +73,17 @@
     public FullscreenUnfoldController(
             @NonNull Context context,
             @NonNull Executor executor,
+            @NonNull UnfoldBackgroundController backgroundController,
             @NonNull ShellUnfoldProgressProvider progressProvider,
-            @NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
             @NonNull DisplayInsetsController displayInsetsController
     ) {
-        mContext = context;
         mExecutor = executor;
-        mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
         mProgressProvider = progressProvider;
         mDisplayInsetsController = displayInsetsController;
         mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
         mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.taskbar_frame_height);
-        mBackgroundColor = getBackgroundColor();
+        mBackgroundController = backgroundController;
     }
 
     /**
@@ -108,7 +98,7 @@
     public void onStateChangeProgress(float progress) {
         if (mAnimationContextByTaskId.size() == 0) return;
 
-        ensureBackground();
+        mBackgroundController.ensureBackground(mTransaction);
 
         for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
             final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
@@ -135,7 +125,7 @@
             resetSurface(context);
         }
 
-        removeBackground();
+        mBackgroundController.removeBackground(mTransaction);
         mTransaction.apply();
     }
 
@@ -178,7 +168,7 @@
         }
 
         if (mAnimationContextByTaskId.size() == 0) {
-            removeBackground();
+            mBackgroundController.removeBackground(mTransaction);
         }
 
         mTransaction.apply();
@@ -194,39 +184,6 @@
                         (float) context.mTaskInfo.positionInParent.y);
     }
 
-    private void ensureBackground() {
-        if (mBackgroundLayer != null) return;
-
-        SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder()
-                .setName("app-unfold-background")
-                .setCallsite("AppUnfoldTransitionController")
-                .setColorLayer();
-        mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder);
-        mBackgroundLayer = colorLayerBuilder.build();
-
-        mTransaction
-                .setColor(mBackgroundLayer, mBackgroundColor)
-                .show(mBackgroundLayer)
-                .setLayer(mBackgroundLayer, BACKGROUND_LAYER_Z_INDEX);
-    }
-
-    private void removeBackground() {
-        if (mBackgroundLayer == null) return;
-        if (mBackgroundLayer.isValid()) {
-            mTransaction.remove(mBackgroundLayer);
-        }
-        mBackgroundLayer = null;
-    }
-
-    private float[] getBackgroundColor() {
-        int colorInt = mContext.getResources().getColor(R.color.unfold_transition_background);
-        return new float[]{
-                (float) red(colorInt) / 255.0F,
-                (float) green(colorInt) / 255.0F,
-                (float) blue(colorInt) / 255.0F
-        };
-    }
-
     private class AnimationContext {
         final SurfaceControl mLeash;
         final Rect mStartCropRect = new Rect();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 8e5c5c5..2e918f6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -295,7 +295,7 @@
             ShellExecutor mainExecutor
     ) {
         // Ensure that we are the primary user's SystemUI.
-        final int processUser = UserManager.get(context).getUserHandle();
+        final int processUser = UserManager.get(context).getProcessUserId();
         if (processUser != UserHandle.USER_SYSTEM) {
             throw new IllegalStateException("Non-primary Pip component not currently supported.");
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
index d0998eb..a47a152 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 
+import android.annotation.Nullable;
 import android.graphics.Rect;
 import android.view.SurfaceSession;
 import android.window.WindowContainerToken;
@@ -38,33 +39,34 @@
 
     MainStage(ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession) {
-        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession);
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+                stageTaskUnfoldController);
     }
 
     boolean isActive() {
         return mIsActive;
     }
 
-    void activate(Rect rootBounds, WindowContainerTransaction wct) {
+    void activate(Rect rootBounds, WindowContainerTransaction wct, boolean includingTopTask) {
         if (mIsActive) return;
 
         final WindowContainerToken rootToken = mRootTaskInfo.token;
         wct.setBounds(rootToken, rootBounds)
                 .setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW)
-                .setLaunchRoot(
-                        rootToken,
-                        CONTROLLED_WINDOWING_MODES,
-                        CONTROLLED_ACTIVITY_TYPES)
-                .reparentTasks(
-                        null /* currentParent */,
-                        rootToken,
-                        CONTROLLED_WINDOWING_MODES,
-                        CONTROLLED_ACTIVITY_TYPES,
-                        true /* onTop */)
                 // Moving the root task to top after the child tasks were re-parented , or the root
                 // task cannot be visible and focused.
                 .reorder(rootToken, true /* onTop */);
+        if (includingTopTask) {
+            wct.reparentTasks(
+                    null /* currentParent */,
+                    rootToken,
+                    CONTROLLED_WINDOWING_MODES,
+                    CONTROLLED_ACTIVITY_TYPES,
+                    true /* onTop */,
+                    true /* reparentTopOnly */);
+        }
 
         mIsActive = true;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
index 0e7ccd3..dc8fb9f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
@@ -46,8 +46,10 @@
 
     SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession) {
-        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession);
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+                stageTaskUnfoldController);
         mContext = context;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index ac68b3b..0d52719 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -68,8 +68,11 @@
 
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.Optional;
 import java.util.concurrent.Executor;
 
+import javax.inject.Provider;
+
 /**
  * Class manages split-screen multitasking mode and implements the main interface
  * {@link SplitScreen}.
@@ -91,6 +94,7 @@
     private final Transitions mTransitions;
     private final TransactionPool mTransactionPool;
     private final SplitscreenEventLogger mLogger;
+    private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider;
 
     private StageCoordinator mStageCoordinator;
 
@@ -99,7 +103,8 @@
             RootTaskDisplayAreaOrganizer rootTDAOrganizer,
             ShellExecutor mainExecutor, DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController,
-            Transitions transitions, TransactionPool transactionPool) {
+            Transitions transitions, TransactionPool transactionPool,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
         mContext = context;
@@ -109,6 +114,7 @@
         mDisplayInsetsController = displayInsetsController;
         mTransitions = transitions;
         mTransactionPool = transactionPool;
+        mUnfoldControllerProvider = unfoldControllerProvider;
         mLogger = new SplitscreenEventLogger();
     }
 
@@ -131,7 +137,8 @@
             // TODO: Multi-display
             mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
                     mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController,
-                    mDisplayInsetsController, mTransitions, mTransactionPool, mLogger);
+                    mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
+                    mUnfoldControllerProvider);
         }
     }
 
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 d7a6cff..3e74ad3 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
@@ -95,6 +95,9 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Provider;
 
 /**
  * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
@@ -121,8 +124,10 @@
 
     private final MainStage mMainStage;
     private final StageListenerImpl mMainStageListener = new StageListenerImpl();
+    private final StageTaskUnfoldController mMainUnfoldController;
     private final SideStage mSideStage;
     private final StageListenerImpl mSideStageListener = new StageListenerImpl();
+    private final StageTaskUnfoldController mSideUnfoldController;
     @SplitPosition
     private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
 
@@ -179,26 +184,32 @@
             RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
             DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController, Transitions transitions,
-            TransactionPool transactionPool, SplitscreenEventLogger logger) {
+            TransactionPool transactionPool, SplitscreenEventLogger logger,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mContext = context;
         mDisplayId = displayId;
         mSyncQueue = syncQueue;
         mRootTDAOrganizer = rootTDAOrganizer;
         mTaskOrganizer = taskOrganizer;
         mLogger = logger;
+        mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
+        mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
+
         mMainStage = new MainStage(
                 mTaskOrganizer,
                 mDisplayId,
                 mMainStageListener,
                 mSyncQueue,
-                mSurfaceSession);
+                mSurfaceSession,
+                mMainUnfoldController);
         mSideStage = new SideStage(
                 mContext,
                 mTaskOrganizer,
                 mDisplayId,
                 mSideStageListener,
                 mSyncQueue,
-                mSurfaceSession);
+                mSurfaceSession,
+                mSideUnfoldController);
         mDisplayImeController = displayImeController;
         mDisplayInsetsController = displayInsetsController;
         mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSideStage);
@@ -218,7 +229,8 @@
             MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
             Transitions transitions, TransactionPool transactionPool,
-            SplitscreenEventLogger logger) {
+            SplitscreenEventLogger logger,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mContext = context;
         mDisplayId = displayId;
         mSyncQueue = syncQueue;
@@ -232,6 +244,8 @@
         mSplitLayout = splitLayout;
         mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
                 mOnTransitionAnimationComplete);
+        mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
+        mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
         mLogger = logger;
         transitions.addHandler(this);
     }
@@ -249,7 +263,7 @@
             @SplitPosition int sideStagePosition) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         setSideStagePosition(sideStagePosition, wct);
-        mMainStage.activate(getMainStageBounds(), wct);
+        mMainStage.activate(getMainStageBounds(), wct, true /* reparent */);
         mSideStage.addTask(task, getSideStageBounds(), wct);
         mSyncQueue.queue(wct);
         mSyncQueue.runInSync(t -> updateSurfaceBounds(null /* layout */, t));
@@ -285,7 +299,7 @@
 
         // Build a request WCT that will launch both apps such that task 0 is on the main stage
         // while task 1 is on the side stage.
-        mMainStage.activate(getMainStageBounds(), wct);
+        mMainStage.activate(getMainStageBounds(), wct, false /* reparent */);
         mSideStage.setBounds(getSideStageBounds(), wct);
 
         // Make sure the launch options will put tasks in the corresponding split roots
@@ -354,7 +368,7 @@
 
         // Build a request WCT that will launch both apps such that task 0 is on the main stage
         // while task 1 is on the side stage.
-        mMainStage.activate(getMainStageBounds(), wct);
+        mMainStage.activate(getMainStageBounds(), wct, false /* reparent */);
         mSideStage.setBounds(getSideStageBounds(), wct);
 
         // Make sure the launch options will put tasks in the corresponding split roots
@@ -460,6 +474,7 @@
                 onLayoutChanged(mSplitLayout);
             } else {
                 updateWindowBounds(mSplitLayout, wct);
+                updateUnfoldBounds();
             }
         }
     }
@@ -515,6 +530,9 @@
         mSideStage.removeAllTasks(wct, childrenToTop == mSideStage);
         mMainStage.deactivate(wct, childrenToTop == mMainStage);
         mTaskOrganizer.applyTransaction(wct);
+        mSyncQueue.runInSync(t -> t
+                .setWindowCrop(mMainStage.mRootLeash, null)
+                .setWindowCrop(mSideStage.mRootLeash, null));
         // Hide divider and reset its position.
         setDividerVisibility(false);
         mSplitLayout.resetDividerPosition();
@@ -603,6 +621,11 @@
             final SplitScreen.SplitScreenListener l = mListeners.get(i);
             l.onSplitVisibilityChanged(mDividerVisible);
         }
+
+        if (mMainUnfoldController != null && mSideUnfoldController != null) {
+            mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible);
+            mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible);
+        }
     }
 
     private void onStageRootTaskAppeared(StageListenerImpl stageListener) {
@@ -641,6 +664,7 @@
         mDividerVisible = visible;
         if (visible) {
             mSplitLayout.init();
+            updateUnfoldBounds();
         } else {
             mSplitLayout.release();
         }
@@ -732,7 +756,7 @@
         } else if (isSideStage) {
             final WindowContainerTransaction wct = new WindowContainerTransaction();
             // Make sure the main stage is active.
-            mMainStage.activate(getMainStageBounds(), wct);
+            mMainStage.activate(getMainStageBounds(), wct, true /* reparent */);
             mSideStage.setBounds(getSideStageBounds(), wct);
             mTaskOrganizer.applyTransaction(wct);
         }
@@ -784,12 +808,20 @@
     public void onLayoutChanged(SplitLayout layout) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         updateWindowBounds(layout, wct);
+        updateUnfoldBounds();
         mSyncQueue.queue(wct);
         mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
         mSideStage.setOutlineVisibility(true);
         mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
     }
 
+    private void updateUnfoldBounds() {
+        if (mMainUnfoldController != null && mSideUnfoldController != null) {
+            mMainUnfoldController.onLayoutChanged(getMainStageBounds());
+            mSideUnfoldController.onLayoutChanged(getSideStageBounds());
+        }
+    }
+
     /**
      * Populates `wct` with operations that match the split windows to the current layout.
      * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied
@@ -846,6 +878,11 @@
                     mDisplayAreaInfo.configuration, this, mParentContainerCallbacks,
                     mDisplayImeController, mTaskOrganizer);
             mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
+
+            if (mMainUnfoldController != null && mSideUnfoldController != null) {
+                mMainUnfoldController.init();
+                mSideUnfoldController.init();
+            }
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 4140332..84d570f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -24,6 +24,7 @@
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
 import android.annotation.CallSuper;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -80,12 +81,16 @@
     protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>();
     private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>();
 
+    private final StageTaskUnfoldController mStageTaskUnfoldController;
+
     StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession) {
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
         mCallbacks = callbacks;
         mSyncQueue = syncQueue;
         mSurfaceSession = surfaceSession;
+        mStageTaskUnfoldController = stageTaskUnfoldController;
         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
     }
 
@@ -158,6 +163,10 @@
             throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
                     + "\n mRootTaskInfo: " + mRootTaskInfo);
         }
+
+        if (mStageTaskUnfoldController != null) {
+            mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash);
+        }
     }
 
     @Override
@@ -210,6 +219,10 @@
             throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
                     + "\n mRootTaskInfo: " + mRootTaskInfo);
         }
+
+        if (mStageTaskUnfoldController != null) {
+            mStageTaskUnfoldController.onTaskVanished(taskInfo);
+        }
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java
new file mode 100644
index 0000000..e904f6a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.splitscreen;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.animation.RectEvaluator;
+import android.animation.TypeEvaluator;
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
+import com.android.wm.shell.unfold.UnfoldBackgroundController;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Controls transformations of the split screen task surfaces in response
+ * to the unfolding/folding action on foldable devices
+ */
+public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener {
+
+    private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
+    private static final float CROPPING_START_MARGIN_FRACTION = 0.05f;
+
+    private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
+    private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
+    private final DisplayInsetsController mDisplayInsetsController;
+    private final UnfoldBackgroundController mBackgroundController;
+    private final Executor mExecutor;
+    private final int mExpandedTaskBarHeight;
+    private final float mWindowCornerRadiusPx;
+    private final Rect mStageBounds = new Rect();
+    private final TransactionPool mTransactionPool;
+
+    private InsetsSource mTaskbarInsetsSource;
+    private boolean mBothStagesVisible;
+
+    public StageTaskUnfoldController(@NonNull Context context,
+            @NonNull TransactionPool transactionPool,
+            @NonNull ShellUnfoldProgressProvider unfoldProgressProvider,
+            @NonNull DisplayInsetsController displayInsetsController,
+            @NonNull UnfoldBackgroundController backgroundController,
+            @NonNull Executor executor) {
+        mUnfoldProgressProvider = unfoldProgressProvider;
+        mTransactionPool = transactionPool;
+        mExecutor = executor;
+        mBackgroundController = backgroundController;
+        mDisplayInsetsController = displayInsetsController;
+        mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
+        mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.taskbar_frame_height);
+    }
+
+    /**
+     * Initializes the controller, starts listening for the external events
+     */
+    public void init() {
+        mUnfoldProgressProvider.addListener(mExecutor, this);
+        mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
+    }
+
+    @Override
+    public void insetsChanged(InsetsState insetsState) {
+        mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            context.update();
+        }
+    }
+
+    /**
+     * Called when split screen task appeared
+     * @param taskInfo info for the appeared task
+     * @param leash surface leash for the appeared task
+     */
+    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+        AnimationContext context = new AnimationContext(leash);
+        mAnimationContextByTaskId.put(taskInfo.taskId, context);
+    }
+
+    /**
+     * Called when a split screen task vanished
+     * @param taskInfo info for the vanished task
+     */
+    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+        AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId);
+        if (context != null) {
+            final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+            resetSurface(transaction, context);
+            transaction.apply();
+            mTransactionPool.release(transaction);
+        }
+        mAnimationContextByTaskId.remove(taskInfo.taskId);
+    }
+
+    @Override
+    public void onStateChangeProgress(float progress) {
+        if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return;
+
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+        mBackgroundController.ensureBackground(transaction);
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+
+            context.mCurrentCropRect.set(RECT_EVALUATOR
+                    .evaluate(progress, context.mStartCropRect, context.mEndCropRect));
+
+            transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
+                    .setCornerRadius(context.mLeash, mWindowCornerRadiusPx);
+        }
+
+        transaction.apply();
+
+        mTransactionPool.release(transaction);
+    }
+
+    @Override
+    public void onStateChangeFinished() {
+        resetTransformations();
+    }
+
+    /**
+     * Called when split screen visibility changes
+     * @param bothStagesVisible true if both stages of the split screen are visible
+     */
+    public void onSplitVisibilityChanged(boolean bothStagesVisible) {
+        mBothStagesVisible = bothStagesVisible;
+        if (!bothStagesVisible) {
+            resetTransformations();
+        }
+    }
+
+    /**
+     * Called when split screen stage bounds changed
+     * @param bounds new bounds for this stage
+     */
+    public void onLayoutChanged(Rect bounds) {
+        mStageBounds.set(bounds);
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            context.update();
+        }
+    }
+
+    private void resetTransformations() {
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            resetSurface(transaction, context);
+        }
+        mBackgroundController.removeBackground(transaction);
+        transaction.apply();
+
+        mTransactionPool.release(transaction);
+    }
+
+    private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) {
+        transaction
+                .setWindowCrop(context.mLeash, null)
+                .setCornerRadius(context.mLeash, 0.0F);
+    }
+
+    private class AnimationContext {
+        final SurfaceControl mLeash;
+        final Rect mStartCropRect = new Rect();
+        final Rect mEndCropRect = new Rect();
+        final Rect mCurrentCropRect = new Rect();
+
+        private AnimationContext(SurfaceControl leash) {
+            this.mLeash = leash;
+            update();
+        }
+
+        private void update() {
+            mStartCropRect.set(mStageBounds);
+
+            if (mTaskbarInsetsSource != null) {
+                // Only insets the cropping window with taskbar when taskbar is expanded
+                if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
+                    mStartCropRect.inset(mTaskbarInsetsSource
+                            .calculateVisibleInsets(mStartCropRect));
+                }
+            }
+
+            // Offset to surface coordinates as layout bounds are in screen coordinates
+            mStartCropRect.offsetTo(0, 0);
+
+            mEndCropRect.set(mStartCropRect);
+
+            int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height());
+            int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION);
+            mStartCropRect.inset(margin, margin, margin, margin);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl
new file mode 100644
index 0000000..45f6d3c
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.window.RemoteTransition;
+
+import com.android.wm.shell.stagesplit.ISplitScreenListener;
+
+/**
+ * Interface that is exposed to remote callers to manipulate the splitscreen feature.
+ */
+interface ISplitScreen {
+
+    /**
+     * Registers a split screen listener.
+     */
+    oneway void registerSplitScreenListener(in ISplitScreenListener listener) = 1;
+
+    /**
+     * Unregisters a split screen listener.
+     */
+    oneway void unregisterSplitScreenListener(in ISplitScreenListener listener) = 2;
+
+    /**
+     * Hides the side-stage if it is currently visible.
+     */
+    oneway void setSideStageVisibility(boolean visible) = 3;
+
+    /**
+     * Removes a task from the side stage.
+     */
+    oneway void removeFromSideStage(int taskId) = 4;
+
+    /**
+     * Removes the split-screen stages and leaving indicated task to top. Passing INVALID_TASK_ID
+     * to indicate leaving no top task after leaving split-screen.
+     */
+    oneway void exitSplitScreen(int toTopTaskId) = 5;
+
+    /**
+     * @param exitSplitScreenOnHide if to exit split-screen if both stages are not visible.
+     */
+    oneway void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) = 6;
+
+    /**
+     * Starts a task in a stage.
+     */
+    oneway void startTask(int taskId, int stage, int position, in Bundle options) = 7;
+
+    /**
+     * Starts a shortcut in a stage.
+     */
+    oneway void startShortcut(String packageName, String shortcutId, int stage, int position,
+            in Bundle options, in UserHandle user) = 8;
+
+    /**
+     * Starts an activity in a stage.
+     */
+    oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int stage,
+            int position, in Bundle options) = 9;
+
+    /**
+     * Starts tasks simultaneously in one transition.
+     */
+    oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId,
+            in Bundle sideOptions, int sidePosition, in RemoteTransition remoteTransition) = 10;
+
+    /**
+     * Version of startTasks using legacy transition system.
+     */
+     oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions,
+                            int sideTaskId, in Bundle sideOptions, int sidePosition,
+                            in RemoteAnimationAdapter adapter) = 11;
+
+    /**
+     * Blocking call that notifies and gets additional split-screen targets when entering
+     * recents (for example: the dividerBar).
+     * @param cancel is true if leaving recents back to split (eg. the gesture was cancelled).
+     * @param appTargets apps that will be re-parented to display area
+     */
+    RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel,
+                                                   in RemoteAnimationTarget[] appTargets) = 12;
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl
new file mode 100644
index 0000000..46e4299
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+/**
+ * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks.
+ */
+oneway interface ISplitScreenListener {
+
+    /**
+     * Called when the stage position changes.
+     */
+    void onStagePositionChanged(int stage, int position);
+
+    /**
+     * Called when a task changes stages.
+     */
+    void onTaskStageChanged(int taskId, int stage, boolean visible);
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java
new file mode 100644
index 0000000..83855be
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java
@@ -0,0 +1,104 @@
+/*
+ * 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.wm.shell.stagesplit;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.view.SurfaceSession;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+/**
+ * Main stage for split-screen mode. When split-screen is active all standard activity types launch
+ * on the main stage, except for task that are explicitly pinned to the {@link SideStage}.
+ * @see StageCoordinator
+ */
+class MainStage extends StageTaskListener {
+    private static final String TAG = MainStage.class.getSimpleName();
+
+    private boolean mIsActive = false;
+
+    MainStage(ShellTaskOrganizer taskOrganizer, int displayId,
+            StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+                stageTaskUnfoldController);
+    }
+
+    boolean isActive() {
+        return mIsActive;
+    }
+
+    void activate(Rect rootBounds, WindowContainerTransaction wct) {
+        if (mIsActive) return;
+
+        final WindowContainerToken rootToken = mRootTaskInfo.token;
+        wct.setBounds(rootToken, rootBounds)
+                .setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW)
+                .setLaunchRoot(
+                        rootToken,
+                        CONTROLLED_WINDOWING_MODES,
+                        CONTROLLED_ACTIVITY_TYPES)
+                .reparentTasks(
+                        null /* currentParent */,
+                        rootToken,
+                        CONTROLLED_WINDOWING_MODES,
+                        CONTROLLED_ACTIVITY_TYPES,
+                        true /* onTop */)
+                // Moving the root task to top after the child tasks were re-parented , or the root
+                // task cannot be visible and focused.
+                .reorder(rootToken, true /* onTop */);
+
+        mIsActive = true;
+    }
+
+    void deactivate(WindowContainerTransaction wct) {
+        deactivate(wct, false /* toTop */);
+    }
+
+    void deactivate(WindowContainerTransaction wct, boolean toTop) {
+        if (!mIsActive) return;
+        mIsActive = false;
+
+        if (mRootTaskInfo == null) return;
+        final WindowContainerToken rootToken = mRootTaskInfo.token;
+        wct.setLaunchRoot(
+                        rootToken,
+                        null,
+                        null)
+                .reparentTasks(
+                        rootToken,
+                        null /* newParent */,
+                        CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
+                        CONTROLLED_ACTIVITY_TYPES,
+                        toTop)
+                // We want this re-order to the bottom regardless since we are re-parenting
+                // all its tasks.
+                .reorder(rootToken, false /* onTop */);
+    }
+
+    void updateConfiguration(int windowingMode, Rect bounds, WindowContainerTransaction wct) {
+        wct.setBounds(mRootTaskInfo.token, bounds)
+                .setWindowingMode(mRootTaskInfo.token, windowingMode);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java
new file mode 100644
index 0000000..8fbad52
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.view.IWindow;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.LayoutInflater;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+import android.widget.FrameLayout;
+
+import com.android.wm.shell.R;
+
+/**
+ * Handles drawing outline of the bounds of provided root surface. The outline will be drown with
+ * the consideration of display insets like status bar, navigation bar and display cutout.
+ */
+class OutlineManager extends WindowlessWindowManager {
+    private static final String WINDOW_NAME = "SplitOutlineLayer";
+    private final Context mContext;
+    private final Rect mRootBounds = new Rect();
+    private final Rect mTempRect = new Rect();
+    private final Rect mLastOutlineBounds = new Rect();
+    private final InsetsState mInsetsState = new InsetsState();
+    private final int mExpandedTaskBarHeight;
+    private OutlineView mOutlineView;
+    private SurfaceControlViewHost mViewHost;
+    private SurfaceControl mHostLeash;
+    private SurfaceControl mLeash;
+
+    OutlineManager(Context context, Configuration configuration) {
+        super(configuration, null /* rootSurface */, null /* hostInputToken */);
+        mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY,
+                null /* options */);
+        mExpandedTaskBarHeight = mContext.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.taskbar_frame_height);
+    }
+
+    @Override
+    protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
+        b.setParent(mHostLeash);
+    }
+
+    void inflate(SurfaceControl rootLeash, Rect rootBounds) {
+        if (mLeash != null || mViewHost != null) return;
+
+        mHostLeash = rootLeash;
+        mRootBounds.set(rootBounds);
+        mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this);
+
+        final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(mContext)
+                .inflate(R.layout.split_outline, null);
+        mOutlineView = rootLayout.findViewById(R.id.split_outline);
+
+        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY,
+                FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
+        lp.width = mRootBounds.width();
+        lp.height = mRootBounds.height();
+        lp.token = new Binder();
+        lp.setTitle(WINDOW_NAME);
+        lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
+        // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports
+        //  TRUSTED_OVERLAY for windowless window without input channel.
+        mViewHost.setView(rootLayout, lp);
+        mLeash = getSurfaceControl(mViewHost.getWindowToken());
+
+        drawOutline();
+    }
+
+    void release() {
+        if (mViewHost != null) {
+            mViewHost.release();
+            mViewHost = null;
+        }
+        mRootBounds.setEmpty();
+        mLastOutlineBounds.setEmpty();
+        mOutlineView = null;
+        mHostLeash = null;
+        mLeash = null;
+    }
+
+    @Nullable
+    SurfaceControl getOutlineLeash() {
+        return mLeash;
+    }
+
+    void setVisibility(boolean visible) {
+        if (mOutlineView != null) {
+            mOutlineView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+        }
+    }
+
+    void setRootBounds(Rect rootBounds) {
+        if (mViewHost == null || mViewHost.getView() == null) {
+            return;
+        }
+
+        if (!mRootBounds.equals(rootBounds)) {
+            WindowManager.LayoutParams lp =
+                    (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams();
+            lp.width = rootBounds.width();
+            lp.height = rootBounds.height();
+            mViewHost.relayout(lp);
+            mRootBounds.set(rootBounds);
+            drawOutline();
+        }
+    }
+
+    void onInsetsChanged(InsetsState insetsState) {
+        if (!mInsetsState.equals(insetsState)) {
+            mInsetsState.set(insetsState);
+            drawOutline();
+        }
+    }
+
+    private void computeOutlineBounds(Rect rootBounds, InsetsState insetsState, Rect outBounds) {
+        outBounds.set(rootBounds);
+        final InsetsSource taskBarInsetsSource =
+                insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        // Only insets the divider bar with task bar when it's expanded so that the rounded corners
+        // will be drawn against task bar.
+        if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
+            outBounds.inset(taskBarInsetsSource.calculateVisibleInsets(outBounds));
+        }
+
+        // Offset the coordinate from screen based to surface based.
+        outBounds.offset(-rootBounds.left, -rootBounds.top);
+    }
+
+    void drawOutline() {
+        if (mOutlineView == null) {
+            return;
+        }
+
+        computeOutlineBounds(mRootBounds, mInsetsState, mTempRect);
+        if (mTempRect.equals(mLastOutlineBounds)) {
+            return;
+        }
+
+        ViewGroup.MarginLayoutParams lp =
+                (ViewGroup.MarginLayoutParams) mOutlineView.getLayoutParams();
+        lp.leftMargin = mTempRect.left;
+        lp.topMargin = mTempRect.top;
+        lp.width = mTempRect.width();
+        lp.height = mTempRect.height();
+        mOutlineView.setLayoutParams(lp);
+        mLastOutlineBounds.set(mTempRect);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java
new file mode 100644
index 0000000..92b1381
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
+import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
+import static android.view.RoundedCorner.POSITION_TOP_LEFT;
+import static android.view.RoundedCorner.POSITION_TOP_RIGHT;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.view.RoundedCorner;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.R;
+
+/** View for drawing split outline. */
+public class OutlineView extends View {
+    private final Paint mPaint = new Paint();
+    private final Path mPath = new Path();
+    private final float[] mRadii = new float[8];
+
+    public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeWidth(
+                getResources().getDimension(R.dimen.accessibility_focus_highlight_stroke_width));
+        mPaint.setColor(getResources().getColor(R.color.system_accent1_100, null));
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        // TODO(b/200850654): match the screen corners with the actual display decor.
+        mRadii[0] = mRadii[1] = getCornerRadius(POSITION_TOP_LEFT);
+        mRadii[2] = mRadii[3] = getCornerRadius(POSITION_TOP_RIGHT);
+        mRadii[4] = mRadii[5] = getCornerRadius(POSITION_BOTTOM_RIGHT);
+        mRadii[6] = mRadii[7] = getCornerRadius(POSITION_BOTTOM_LEFT);
+    }
+
+    private int getCornerRadius(@RoundedCorner.Position int position) {
+        final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(position);
+        return roundedCorner == null ? 0 : roundedCorner.getRadius();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (changed) {
+            mPath.reset();
+            mPath.addRoundRect(0, 0, getWidth(), getHeight(), mRadii, Path.Direction.CW);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        canvas.drawPath(mPath, mPaint);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java
new file mode 100644
index 0000000..55c4f3a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java
@@ -0,0 +1,144 @@
+/*
+ * 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.wm.shell.stagesplit;
+
+import android.annotation.CallSuper;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.InsetsSourceControl;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+/**
+ * Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up
+ * here. All other task are launch in the {@link MainStage}.
+ *
+ * @see StageCoordinator
+ */
+class SideStage extends StageTaskListener implements
+        DisplayInsetsController.OnInsetsChangedListener {
+    private static final String TAG = SideStage.class.getSimpleName();
+    private final Context mContext;
+    private OutlineManager mOutlineManager;
+
+    SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
+            StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+        super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+                stageTaskUnfoldController);
+        mContext = context;
+    }
+
+    void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds,
+            WindowContainerTransaction wct) {
+        final WindowContainerToken rootToken = mRootTaskInfo.token;
+        wct.setBounds(rootToken, rootBounds)
+                .reparent(task.token, rootToken, true /* onTop*/)
+                // Moving the root task to top after the child tasks were reparented , or the root
+                // task cannot be visible and focused.
+                .reorder(rootToken, true /* onTop */);
+    }
+
+    boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) {
+        // No matter if the root task is empty or not, moving the root to bottom because it no
+        // longer preserves visible child task.
+        wct.reorder(mRootTaskInfo.token, false /* onTop */);
+        if (mChildrenTaskInfo.size() == 0) return false;
+        wct.reparentTasks(
+                mRootTaskInfo.token,
+                null /* newParent */,
+                CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE,
+                CONTROLLED_ACTIVITY_TYPES,
+                toTop);
+        return true;
+    }
+
+    boolean removeTask(int taskId, WindowContainerToken newParent, WindowContainerTransaction wct) {
+        final ActivityManager.RunningTaskInfo task = mChildrenTaskInfo.get(taskId);
+        if (task == null) return false;
+        wct.reparent(task.token, newParent, false /* onTop */);
+        return true;
+    }
+
+    @Nullable
+    public SurfaceControl getOutlineLeash() {
+        return mOutlineManager.getOutlineLeash();
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+        super.onTaskAppeared(taskInfo, leash);
+        if (isRootTask(taskInfo)) {
+            mOutlineManager = new OutlineManager(mContext, taskInfo.configuration);
+            enableOutline(true);
+        }
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        super.onTaskInfoChanged(taskInfo);
+        if (isRootTask(taskInfo)) {
+            mOutlineManager.setRootBounds(taskInfo.configuration.windowConfiguration.getBounds());
+        }
+    }
+
+    private boolean isRootTask(ActivityManager.RunningTaskInfo taskInfo) {
+        return mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId;
+    }
+
+    void enableOutline(boolean enable) {
+        if (mOutlineManager == null) {
+            return;
+        }
+
+        if (enable) {
+            if (mRootTaskInfo != null) {
+                mOutlineManager.inflate(mRootLeash,
+                        mRootTaskInfo.configuration.windowConfiguration.getBounds());
+            }
+        } else {
+            mOutlineManager.release();
+        }
+    }
+
+    void setOutlineVisibility(boolean visible) {
+        mOutlineManager.setVisibility(visible);
+    }
+
+    @Override
+    public void insetsChanged(InsetsState insetsState) {
+        mOutlineManager.onInsetsChanged(insetsState);
+    }
+
+    @Override
+    public void insetsControlChanged(InsetsState insetsState,
+            InsetsSourceControl[] activeControls) {
+        insetsChanged(insetsState);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java
new file mode 100644
index 0000000..aec81a1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java
@@ -0,0 +1,99 @@
+/*
+ * 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.wm.shell.stagesplit;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+
+import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Interface to engage split-screen feature.
+ * TODO: Figure out which of these are actually needed outside of the Shell
+ */
+@ExternalThread
+public interface SplitScreen {
+    /**
+     * Stage type isn't specified normally meaning to use what ever the default is.
+     * E.g. exit split-screen and launch the app in fullscreen.
+     */
+    int STAGE_TYPE_UNDEFINED = -1;
+    /**
+     * The main stage type.
+     * @see MainStage
+     */
+    int STAGE_TYPE_MAIN = 0;
+
+    /**
+     * The side stage type.
+     * @see SideStage
+     */
+    int STAGE_TYPE_SIDE = 1;
+
+    @IntDef(prefix = { "STAGE_TYPE_" }, value = {
+            STAGE_TYPE_UNDEFINED,
+            STAGE_TYPE_MAIN,
+            STAGE_TYPE_SIDE
+    })
+    @interface StageType {}
+
+    /** Callback interface for listening to changes in a split-screen stage. */
+    interface SplitScreenListener {
+        default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {}
+        default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {}
+        default void onSplitVisibilityChanged(boolean visible) {}
+    }
+
+    /** Registers listener that gets split screen callback. */
+    void registerSplitScreenListener(@NonNull SplitScreenListener listener,
+            @NonNull Executor executor);
+
+    /** Unregisters listener that gets split screen callback. */
+    void unregisterSplitScreenListener(@NonNull SplitScreenListener listener);
+
+    /**
+     * Returns a binder that can be passed to an external process to manipulate SplitScreen.
+     */
+    default ISplitScreen createExternalInterface() {
+        return null;
+    }
+
+    /**
+     * Called when the keyguard occluded state changes.
+     * @param occluded Indicates if the keyguard is now occluded.
+     */
+    void onKeyguardOccludedChanged(boolean occluded);
+
+    /**
+     * Called when the visibility of the keyguard changes.
+     * @param showing Indicates if the keyguard is now visible.
+     */
+    void onKeyguardVisibilityChanged(boolean showing);
+
+    /** Get a string representation of a stage type */
+    static String stageTypeToString(@StageType int stage) {
+        switch (stage) {
+            case STAGE_TYPE_UNDEFINED: return "UNDEFINED";
+            case STAGE_TYPE_MAIN: return "MAIN";
+            case STAGE_TYPE_SIDE: return "SIDE";
+            default: return "UNKNOWN(" + stage + ")";
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java
new file mode 100644
index 0000000..94db9cd9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java
@@ -0,0 +1,595 @@
+/*
+ * 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.wm.shell.stagesplit;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
+
+import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
+
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.view.WindowManager;
+import android.window.RemoteTransition;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.BinderThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.RemoteCallable;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
+import com.android.wm.shell.draganddrop.DragAndDropPolicy;
+import com.android.wm.shell.transition.LegacyTransitions;
+import com.android.wm.shell.transition.Transitions;
+
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+import javax.inject.Provider;
+
+/**
+ * Class manages split-screen multitasking mode and implements the main interface
+ * {@link SplitScreen}.
+ * @see StageCoordinator
+ */
+// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen.
+public class SplitScreenController implements DragAndDropPolicy.Starter,
+        RemoteCallable<SplitScreenController> {
+    private static final String TAG = SplitScreenController.class.getSimpleName();
+
+    private final ShellTaskOrganizer mTaskOrganizer;
+    private final SyncTransactionQueue mSyncQueue;
+    private final Context mContext;
+    private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
+    private final ShellExecutor mMainExecutor;
+    private final SplitScreenImpl mImpl = new SplitScreenImpl();
+    private final DisplayImeController mDisplayImeController;
+    private final DisplayInsetsController mDisplayInsetsController;
+    private final Transitions mTransitions;
+    private final TransactionPool mTransactionPool;
+    private final SplitscreenEventLogger mLogger;
+    private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider;
+
+    private StageCoordinator mStageCoordinator;
+
+    public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
+            SyncTransactionQueue syncQueue, Context context,
+            RootTaskDisplayAreaOrganizer rootTDAOrganizer,
+            ShellExecutor mainExecutor, DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController,
+            Transitions transitions, TransactionPool transactionPool,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+        mTaskOrganizer = shellTaskOrganizer;
+        mSyncQueue = syncQueue;
+        mContext = context;
+        mRootTDAOrganizer = rootTDAOrganizer;
+        mMainExecutor = mainExecutor;
+        mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
+        mTransitions = transitions;
+        mTransactionPool = transactionPool;
+        mUnfoldControllerProvider = unfoldControllerProvider;
+        mLogger = new SplitscreenEventLogger();
+    }
+
+    public SplitScreen asSplitScreen() {
+        return mImpl;
+    }
+
+    @Override
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public ShellExecutor getRemoteCallExecutor() {
+        return mMainExecutor;
+    }
+
+    public void onOrganizerRegistered() {
+        if (mStageCoordinator == null) {
+            // TODO: Multi-display
+            mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
+                    mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController,
+                    mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
+                    mUnfoldControllerProvider);
+        }
+    }
+
+    public boolean isSplitScreenVisible() {
+        return mStageCoordinator.isSplitScreenVisible();
+    }
+
+    public boolean moveToSideStage(int taskId, @SplitPosition int sideStagePosition) {
+        final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId);
+        if (task == null) {
+            throw new IllegalArgumentException("Unknown taskId" + taskId);
+        }
+        return moveToSideStage(task, sideStagePosition);
+    }
+
+    public boolean moveToSideStage(ActivityManager.RunningTaskInfo task,
+            @SplitPosition int sideStagePosition) {
+        return mStageCoordinator.moveToSideStage(task, sideStagePosition);
+    }
+
+    public boolean removeFromSideStage(int taskId) {
+        return mStageCoordinator.removeFromSideStage(taskId);
+    }
+
+    public void setSideStageOutline(boolean enable) {
+        mStageCoordinator.setSideStageOutline(enable);
+    }
+
+    public void setSideStagePosition(@SplitPosition int sideStagePosition) {
+        mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */);
+    }
+
+    public void setSideStageVisibility(boolean visible) {
+        mStageCoordinator.setSideStageVisibility(visible);
+    }
+
+    public void enterSplitScreen(int taskId, boolean leftOrTop) {
+        moveToSideStage(taskId,
+                leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT);
+    }
+
+    public void exitSplitScreen(int toTopTaskId, int exitReason) {
+        mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason);
+    }
+
+    public void onKeyguardOccludedChanged(boolean occluded) {
+        mStageCoordinator.onKeyguardOccludedChanged(occluded);
+    }
+
+    public void onKeyguardVisibilityChanged(boolean showing) {
+        mStageCoordinator.onKeyguardVisibilityChanged(showing);
+    }
+
+    public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
+        mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide);
+    }
+
+    public void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) {
+        mStageCoordinator.getStageBounds(outTopOrLeftBounds, outBottomOrRightBounds);
+    }
+
+    public void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) {
+        mStageCoordinator.registerSplitScreenListener(listener);
+    }
+
+    public void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) {
+        mStageCoordinator.unregisterSplitScreenListener(listener);
+    }
+
+    public void startTask(int taskId, @SplitScreen.StageType int stage,
+            @SplitPosition int position, @Nullable Bundle options) {
+        options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */);
+
+        try {
+            ActivityTaskManager.getService().startActivityFromRecents(taskId, options);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to launch task", e);
+        }
+    }
+
+    public void startShortcut(String packageName, String shortcutId,
+            @SplitScreen.StageType int stage, @SplitPosition int position,
+            @Nullable Bundle options, UserHandle user) {
+        options = mStageCoordinator.resolveStartStage(stage, position, options, null /* wct */);
+
+        try {
+            LauncherApps launcherApps =
+                    mContext.getSystemService(LauncherApps.class);
+            launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
+                    options, user);
+        } catch (ActivityNotFoundException e) {
+            Slog.e(TAG, "Failed to launch shortcut", e);
+        }
+    }
+
+    public void startIntent(PendingIntent intent, Intent fillInIntent,
+            @SplitScreen.StageType int stage, @SplitPosition int position,
+            @Nullable Bundle options) {
+        if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
+            startIntentLegacy(intent, fillInIntent, stage, position, options);
+            return;
+        }
+        mStageCoordinator.startIntent(intent, fillInIntent, stage, position, options,
+                null /* remote */);
+    }
+
+    private void startIntentLegacy(PendingIntent intent, Intent fillInIntent,
+            @SplitScreen.StageType int stage, @SplitPosition int position,
+            @Nullable Bundle options) {
+        LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() {
+            @Override
+            public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
+                    RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
+                    IRemoteAnimationFinishedCallback finishedCallback,
+                    SurfaceControl.Transaction t) {
+                mStageCoordinator.updateSurfaceBounds(null /* layout */, t);
+
+                if (apps != null) {
+                    for (int i = 0; i < apps.length; ++i) {
+                        if (apps[i].mode == MODE_OPENING) {
+                            t.show(apps[i].leash);
+                        }
+                    }
+                }
+
+                t.apply();
+                if (finishedCallback != null) {
+                    try {
+                        finishedCallback.onAnimationFinished();
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Error finishing legacy transition: ", e);
+                    }
+                }
+            }
+        };
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        options = mStageCoordinator.resolveStartStage(stage, position, options, wct);
+        wct.sendPendingIntent(intent, fillInIntent, options);
+        mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
+    }
+
+    RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) {
+        if (!isSplitScreenVisible()) return null;
+        final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+                .setContainerLayer()
+                .setName("RecentsAnimationSplitTasks")
+                .setHidden(false)
+                .setCallsite("SplitScreenController#onGoingtoRecentsLegacy");
+        mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder);
+        SurfaceControl sc = builder.build();
+        SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
+
+        // Ensure that we order these in the parent in the right z-order as their previous order
+        Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex);
+        int layer = 1;
+        for (RemoteAnimationTarget appTarget : apps) {
+            transaction.reparent(appTarget.leash, sc);
+            transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left,
+                    appTarget.screenSpaceBounds.top);
+            transaction.setLayer(appTarget.leash, layer++);
+        }
+        transaction.apply();
+        transaction.close();
+        return new RemoteAnimationTarget[]{
+                mStageCoordinator.getDividerBarLegacyTarget(),
+                mStageCoordinator.getOutlineLegacyTarget()};
+    }
+
+    /**
+     * Sets drag info to be logged when splitscreen is entered.
+     */
+    public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+        mStageCoordinator.logOnDroppedToSplit(position, dragSessionId);
+    }
+
+    public void dump(@NonNull PrintWriter pw, String prefix) {
+        pw.println(prefix + TAG);
+        if (mStageCoordinator != null) {
+            mStageCoordinator.dump(pw, prefix);
+        }
+    }
+
+    /**
+     * The interface for calls from outside the Shell, within the host process.
+     */
+    @ExternalThread
+    private class SplitScreenImpl implements SplitScreen {
+        private ISplitScreenImpl mISplitScreen;
+        private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>();
+        private final SplitScreenListener mListener = new SplitScreenListener() {
+            @Override
+            public void onStagePositionChanged(int stage, int position) {
+                for (int i = 0; i < mExecutors.size(); i++) {
+                    final int index = i;
+                    mExecutors.valueAt(index).execute(() -> {
+                        mExecutors.keyAt(index).onStagePositionChanged(stage, position);
+                    });
+                }
+            }
+
+            @Override
+            public void onTaskStageChanged(int taskId, int stage, boolean visible) {
+                for (int i = 0; i < mExecutors.size(); i++) {
+                    final int index = i;
+                    mExecutors.valueAt(index).execute(() -> {
+                        mExecutors.keyAt(index).onTaskStageChanged(taskId, stage, visible);
+                    });
+                }
+            }
+
+            @Override
+            public void onSplitVisibilityChanged(boolean visible) {
+                for (int i = 0; i < mExecutors.size(); i++) {
+                    final int index = i;
+                    mExecutors.valueAt(index).execute(() -> {
+                        mExecutors.keyAt(index).onSplitVisibilityChanged(visible);
+                    });
+                }
+            }
+        };
+
+        @Override
+        public ISplitScreen createExternalInterface() {
+            if (mISplitScreen != null) {
+                mISplitScreen.invalidate();
+            }
+            mISplitScreen = new ISplitScreenImpl(SplitScreenController.this);
+            return mISplitScreen;
+        }
+
+        @Override
+        public void onKeyguardOccludedChanged(boolean occluded) {
+            mMainExecutor.execute(() -> {
+                SplitScreenController.this.onKeyguardOccludedChanged(occluded);
+            });
+        }
+
+        @Override
+        public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) {
+            if (mExecutors.containsKey(listener)) return;
+
+            mMainExecutor.execute(() -> {
+                if (mExecutors.size() == 0) {
+                    SplitScreenController.this.registerSplitScreenListener(mListener);
+                }
+
+                mExecutors.put(listener, executor);
+            });
+
+            executor.execute(() -> {
+                mStageCoordinator.sendStatusToListener(listener);
+            });
+        }
+
+        @Override
+        public void unregisterSplitScreenListener(SplitScreenListener listener) {
+            mMainExecutor.execute(() -> {
+                mExecutors.remove(listener);
+
+                if (mExecutors.size() == 0) {
+                    SplitScreenController.this.unregisterSplitScreenListener(mListener);
+                }
+            });
+        }
+
+        @Override
+        public void onKeyguardVisibilityChanged(boolean showing) {
+            mMainExecutor.execute(() -> {
+                SplitScreenController.this.onKeyguardVisibilityChanged(showing);
+            });
+        }
+    }
+
+    /**
+     * The interface for calls from outside the host process.
+     */
+    @BinderThread
+    private static class ISplitScreenImpl extends ISplitScreen.Stub {
+        private SplitScreenController mController;
+        private ISplitScreenListener mListener;
+        private final SplitScreen.SplitScreenListener mSplitScreenListener =
+                new SplitScreen.SplitScreenListener() {
+                    @Override
+                    public void onStagePositionChanged(int stage, int position) {
+                        try {
+                            if (mListener != null) {
+                                mListener.onStagePositionChanged(stage, position);
+                            }
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "onStagePositionChanged", e);
+                        }
+                    }
+
+                    @Override
+                    public void onTaskStageChanged(int taskId, int stage, boolean visible) {
+                        try {
+                            if (mListener != null) {
+                                mListener.onTaskStageChanged(taskId, stage, visible);
+                            }
+                        } catch (RemoteException e) {
+                            Slog.e(TAG, "onTaskStageChanged", e);
+                        }
+                    }
+                };
+        private final IBinder.DeathRecipient mListenerDeathRecipient =
+                new IBinder.DeathRecipient() {
+                    @Override
+                    @BinderThread
+                    public void binderDied() {
+                        final SplitScreenController controller = mController;
+                        controller.getRemoteCallExecutor().execute(() -> {
+                            mListener = null;
+                            controller.unregisterSplitScreenListener(mSplitScreenListener);
+                        });
+                    }
+                };
+
+        public ISplitScreenImpl(SplitScreenController controller) {
+            mController = controller;
+        }
+
+        /**
+         * Invalidates this instance, preventing future calls from updating the controller.
+         */
+        void invalidate() {
+            mController = null;
+        }
+
+        @Override
+        public void registerSplitScreenListener(ISplitScreenListener listener) {
+            executeRemoteCallWithTaskPermission(mController, "registerSplitScreenListener",
+                    (controller) -> {
+                        if (mListener != null) {
+                            mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
+                                    0 /* flags */);
+                        }
+                        if (listener != null) {
+                            try {
+                                listener.asBinder().linkToDeath(mListenerDeathRecipient,
+                                        0 /* flags */);
+                            } catch (RemoteException e) {
+                                Slog.e(TAG, "Failed to link to death");
+                                return;
+                            }
+                        }
+                        mListener = listener;
+                        controller.registerSplitScreenListener(mSplitScreenListener);
+                    });
+        }
+
+        @Override
+        public void unregisterSplitScreenListener(ISplitScreenListener listener) {
+            executeRemoteCallWithTaskPermission(mController, "unregisterSplitScreenListener",
+                    (controller) -> {
+                        if (mListener != null) {
+                            mListener.asBinder().unlinkToDeath(mListenerDeathRecipient,
+                                    0 /* flags */);
+                        }
+                        mListener = null;
+                        controller.unregisterSplitScreenListener(mSplitScreenListener);
+                    });
+        }
+
+        @Override
+        public void exitSplitScreen(int toTopTaskId) {
+            executeRemoteCallWithTaskPermission(mController, "exitSplitScreen",
+                    (controller) -> {
+                        controller.exitSplitScreen(toTopTaskId,
+                                FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT);
+                    });
+        }
+
+        @Override
+        public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
+            executeRemoteCallWithTaskPermission(mController, "exitSplitScreenOnHide",
+                    (controller) -> {
+                        controller.exitSplitScreenOnHide(exitSplitScreenOnHide);
+                    });
+        }
+
+        @Override
+        public void setSideStageVisibility(boolean visible) {
+            executeRemoteCallWithTaskPermission(mController, "setSideStageVisibility",
+                    (controller) -> {
+                        controller.setSideStageVisibility(visible);
+                    });
+        }
+
+        @Override
+        public void removeFromSideStage(int taskId) {
+            executeRemoteCallWithTaskPermission(mController, "removeFromSideStage",
+                    (controller) -> {
+                        controller.removeFromSideStage(taskId);
+                    });
+        }
+
+        @Override
+        public void startTask(int taskId, int stage, int position, @Nullable Bundle options) {
+            executeRemoteCallWithTaskPermission(mController, "startTask",
+                    (controller) -> {
+                        controller.startTask(taskId, stage, position, options);
+                    });
+        }
+
+        @Override
+        public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
+                int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
+                RemoteAnimationAdapter adapter) {
+            executeRemoteCallWithTaskPermission(mController, "startTasks",
+                    (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition(
+                            mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition,
+                            adapter));
+        }
+
+        @Override
+        public void startTasks(int mainTaskId, @Nullable Bundle mainOptions,
+                int sideTaskId, @Nullable Bundle sideOptions,
+                @SplitPosition int sidePosition,
+                @Nullable RemoteTransition remoteTransition) {
+            executeRemoteCallWithTaskPermission(mController, "startTasks",
+                    (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions,
+                            sideTaskId, sideOptions, sidePosition, remoteTransition));
+        }
+
+        @Override
+        public void startShortcut(String packageName, String shortcutId, int stage, int position,
+                @Nullable Bundle options, UserHandle user) {
+            executeRemoteCallWithTaskPermission(mController, "startShortcut",
+                    (controller) -> {
+                        controller.startShortcut(packageName, shortcutId, stage, position,
+                                options, user);
+                    });
+        }
+
+        @Override
+        public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position,
+                @Nullable Bundle options) {
+            executeRemoteCallWithTaskPermission(mController, "startIntent",
+                    (controller) -> {
+                        controller.startIntent(intent, fillInIntent, stage, position, options);
+                    });
+        }
+
+        @Override
+        public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel,
+                RemoteAnimationTarget[] apps) {
+            final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null};
+            executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy",
+                    (controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps),
+                    true /* blocking */);
+            return out[0];
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java
new file mode 100644
index 0000000..af9a5aa
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+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.window.TransitionInfo.FLAG_FIRST_CUSTOM;
+
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
+import static com.android.wm.shell.transition.Transitions.isOpeningType;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.RemoteTransition;
+import android.window.TransitionInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.transition.OneShotRemoteHandler;
+import com.android.wm.shell.transition.Transitions;
+
+import java.util.ArrayList;
+
+/** Manages transition animations for split-screen. */
+class SplitScreenTransitions {
+    private static final String TAG = "SplitScreenTransitions";
+
+    /** Flag applied to a transition change to identify it as a divider bar for animation. */
+    public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
+
+    private final TransactionPool mTransactionPool;
+    private final Transitions mTransitions;
+    private final Runnable mOnFinish;
+
+    IBinder mPendingDismiss = null;
+    IBinder mPendingEnter = null;
+
+    private IBinder mAnimatingTransition = null;
+    private OneShotRemoteHandler mRemoteHandler = null;
+
+    private Transitions.TransitionFinishCallback mRemoteFinishCB = (wct, wctCB) -> {
+        if (wct != null || wctCB != null) {
+            throw new UnsupportedOperationException("finish transactions not supported yet.");
+        }
+        onFinish();
+    };
+
+    /** Keeps track of currently running animations */
+    private final ArrayList<Animator> mAnimations = new ArrayList<>();
+
+    private Transitions.TransitionFinishCallback mFinishCallback = null;
+    private SurfaceControl.Transaction mFinishTransaction;
+
+    SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions,
+            @NonNull Runnable onFinishCallback) {
+        mTransactionPool = pool;
+        mTransitions = transitions;
+        mOnFinish = onFinishCallback;
+    }
+
+    void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) {
+        mFinishCallback = finishCallback;
+        mAnimatingTransition = transition;
+        if (mRemoteHandler != null) {
+            mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction,
+                    mRemoteFinishCB);
+            mRemoteHandler = null;
+            return;
+        }
+        playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot);
+    }
+
+    private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot,
+            @NonNull WindowContainerToken sideRoot) {
+        mFinishTransaction = mTransactionPool.acquire();
+
+        // Play some place-holder fade animations
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            final SurfaceControl leash = change.getLeash();
+            final int mode = info.getChanges().get(i).getMode();
+
+            if (mode == TRANSIT_CHANGE) {
+                if (change.getParent() != null) {
+                    // This is probably reparented, so we want the parent to be immediately visible
+                    final TransitionInfo.Change parentChange = info.getChange(change.getParent());
+                    t.show(parentChange.getLeash());
+                    t.setAlpha(parentChange.getLeash(), 1.f);
+                    // and then animate this layer outside the parent (since, for example, this is
+                    // the home task animating from fullscreen to part-screen).
+                    t.reparent(leash, info.getRootLeash());
+                    t.setLayer(leash, info.getChanges().size() - i);
+                    // build the finish reparent/reposition
+                    mFinishTransaction.reparent(leash, parentChange.getLeash());
+                    mFinishTransaction.setPosition(leash,
+                            change.getEndRelOffset().x, change.getEndRelOffset().y);
+                }
+                // TODO(shell-transitions): screenshot here
+                final Rect startBounds = new Rect(change.getStartAbsBounds());
+                if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
+                    // Dismissing split via snap which means the still-visible task has been
+                    // dragged to its end position at animation start so reflect that here.
+                    startBounds.offsetTo(change.getEndAbsBounds().left,
+                            change.getEndAbsBounds().top);
+                }
+                final Rect endBounds = new Rect(change.getEndAbsBounds());
+                startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
+                endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
+                startExampleResizeAnimation(leash, startBounds, endBounds);
+            }
+            if (change.getParent() != null) {
+                continue;
+            }
+
+            if (transition == mPendingEnter && (mainRoot.equals(change.getContainer())
+                    || sideRoot.equals(change.getContainer()))) {
+                t.setWindowCrop(leash, change.getStartAbsBounds().width(),
+                        change.getStartAbsBounds().height());
+            }
+            boolean isOpening = isOpeningType(info.getType());
+            if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
+                // fade in
+                startExampleAnimation(leash, true /* show */);
+            } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) {
+                // fade out
+                if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
+                    // Dismissing via snap-to-top/bottom means that the dismissed task is already
+                    // not-visible (usually cropped to oblivion) so immediately set its alpha to 0
+                    // and don't animate it so it doesn't pop-in when reparented.
+                    t.setAlpha(leash, 0.f);
+                } else {
+                    startExampleAnimation(leash, false /* show */);
+                }
+            }
+        }
+        t.apply();
+        onFinish();
+    }
+
+    /** Starts a transition to enter split with a remote transition animator. */
+    IBinder startEnterTransition(@WindowManager.TransitionType int transitType,
+            @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition,
+            @NonNull Transitions.TransitionHandler handler) {
+        if (remoteTransition != null) {
+            // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff)
+            mRemoteHandler = new OneShotRemoteHandler(
+                    mTransitions.getMainExecutor(), remoteTransition);
+        }
+        final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
+        mPendingEnter = transition;
+        if (mRemoteHandler != null) {
+            mRemoteHandler.setTransition(transition);
+        }
+        return transition;
+    }
+
+    /** Starts a transition for dismissing split after dragging the divider to a screen edge */
+    IBinder startSnapToDismiss(@NonNull WindowContainerTransaction wct,
+            @NonNull Transitions.TransitionHandler handler) {
+        final IBinder transition = mTransitions.startTransition(
+                TRANSIT_SPLIT_DISMISS_SNAP, wct, handler);
+        mPendingDismiss = transition;
+        return transition;
+    }
+
+    void onFinish() {
+        if (!mAnimations.isEmpty()) return;
+        mOnFinish.run();
+        if (mFinishTransaction != null) {
+            mFinishTransaction.apply();
+            mTransactionPool.release(mFinishTransaction);
+            mFinishTransaction = null;
+        }
+        mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+        mFinishCallback = null;
+        if (mAnimatingTransition == mPendingEnter) {
+            mPendingEnter = null;
+        }
+        if (mAnimatingTransition == mPendingDismiss) {
+            mPendingDismiss = null;
+        }
+        mAnimatingTransition = null;
+    }
+
+    // TODO(shell-transitions): real animations
+    private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) {
+        final float end = show ? 1.f : 0.f;
+        final float start = 1.f - end;
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+        final ValueAnimator va = ValueAnimator.ofFloat(start, end);
+        va.setDuration(500);
+        va.addUpdateListener(animation -> {
+            float fraction = animation.getAnimatedFraction();
+            transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction);
+            transaction.apply();
+        });
+        final Runnable finisher = () -> {
+            transaction.setAlpha(leash, end);
+            transaction.apply();
+            mTransactionPool.release(transaction);
+            mTransitions.getMainExecutor().execute(() -> {
+                mAnimations.remove(va);
+                onFinish();
+            });
+        };
+        va.addListener(new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animation) { }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                finisher.run();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                finisher.run();
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animation) { }
+        });
+        mAnimations.add(va);
+        mTransitions.getAnimExecutor().execute(va::start);
+    }
+
+    // TODO(shell-transitions): real animations
+    private void startExampleResizeAnimation(@NonNull SurfaceControl leash,
+            @NonNull Rect startBounds, @NonNull Rect endBounds) {
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+        final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f);
+        va.setDuration(500);
+        va.addUpdateListener(animation -> {
+            float fraction = animation.getAnimatedFraction();
+            transaction.setWindowCrop(leash,
+                    (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction),
+                    (int) (startBounds.height() * (1.f - fraction)
+                            + endBounds.height() * fraction));
+            transaction.setPosition(leash,
+                    startBounds.left * (1.f - fraction) + endBounds.left * fraction,
+                    startBounds.top * (1.f - fraction) + endBounds.top * fraction);
+            transaction.apply();
+        });
+        final Runnable finisher = () -> {
+            transaction.setWindowCrop(leash, 0, 0);
+            transaction.setPosition(leash, endBounds.left, endBounds.top);
+            transaction.apply();
+            mTransactionPool.release(transaction);
+            mTransitions.getMainExecutor().execute(() -> {
+                mAnimations.remove(va);
+                onFinish();
+            });
+        };
+        va.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                finisher.run();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                finisher.run();
+            }
+        });
+        mAnimations.add(va);
+        mTransitions.getAnimExecutor().execute(va::start);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java
new file mode 100644
index 0000000..aab7902
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
+
+/**
+ * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent
+ */
+public class SplitscreenEventLogger {
+
+    // Used to generate instance ids for this drag if one is not provided
+    private final InstanceIdSequence mIdSequence;
+
+    // The instance id for the current splitscreen session (from start to end)
+    private InstanceId mLoggerSessionId;
+
+    // Drag info
+    private @SplitPosition int mDragEnterPosition;
+    private InstanceId mDragEnterSessionId;
+
+    // For deduping async events
+    private int mLastMainStagePosition = -1;
+    private int mLastMainStageUid = -1;
+    private int mLastSideStagePosition = -1;
+    private int mLastSideStageUid = -1;
+    private float mLastSplitRatio = -1f;
+
+    public SplitscreenEventLogger() {
+        mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE);
+    }
+
+    /**
+     * Return whether a splitscreen session has started.
+     */
+    public boolean hasStartedSession() {
+        return mLoggerSessionId != null;
+    }
+
+    /**
+     * May be called before logEnter() to indicate that the session was started from a drag.
+     */
+    public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) {
+        mDragEnterPosition = position;
+        mDragEnterSessionId = dragSessionId;
+    }
+
+    /**
+     * Logs when the user enters splitscreen.
+     */
+    public void logEnter(float splitRatio,
+            @SplitPosition int mainStagePosition, int mainStageUid,
+            @SplitPosition int sideStagePosition, int sideStageUid,
+            boolean isLandscape) {
+        mLoggerSessionId = mIdSequence.newInstanceId();
+        int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED
+                ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape)
+                : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
+        updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
+                mainStageUid);
+        updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
+                sideStageUid);
+        updateSplitRatioState(splitRatio);
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER,
+                enterReason,
+                0 /* exitReason */,
+                splitRatio,
+                mLastMainStagePosition,
+                mLastMainStageUid,
+                mLastSideStagePosition,
+                mLastSideStageUid,
+                mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when the user exits splitscreen.  Only one of the main or side stages should be
+     * specified to indicate which position was focused as a part of exiting (both can be unset).
+     */
+    public void logExit(int exitReason, @SplitPosition int mainStagePosition, int mainStageUid,
+            @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if ((mainStagePosition != SPLIT_POSITION_UNDEFINED
+                && sideStagePosition != SPLIT_POSITION_UNDEFINED)
+                        || (mainStageUid != 0 && sideStageUid != 0)) {
+            throw new IllegalArgumentException("Only main or side stage should be set");
+        }
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT,
+                0 /* enterReason */,
+                exitReason,
+                0f /* splitRatio */,
+                getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
+                mainStageUid,
+                getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
+                sideStageUid,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+
+        // Reset states
+        mLoggerSessionId = null;
+        mDragEnterPosition = SPLIT_POSITION_UNDEFINED;
+        mDragEnterSessionId = null;
+        mLastMainStagePosition = -1;
+        mLastMainStageUid = -1;
+        mLastSideStagePosition = -1;
+        mLastSideStageUid = -1;
+    }
+
+    /**
+     * Logs when an app in the main stage changes.
+     */
+    public void logMainStageAppChange(@SplitPosition int mainStagePosition, int mainStageUid,
+            boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if (!updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition,
+                isLandscape), mainStageUid)) {
+            // Ignore if there are no user perceived changes
+            return;
+        }
+
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                0f /* splitRatio */,
+                mLastMainStagePosition,
+                mLastMainStageUid,
+                0 /* sideStagePosition */,
+                0 /* sideStageUid */,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when an app in the side stage changes.
+     */
+    public void logSideStageAppChange(@SplitPosition int sideStagePosition, int sideStageUid,
+            boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if (!updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition,
+                isLandscape), sideStageUid)) {
+            // Ignore if there are no user perceived changes
+            return;
+        }
+
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                0f /* splitRatio */,
+                0 /* mainStagePosition */,
+                0 /* mainStageUid */,
+                mLastSideStagePosition,
+                mLastSideStageUid,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when the splitscreen ratio changes.
+     */
+    public void logResize(float splitRatio) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+        if (splitRatio <= 0f || splitRatio >= 1f) {
+            // Don't bother reporting resizes that end up dismissing the split, that will be logged
+            // via the exit event
+            return;
+        }
+        if (!updateSplitRatioState(splitRatio)) {
+            // Ignore if there are no user perceived changes
+            return;
+        }
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                mLastSplitRatio,
+                0 /* mainStagePosition */, 0 /* mainStageUid */,
+                0 /* sideStagePosition */, 0 /* sideStageUid */,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    /**
+     * Logs when the apps in splitscreen are swapped.
+     */
+    public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid,
+            @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
+        if (mLoggerSessionId == null) {
+            // Ignore changes until we've started logging the session
+            return;
+        }
+
+        updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
+                mainStageUid);
+        updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape),
+                sideStageUid);
+        FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
+                FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP,
+                0 /* enterReason */,
+                0 /* exitReason */,
+                0f /* splitRatio */,
+                mLastMainStagePosition,
+                mLastMainStageUid,
+                mLastSideStagePosition,
+                mLastSideStageUid,
+                0 /* dragInstanceId */,
+                mLoggerSessionId.getId());
+    }
+
+    private boolean updateMainStageState(int mainStagePosition, int mainStageUid) {
+        boolean changed = (mLastMainStagePosition != mainStagePosition)
+                || (mLastMainStageUid != mainStageUid);
+        if (!changed) {
+            return false;
+        }
+
+        mLastMainStagePosition = mainStagePosition;
+        mLastMainStageUid = mainStageUid;
+        return true;
+    }
+
+    private boolean updateSideStageState(int sideStagePosition, int sideStageUid) {
+        boolean changed = (mLastSideStagePosition != sideStagePosition)
+                || (mLastSideStageUid != sideStageUid);
+        if (!changed) {
+            return false;
+        }
+
+        mLastSideStagePosition = sideStagePosition;
+        mLastSideStageUid = sideStageUid;
+        return true;
+    }
+
+    private boolean updateSplitRatioState(float splitRatio) {
+        boolean changed = Float.compare(mLastSplitRatio, splitRatio) != 0;
+        if (!changed) {
+            return false;
+        }
+
+        mLastSplitRatio = splitRatio;
+        return true;
+    }
+
+    public int getDragEnterReasonFromSplitPosition(@SplitPosition int position,
+            boolean isLandscape) {
+        if (isLandscape) {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_LEFT
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_RIGHT;
+        } else {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_TOP
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_BOTTOM;
+        }
+    }
+
+    private int getMainStagePositionFromSplitPosition(@SplitPosition int position,
+            boolean isLandscape) {
+        if (position == SPLIT_POSITION_UNDEFINED) {
+            return 0;
+        }
+        if (isLandscape) {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__LEFT
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__RIGHT;
+        } else {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__TOP
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__BOTTOM;
+        }
+    }
+
+    private int getSideStagePositionFromSplitPosition(@SplitPosition int position,
+            boolean isLandscape) {
+        if (position == SPLIT_POSITION_UNDEFINED) {
+            return 0;
+        }
+        if (isLandscape) {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__LEFT
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__RIGHT;
+        } else {
+            return position == SPLIT_POSITION_TOP_OR_LEFT
+                    ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__TOP
+                    : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__BOTTOM;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java
new file mode 100644
index 0000000..2f75f8b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java
@@ -0,0 +1,1325 @@
+/*
+ * 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.wm.shell.stagesplit;
+
+import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
+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.transitTypeToString;
+import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER;
+
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_MAIN;
+import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_SIDE;
+import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_UNDEFINED;
+import static com.android.wm.shell.stagesplit.SplitScreen.stageTypeToString;
+import static com.android.wm.shell.stagesplit.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR;
+import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
+import static com.android.wm.shell.transition.Transitions.isClosingType;
+import static com.android.wm.shell.transition.Transitions.isOpeningType;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.ActivityTaskManager;
+import android.app.PendingIntent;
+import android.app.WindowConfiguration;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.hardware.devicestate.DeviceStateManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.view.WindowManager;
+import android.window.DisplayAreaInfo;
+import android.window.RemoteTransition;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.InstanceId;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitLayout;
+import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
+import com.android.wm.shell.common.split.SplitWindowManager;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.transition.Transitions;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Provider;
+
+/**
+ * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
+ * {@link SideStage} stages.
+ * Some high-level rules:
+ * - The {@link StageCoordinator} is only considered active if the {@link SideStage} contains at
+ * least one child task.
+ * - The {@link MainStage} should only have children if the coordinator is active.
+ * - The {@link SplitLayout} divider is only visible if both the {@link MainStage}
+ * and {@link SideStage} are visible.
+ * - The {@link MainStage} configuration is fullscreen when the {@link SideStage} isn't visible.
+ * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and
+ * {@link #onStageHasChildrenChanged(StageListenerImpl).}
+ */
+class StageCoordinator implements SplitLayout.SplitLayoutHandler,
+        RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener, Transitions.TransitionHandler {
+
+    private static final String TAG = StageCoordinator.class.getSimpleName();
+
+    /** internal value for mDismissTop that represents no dismiss */
+    private static final int NO_DISMISS = -2;
+
+    private final SurfaceSession mSurfaceSession = new SurfaceSession();
+
+    private final MainStage mMainStage;
+    private final StageListenerImpl mMainStageListener = new StageListenerImpl();
+    private final StageTaskUnfoldController mMainUnfoldController;
+    private final SideStage mSideStage;
+    private final StageListenerImpl mSideStageListener = new StageListenerImpl();
+    private final StageTaskUnfoldController mSideUnfoldController;
+    @SplitPosition
+    private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
+
+    private final int mDisplayId;
+    private SplitLayout mSplitLayout;
+    private boolean mDividerVisible;
+    private final SyncTransactionQueue mSyncQueue;
+    private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
+    private final ShellTaskOrganizer mTaskOrganizer;
+    private DisplayAreaInfo mDisplayAreaInfo;
+    private final Context mContext;
+    private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>();
+    private final DisplayImeController mDisplayImeController;
+    private final DisplayInsetsController mDisplayInsetsController;
+    private final SplitScreenTransitions mSplitTransitions;
+    private final SplitscreenEventLogger mLogger;
+    private boolean mExitSplitScreenOnHide;
+    private boolean mKeyguardOccluded;
+
+    // TODO(b/187041611): remove this flag after totally deprecated legacy split
+    /** Whether the device is supporting legacy split or not. */
+    private boolean mUseLegacySplit;
+
+    @SplitScreen.StageType private int mDismissTop = NO_DISMISS;
+
+    /** The target stage to dismiss to when unlock after folded. */
+    @SplitScreen.StageType private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
+
+    private final Runnable mOnTransitionAnimationComplete = () -> {
+        // If still playing, let it finish.
+        if (!isSplitScreenVisible()) {
+            // Update divider state after animation so that it is still around and positioned
+            // properly for the animation itself.
+            setDividerVisibility(false);
+            mSplitLayout.resetDividerPosition();
+        }
+        mDismissTop = NO_DISMISS;
+    };
+
+    private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks =
+            new SplitWindowManager.ParentContainerCallbacks() {
+        @Override
+        public void attachToParentSurface(SurfaceControl.Builder b) {
+            mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b);
+        }
+
+        @Override
+        public void onLeashReady(SurfaceControl leash) {
+            mSyncQueue.runInSync(t -> applyDividerVisibility(t));
+        }
+    };
+
+    StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
+            RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
+            DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController, Transitions transitions,
+            TransactionPool transactionPool, SplitscreenEventLogger logger,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+        mContext = context;
+        mDisplayId = displayId;
+        mSyncQueue = syncQueue;
+        mRootTDAOrganizer = rootTDAOrganizer;
+        mTaskOrganizer = taskOrganizer;
+        mLogger = logger;
+        mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
+        mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
+
+        mMainStage = new MainStage(
+                mTaskOrganizer,
+                mDisplayId,
+                mMainStageListener,
+                mSyncQueue,
+                mSurfaceSession,
+                mMainUnfoldController);
+        mSideStage = new SideStage(
+                mContext,
+                mTaskOrganizer,
+                mDisplayId,
+                mSideStageListener,
+                mSyncQueue,
+                mSurfaceSession,
+                mSideUnfoldController);
+        mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
+        mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSideStage);
+        mRootTDAOrganizer.registerListener(displayId, this);
+        final DeviceStateManager deviceStateManager =
+                mContext.getSystemService(DeviceStateManager.class);
+        deviceStateManager.registerCallback(taskOrganizer.getExecutor(),
+                new DeviceStateManager.FoldStateListener(mContext, this::onFoldedStateChanged));
+        mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
+                mOnTransitionAnimationComplete);
+        transitions.addHandler(this);
+    }
+
+    @VisibleForTesting
+    StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
+            RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer,
+            MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
+            Transitions transitions, TransactionPool transactionPool,
+            SplitscreenEventLogger logger,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+        mContext = context;
+        mDisplayId = displayId;
+        mSyncQueue = syncQueue;
+        mRootTDAOrganizer = rootTDAOrganizer;
+        mTaskOrganizer = taskOrganizer;
+        mMainStage = mainStage;
+        mSideStage = sideStage;
+        mDisplayImeController = displayImeController;
+        mDisplayInsetsController = displayInsetsController;
+        mRootTDAOrganizer.registerListener(displayId, this);
+        mSplitLayout = splitLayout;
+        mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
+                mOnTransitionAnimationComplete);
+        mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
+        mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
+        mLogger = logger;
+        transitions.addHandler(this);
+    }
+
+    @VisibleForTesting
+    SplitScreenTransitions getSplitTransitions() {
+        return mSplitTransitions;
+    }
+
+    boolean isSplitScreenVisible() {
+        return mSideStageListener.mVisible && mMainStageListener.mVisible;
+    }
+
+    boolean moveToSideStage(ActivityManager.RunningTaskInfo task,
+            @SplitPosition int sideStagePosition) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        setSideStagePosition(sideStagePosition, wct);
+        mMainStage.activate(getMainStageBounds(), wct);
+        mSideStage.addTask(task, getSideStageBounds(), wct);
+        mSyncQueue.queue(wct);
+        mSyncQueue.runInSync(t -> updateSurfaceBounds(null /* layout */, t));
+        return true;
+    }
+
+    boolean removeFromSideStage(int taskId) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+
+        /**
+         * {@link MainStage} will be deactivated in {@link #onStageHasChildrenChanged} if the
+         * {@link SideStage} no longer has children.
+         */
+        final boolean result = mSideStage.removeTask(taskId,
+                mMainStage.isActive() ? mMainStage.mRootTaskInfo.token : null,
+                wct);
+        mTaskOrganizer.applyTransaction(wct);
+        return result;
+    }
+
+    void setSideStageOutline(boolean enable) {
+        mSideStage.enableOutline(enable);
+    }
+
+    /** Starts 2 tasks in one transition. */
+    void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId,
+            @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
+            @Nullable RemoteTransition remoteTransition) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mainOptions = mainOptions != null ? mainOptions : new Bundle();
+        sideOptions = sideOptions != null ? sideOptions : new Bundle();
+        setSideStagePosition(sidePosition, wct);
+
+        // Build a request WCT that will launch both apps such that task 0 is on the main stage
+        // while task 1 is on the side stage.
+        mMainStage.activate(getMainStageBounds(), wct);
+        mSideStage.setBounds(getSideStageBounds(), wct);
+
+        // Make sure the launch options will put tasks in the corresponding split roots
+        addActivityOptions(mainOptions, mMainStage);
+        addActivityOptions(sideOptions, mSideStage);
+
+        // Add task launch requests
+        wct.startTask(mainTaskId, mainOptions);
+        wct.startTask(sideTaskId, sideOptions);
+
+        mSplitTransitions.startEnterTransition(
+                TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this);
+    }
+
+    /** Starts 2 tasks in one legacy transition. */
+    void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions,
+            int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition,
+            RemoteAnimationAdapter adapter) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        // Need to add another wrapper here in shell so that we can inject the divider bar
+        // and also manage the process elevation via setRunningRemote
+        IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
+            @Override
+            public void onAnimationStart(@WindowManager.TransitionOldType int transit,
+                    RemoteAnimationTarget[] apps,
+                    RemoteAnimationTarget[] wallpapers,
+                    RemoteAnimationTarget[] nonApps,
+                    final IRemoteAnimationFinishedCallback finishedCallback) {
+                RemoteAnimationTarget[] augmentedNonApps =
+                        new RemoteAnimationTarget[nonApps.length + 1];
+                for (int i = 0; i < nonApps.length; ++i) {
+                    augmentedNonApps[i] = nonApps[i];
+                }
+                augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget();
+                try {
+                    ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(
+                            adapter.getCallingApplication());
+                    adapter.getRunner().onAnimationStart(transit, apps, wallpapers, nonApps,
+                            finishedCallback);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error starting remote animation", e);
+                }
+            }
+
+            @Override
+            public void onAnimationCancelled() {
+                try {
+                    adapter.getRunner().onAnimationCancelled();
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error starting remote animation", e);
+                }
+            }
+        };
+        RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(
+                wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay());
+
+        if (mainOptions == null) {
+            mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle();
+        } else {
+            ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions);
+            mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
+        }
+
+        sideOptions = sideOptions != null ? sideOptions : new Bundle();
+        setSideStagePosition(sidePosition, wct);
+
+        // Build a request WCT that will launch both apps such that task 0 is on the main stage
+        // while task 1 is on the side stage.
+        mMainStage.activate(getMainStageBounds(), wct);
+        mSideStage.setBounds(getSideStageBounds(), wct);
+
+        // Make sure the launch options will put tasks in the corresponding split roots
+        addActivityOptions(mainOptions, mMainStage);
+        addActivityOptions(sideOptions, mSideStage);
+
+        // Add task launch requests
+        wct.startTask(mainTaskId, mainOptions);
+        wct.startTask(sideTaskId, sideOptions);
+
+        // Using legacy transitions, so we can't use blast sync since it conflicts.
+        mTaskOrganizer.applyTransaction(wct);
+    }
+
+    public void startIntent(PendingIntent intent, Intent fillInIntent,
+            @SplitScreen.StageType int stage, @SplitPosition int position,
+            @androidx.annotation.Nullable Bundle options,
+            @Nullable RemoteTransition remoteTransition) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        options = resolveStartStage(stage, position, options, wct);
+        wct.sendPendingIntent(intent, fillInIntent, options);
+        mSplitTransitions.startEnterTransition(
+                TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, remoteTransition, this);
+    }
+
+    Bundle resolveStartStage(@SplitScreen.StageType int stage,
+            @SplitPosition int position, @androidx.annotation.Nullable Bundle options,
+            @androidx.annotation.Nullable WindowContainerTransaction wct) {
+        switch (stage) {
+            case STAGE_TYPE_UNDEFINED: {
+                // Use the stage of the specified position is valid.
+                if (position != SPLIT_POSITION_UNDEFINED) {
+                    if (position == getSideStagePosition()) {
+                        options = resolveStartStage(STAGE_TYPE_SIDE, position, options, wct);
+                    } else {
+                        options = resolveStartStage(STAGE_TYPE_MAIN, position, options, wct);
+                    }
+                } else {
+                    // Exit split-screen and launch fullscreen since stage wasn't specified.
+                    prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct);
+                }
+                break;
+            }
+            case STAGE_TYPE_SIDE: {
+                if (position != SPLIT_POSITION_UNDEFINED) {
+                    setSideStagePosition(position, wct);
+                } else {
+                    position = getSideStagePosition();
+                }
+                if (options == null) {
+                    options = new Bundle();
+                }
+                updateActivityOptions(options, position);
+                break;
+            }
+            case STAGE_TYPE_MAIN: {
+                if (position != SPLIT_POSITION_UNDEFINED) {
+                    // Set the side stage opposite of what we want to the main stage.
+                    final int sideStagePosition = position == SPLIT_POSITION_TOP_OR_LEFT
+                            ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
+                    setSideStagePosition(sideStagePosition, wct);
+                } else {
+                    position = getMainStagePosition();
+                }
+                if (options == null) {
+                    options = new Bundle();
+                }
+                updateActivityOptions(options, position);
+                break;
+            }
+            default:
+                throw new IllegalArgumentException("Unknown stage=" + stage);
+        }
+
+        return options;
+    }
+
+    @SplitPosition
+    int getSideStagePosition() {
+        return mSideStagePosition;
+    }
+
+    @SplitPosition
+    int getMainStagePosition() {
+        return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT;
+    }
+
+    void setSideStagePosition(@SplitPosition int sideStagePosition,
+            @Nullable WindowContainerTransaction wct) {
+        setSideStagePosition(sideStagePosition, true /* updateBounds */, wct);
+    }
+
+    private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds,
+            @Nullable WindowContainerTransaction wct) {
+        if (mSideStagePosition == sideStagePosition) return;
+        mSideStagePosition = sideStagePosition;
+        sendOnStagePositionChanged();
+
+        if (mSideStageListener.mVisible && updateBounds) {
+            if (wct == null) {
+                // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds.
+                onLayoutChanged(mSplitLayout);
+            } else {
+                updateWindowBounds(mSplitLayout, wct);
+                updateUnfoldBounds();
+            }
+        }
+    }
+
+    void setSideStageVisibility(boolean visible) {
+        if (mSideStageListener.mVisible == visible) return;
+
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        mSideStage.setVisibility(visible, wct);
+        mTaskOrganizer.applyTransaction(wct);
+    }
+
+    void onKeyguardOccludedChanged(boolean occluded) {
+        // Do not exit split directly, because it needs to wait for task info update to determine
+        // which task should remain on top after split dismissed.
+        mKeyguardOccluded = occluded;
+    }
+
+    void onKeyguardVisibilityChanged(boolean showing) {
+        if (!showing && mMainStage.isActive()
+                && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) {
+            exitSplitScreen(mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage,
+                    SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED);
+        }
+    }
+
+    void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
+        mExitSplitScreenOnHide = exitSplitScreenOnHide;
+    }
+
+    void exitSplitScreen(int toTopTaskId, int exitReason) {
+        StageTaskListener childrenToTop = null;
+        if (mMainStage.containsTask(toTopTaskId)) {
+            childrenToTop = mMainStage;
+        } else if (mSideStage.containsTask(toTopTaskId)) {
+            childrenToTop = mSideStage;
+        }
+
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        if (childrenToTop != null) {
+            childrenToTop.reorderChild(toTopTaskId, true /* onTop */, wct);
+        }
+        applyExitSplitScreen(childrenToTop, wct, exitReason);
+    }
+
+    private void exitSplitScreen(StageTaskListener childrenToTop, int exitReason) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        applyExitSplitScreen(childrenToTop, wct, exitReason);
+    }
+
+    private void applyExitSplitScreen(
+            StageTaskListener childrenToTop,
+            WindowContainerTransaction wct, int exitReason) {
+        mSideStage.removeAllTasks(wct, childrenToTop == mSideStage);
+        mMainStage.deactivate(wct, childrenToTop == mMainStage);
+        mTaskOrganizer.applyTransaction(wct);
+        mSyncQueue.runInSync(t -> t
+                .setWindowCrop(mMainStage.mRootLeash, null)
+                .setWindowCrop(mSideStage.mRootLeash, null));
+        // Hide divider and reset its position.
+        setDividerVisibility(false);
+        mSplitLayout.resetDividerPosition();
+        mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
+        if (childrenToTop != null) {
+            logExitToStage(exitReason, childrenToTop == mMainStage);
+        } else {
+            logExit(exitReason);
+        }
+    }
+
+    /**
+     * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates
+     * an existing WindowContainerTransaction (rather than applying immediately). This is intended
+     * to be used when exiting split might be bundled with other window operations.
+     */
+    void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop,
+            @NonNull WindowContainerTransaction wct) {
+        mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE);
+        mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN);
+    }
+
+    void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) {
+        outTopOrLeftBounds.set(mSplitLayout.getBounds1());
+        outBottomOrRightBounds.set(mSplitLayout.getBounds2());
+    }
+
+    private void addActivityOptions(Bundle opts, StageTaskListener stage) {
+        opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token);
+    }
+
+    void updateActivityOptions(Bundle opts, @SplitPosition int position) {
+        addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage);
+    }
+
+    void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) {
+        if (mListeners.contains(listener)) return;
+        mListeners.add(listener);
+        sendStatusToListener(listener);
+    }
+
+    void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) {
+        mListeners.remove(listener);
+    }
+
+    void sendStatusToListener(SplitScreen.SplitScreenListener listener) {
+        listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
+        listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
+        listener.onSplitVisibilityChanged(isSplitScreenVisible());
+        mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE);
+        mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN);
+    }
+
+    private void sendOnStagePositionChanged() {
+        for (int i = mListeners.size() - 1; i >= 0; --i) {
+            final SplitScreen.SplitScreenListener l = mListeners.get(i);
+            l.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
+            l.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
+        }
+    }
+
+    private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId,
+            boolean present, boolean visible) {
+        int stage;
+        if (present) {
+            stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
+        } else {
+            // No longer on any stage
+            stage = STAGE_TYPE_UNDEFINED;
+        }
+        if (stage == STAGE_TYPE_MAIN) {
+            mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                    mSplitLayout.isLandscape());
+        } else {
+            mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                    mSplitLayout.isLandscape());
+        }
+
+        for (int i = mListeners.size() - 1; i >= 0; --i) {
+            mListeners.get(i).onTaskStageChanged(taskId, stage, visible);
+        }
+    }
+
+    private void sendSplitVisibilityChanged() {
+        for (int i = mListeners.size() - 1; i >= 0; --i) {
+            final SplitScreen.SplitScreenListener l = mListeners.get(i);
+            l.onSplitVisibilityChanged(mDividerVisible);
+        }
+
+        if (mMainUnfoldController != null && mSideUnfoldController != null) {
+            mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible);
+            mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible);
+        }
+    }
+
+    private void onStageRootTaskAppeared(StageListenerImpl stageListener) {
+        if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) {
+            mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit);
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            // Make the stages adjacent to each other so they occlude what's behind them.
+            wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+
+            // Only sets side stage as launch-adjacent-flag-root when the device is not using legacy
+            // split to prevent new split behavior confusing users.
+            if (!mUseLegacySplit) {
+                wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token);
+            }
+
+            mTaskOrganizer.applyTransaction(wct);
+        }
+    }
+
+    private void onStageRootTaskVanished(StageListenerImpl stageListener) {
+        if (stageListener == mMainStageListener || stageListener == mSideStageListener) {
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            // Deactivate the main stage if it no longer has a root task.
+            mMainStage.deactivate(wct);
+
+            if (!mUseLegacySplit) {
+                wct.clearLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token);
+            }
+
+            mTaskOrganizer.applyTransaction(wct);
+        }
+    }
+
+    private void setDividerVisibility(boolean visible) {
+        if (mDividerVisible == visible) return;
+        mDividerVisible = visible;
+        if (visible) {
+            mSplitLayout.init();
+            updateUnfoldBounds();
+        } else {
+            mSplitLayout.release();
+        }
+        sendSplitVisibilityChanged();
+    }
+
+    private void onStageVisibilityChanged(StageListenerImpl stageListener) {
+        final boolean sideStageVisible = mSideStageListener.mVisible;
+        final boolean mainStageVisible = mMainStageListener.mVisible;
+        final boolean bothStageVisible = sideStageVisible && mainStageVisible;
+        final boolean bothStageInvisible = !sideStageVisible && !mainStageVisible;
+        final boolean sameVisibility = sideStageVisible == mainStageVisible;
+        // Only add or remove divider when both visible or both invisible to avoid sometimes we only
+        // got one stage visibility changed for a moment and it will cause flicker.
+        if (sameVisibility) {
+            setDividerVisibility(bothStageVisible);
+        }
+
+        if (bothStageInvisible) {
+            if (mExitSplitScreenOnHide
+            // Don't dismiss staged split when both stages are not visible due to sleeping display,
+            // like the cases keyguard showing or screen off.
+            || (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping)) {
+                exitSplitScreen(null /* childrenToTop */,
+                        SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
+            }
+        } else if (mKeyguardOccluded) {
+            // At least one of the stages is visible while keyguard occluded. Dismiss split because
+            // there's show-when-locked activity showing on top of keyguard. Also make sure the
+            // task contains show-when-locked activity remains on top after split dismissed.
+            final StageTaskListener toTop =
+                    mainStageVisible ? mMainStage : (sideStageVisible ? mSideStage : null);
+            exitSplitScreen(toTop, SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP);
+        }
+
+        mSyncQueue.runInSync(t -> {
+            // Same above, we only set root tasks and divider leash visibility when both stage
+            // change to visible or invisible to avoid flicker.
+            if (sameVisibility) {
+                t.setVisibility(mSideStage.mRootLeash, bothStageVisible)
+                        .setVisibility(mMainStage.mRootLeash, bothStageVisible);
+                applyDividerVisibility(t);
+                applyOutlineVisibility(t);
+            }
+        });
+    }
+
+    private void applyDividerVisibility(SurfaceControl.Transaction t) {
+        final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
+        if (dividerLeash == null) {
+            return;
+        }
+
+        if (mDividerVisible) {
+            t.show(dividerLeash)
+                    .setLayer(dividerLeash, SPLIT_DIVIDER_LAYER)
+                    .setPosition(dividerLeash,
+                            mSplitLayout.getDividerBounds().left,
+                            mSplitLayout.getDividerBounds().top);
+        } else {
+            t.hide(dividerLeash);
+        }
+    }
+
+    private void applyOutlineVisibility(SurfaceControl.Transaction t) {
+        final SurfaceControl outlineLeash = mSideStage.getOutlineLeash();
+        if (outlineLeash == null) {
+            return;
+        }
+
+        if (mDividerVisible) {
+            t.show(outlineLeash).setLayer(outlineLeash, SPLIT_DIVIDER_LAYER);
+        } else {
+            t.hide(outlineLeash);
+        }
+    }
+
+    private void onStageHasChildrenChanged(StageListenerImpl stageListener) {
+        final boolean hasChildren = stageListener.mHasChildren;
+        final boolean isSideStage = stageListener == mSideStageListener;
+        if (!hasChildren) {
+            if (isSideStage && mMainStageListener.mVisible) {
+                // Exit to main stage if side stage no longer has children.
+                exitSplitScreen(mMainStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED);
+            } else if (!isSideStage && mSideStageListener.mVisible) {
+                // Exit to side stage if main stage no longer has children.
+                exitSplitScreen(mSideStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED);
+            }
+        } else if (isSideStage) {
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            // Make sure the main stage is active.
+            mMainStage.activate(getMainStageBounds(), wct);
+            mSideStage.setBounds(getSideStageBounds(), wct);
+            mTaskOrganizer.applyTransaction(wct);
+        }
+        if (!mLogger.hasStartedSession() && mMainStageListener.mHasChildren
+                && mSideStageListener.mHasChildren) {
+            mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
+                    getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                    getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                    mSplitLayout.isLandscape());
+        }
+    }
+
+    @VisibleForTesting
+    IBinder onSnappedToDismissTransition(boolean mainStageToTop) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        prepareExitSplitScreen(mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE, wct);
+        return mSplitTransitions.startSnapToDismiss(wct, this);
+    }
+
+    @Override
+    public void onSnappedToDismiss(boolean bottomOrRight) {
+        final boolean mainStageToTop =
+                bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
+                        : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
+        if (ENABLE_SHELL_TRANSITIONS) {
+            onSnappedToDismissTransition(mainStageToTop);
+            return;
+        }
+        exitSplitScreen(mainStageToTop ? mMainStage : mSideStage,
+                SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER);
+    }
+
+    @Override
+    public void onDoubleTappedDivider() {
+        setSideStagePosition(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT, null /* wct */);
+        mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                mSplitLayout.isLandscape());
+    }
+
+    @Override
+    public void onLayoutChanging(SplitLayout layout) {
+        mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
+        mSideStage.setOutlineVisibility(false);
+    }
+
+    @Override
+    public void onLayoutChanged(SplitLayout layout) {
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        updateWindowBounds(layout, wct);
+        updateUnfoldBounds();
+        mSyncQueue.queue(wct);
+        mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t));
+        mSideStage.setOutlineVisibility(true);
+        mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
+    }
+
+    private void updateUnfoldBounds() {
+        if (mMainUnfoldController != null && mSideUnfoldController != null) {
+            mMainUnfoldController.onLayoutChanged(getMainStageBounds());
+            mSideUnfoldController.onLayoutChanged(getSideStageBounds());
+        }
+    }
+
+    /**
+     * Populates `wct` with operations that match the split windows to the current layout.
+     * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied
+     */
+    private void updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) {
+        final StageTaskListener topLeftStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+        final StageTaskListener bottomRightStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo);
+    }
+
+    void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t) {
+        final StageTaskListener topLeftStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+        final StageTaskListener bottomRightStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash,
+                bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer);
+    }
+
+    @Override
+    public int getSplitItemPosition(WindowContainerToken token) {
+        if (token == null) {
+            return SPLIT_POSITION_UNDEFINED;
+        }
+
+        if (token.equals(mMainStage.mRootTaskInfo.getToken())) {
+            return getMainStagePosition();
+        } else if (token.equals(mSideStage.mRootTaskInfo.getToken())) {
+            return getSideStagePosition();
+        }
+
+        return SPLIT_POSITION_UNDEFINED;
+    }
+
+    @Override
+    public void onLayoutShifted(int offsetX, int offsetY, SplitLayout layout) {
+        final StageTaskListener topLeftStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
+        final StageTaskListener bottomRightStage =
+                mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
+        final WindowContainerTransaction wct = new WindowContainerTransaction();
+        layout.applyLayoutShifted(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo,
+                bottomRightStage.mRootTaskInfo);
+        mTaskOrganizer.applyTransaction(wct);
+    }
+
+    @Override
+    public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) {
+        mDisplayAreaInfo = displayAreaInfo;
+        if (mSplitLayout == null) {
+            mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext,
+                    mDisplayAreaInfo.configuration, this, mParentContainerCallbacks,
+                    mDisplayImeController, mTaskOrganizer);
+            mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
+
+            if (mMainUnfoldController != null && mSideUnfoldController != null) {
+                mMainUnfoldController.init();
+                mSideUnfoldController.init();
+            }
+        }
+    }
+
+    @Override
+    public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) {
+        throw new IllegalStateException("Well that was unexpected...");
+    }
+
+    @Override
+    public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) {
+        mDisplayAreaInfo = displayAreaInfo;
+        if (mSplitLayout != null
+                && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration)
+                && mMainStage.isActive()) {
+            onLayoutChanged(mSplitLayout);
+        }
+    }
+
+    private void onFoldedStateChanged(boolean folded) {
+        mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
+        if (!folded) return;
+
+        if (mMainStage.isFocused()) {
+            mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN;
+        } else if (mSideStage.isFocused()) {
+            mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE;
+        }
+    }
+
+    private Rect getSideStageBounds() {
+        return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2();
+    }
+
+    private Rect getMainStageBounds() {
+        return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
+                ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1();
+    }
+
+    /**
+     * Get the stage that should contain this `taskInfo`. The stage doesn't necessarily contain
+     * this task (yet) so this can also be used to identify which stage to put a task into.
+     */
+    private StageTaskListener getStageOfTask(ActivityManager.RunningTaskInfo taskInfo) {
+        // TODO(b/184679596): Find a way to either include task-org information in the transition,
+        //                    or synchronize task-org callbacks so we can use stage.containsTask
+        if (mMainStage.mRootTaskInfo != null
+                && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) {
+            return mMainStage;
+        } else if (mSideStage.mRootTaskInfo != null
+                && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) {
+            return mSideStage;
+        }
+        return null;
+    }
+
+    @SplitScreen.StageType
+    private int getStageType(StageTaskListener stage) {
+        return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
+    }
+
+    @Override
+    public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+            @Nullable TransitionRequestInfo request) {
+        final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
+        if (triggerTask == null) {
+            // still want to monitor everything while in split-screen, so return non-null.
+            return isSplitScreenVisible() ? new WindowContainerTransaction() : null;
+        }
+
+        WindowContainerTransaction out = null;
+        final @WindowManager.TransitionType int type = request.getType();
+        if (isSplitScreenVisible()) {
+            // try to handle everything while in split-screen, so return a WCT even if it's empty.
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  split is active so using split"
+                            + "Transition to handle request. triggerTask=%d type=%s mainChildren=%d"
+                            + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type),
+                    mMainStage.getChildCount(), mSideStage.getChildCount());
+            out = new WindowContainerTransaction();
+            final StageTaskListener stage = getStageOfTask(triggerTask);
+            if (stage != null) {
+                // dismiss split if the last task in one of the stages is going away
+                if (isClosingType(type) && stage.getChildCount() == 1) {
+                    // The top should be the opposite side that is closing:
+                    mDismissTop = getStageType(stage) == STAGE_TYPE_MAIN
+                            ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
+                }
+            } else {
+                if (triggerTask.getActivityType() == ACTIVITY_TYPE_HOME && isOpeningType(type)) {
+                    // Going home so dismiss both.
+                    mDismissTop = STAGE_TYPE_UNDEFINED;
+                }
+            }
+            if (mDismissTop != NO_DISMISS) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  splitTransition "
+                                + " deduced Dismiss from request. toTop=%s",
+                        stageTypeToString(mDismissTop));
+                prepareExitSplitScreen(mDismissTop, out);
+                mSplitTransitions.mPendingDismiss = transition;
+            }
+        } else {
+            // Not in split mode, so look for an open into a split stage just so we can whine and
+            // complain about how this isn't a supported operation.
+            if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT)) {
+                if (getStageOfTask(triggerTask) != null) {
+                    throw new IllegalStateException("Entering split implicitly with only one task"
+                            + " isn't supported.");
+                }
+            }
+        }
+        return out;
+    }
+
+    @Override
+    public boolean startAnimation(@NonNull IBinder transition,
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        if (transition != mSplitTransitions.mPendingDismiss
+                && transition != mSplitTransitions.mPendingEnter) {
+            // Not entering or exiting, so just do some house-keeping and validation.
+
+            // If we're not in split-mode, just abort so something else can handle it.
+            if (!isSplitScreenVisible()) return false;
+
+            for (int iC = 0; iC < info.getChanges().size(); ++iC) {
+                final TransitionInfo.Change change = info.getChanges().get(iC);
+                final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+                if (taskInfo == null || !taskInfo.hasParentTask()) continue;
+                final StageTaskListener stage = getStageOfTask(taskInfo);
+                if (stage == null) continue;
+                if (isOpeningType(change.getMode())) {
+                    if (!stage.containsTask(taskInfo.taskId)) {
+                        Log.w(TAG, "Expected onTaskAppeared on " + stage + " to have been called"
+                                + " with " + taskInfo.taskId + " before startAnimation().");
+                    }
+                } else if (isClosingType(change.getMode())) {
+                    if (stage.containsTask(taskInfo.taskId)) {
+                        Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called"
+                                + " with " + taskInfo.taskId + " before startAnimation().");
+                    }
+                }
+            }
+            if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
+                // TODO(shell-transitions): Implement a fallback behavior for now.
+                throw new IllegalStateException("Somehow removed the last task in a stage"
+                        + " outside of a proper transition");
+                // This can happen in some pathological cases. For example:
+                // 1. main has 2 tasks [Task A (Single-task), Task B], side has one task [Task C]
+                // 2. Task B closes itself and starts Task A in LAUNCH_ADJACENT at the same time
+                // In this case, the result *should* be that we leave split.
+                // TODO(b/184679596): Find a way to either include task-org information in
+                //                    the transition, or synchronize task-org callbacks.
+            }
+
+            // Use normal animations.
+            return false;
+        }
+
+        boolean shouldAnimate = true;
+        if (mSplitTransitions.mPendingEnter == transition) {
+            shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction);
+        } else if (mSplitTransitions.mPendingDismiss == transition) {
+            shouldAnimate = startPendingDismissAnimation(transition, info, startTransaction);
+        }
+        if (!shouldAnimate) return false;
+
+        mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction,
+                finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+        return true;
+    }
+
+    private boolean startPendingEnterAnimation(@NonNull IBinder transition,
+            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
+        if (info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN) {
+            // First, verify that we actually have opened 2 apps in split.
+            TransitionInfo.Change mainChild = null;
+            TransitionInfo.Change sideChild = null;
+            for (int iC = 0; iC < info.getChanges().size(); ++iC) {
+                final TransitionInfo.Change change = info.getChanges().get(iC);
+                final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+                if (taskInfo == null || !taskInfo.hasParentTask()) continue;
+                final @SplitScreen.StageType int stageType = getStageType(getStageOfTask(taskInfo));
+                if (stageType == STAGE_TYPE_MAIN) {
+                    mainChild = change;
+                } else if (stageType == STAGE_TYPE_SIDE) {
+                    sideChild = change;
+                }
+            }
+            if (mainChild == null || sideChild == null) {
+                throw new IllegalStateException("Launched 2 tasks in split, but didn't receive"
+                        + " 2 tasks in transition. Possibly one of them failed to launch");
+                // TODO: fallback logic. Probably start a new transition to exit split before
+                //       applying anything here. Ideally consolidate with transition-merging.
+            }
+
+            // Update local states (before animating).
+            setDividerVisibility(true);
+            setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateBounds */,
+                    null /* wct */);
+            setSplitsVisible(true);
+
+            addDividerBarToTransition(info, t, true /* show */);
+
+            // Make some noise if things aren't totally expected. These states shouldn't effect
+            // transitions locally, but remotes (like Launcher) may get confused if they were
+            // depending on listener callbacks. This can happen because task-organizer callbacks
+            // aren't serialized with transition callbacks.
+            // TODO(b/184679596): Find a way to either include task-org information in
+            //                    the transition, or synchronize task-org callbacks.
+            if (!mMainStage.containsTask(mainChild.getTaskInfo().taskId)) {
+                Log.w(TAG, "Expected onTaskAppeared on " + mMainStage
+                        + " to have been called with " + mainChild.getTaskInfo().taskId
+                        + " before startAnimation().");
+            }
+            if (!mSideStage.containsTask(sideChild.getTaskInfo().taskId)) {
+                Log.w(TAG, "Expected onTaskAppeared on " + mSideStage
+                        + " to have been called with " + sideChild.getTaskInfo().taskId
+                        + " before startAnimation().");
+            }
+            return true;
+        } else {
+            // TODO: other entry method animations
+            throw new RuntimeException("Unsupported split-entry");
+        }
+    }
+
+    private boolean startPendingDismissAnimation(@NonNull IBinder transition,
+            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
+        // Make some noise if things aren't totally expected. These states shouldn't effect
+        // transitions locally, but remotes (like Launcher) may get confused if they were
+        // depending on listener callbacks. This can happen because task-organizer callbacks
+        // aren't serialized with transition callbacks.
+        // TODO(b/184679596): Find a way to either include task-org information in
+        //                    the transition, or synchronize task-org callbacks.
+        if (mMainStage.getChildCount() != 0) {
+            final StringBuilder tasksLeft = new StringBuilder();
+            for (int i = 0; i < mMainStage.getChildCount(); ++i) {
+                tasksLeft.append(i != 0 ? ", " : "");
+                tasksLeft.append(mMainStage.mChildrenTaskInfo.keyAt(i));
+            }
+            Log.w(TAG, "Expected onTaskVanished on " + mMainStage
+                    + " to have been called with [" + tasksLeft.toString()
+                    + "] before startAnimation().");
+        }
+        if (mSideStage.getChildCount() != 0) {
+            final StringBuilder tasksLeft = new StringBuilder();
+            for (int i = 0; i < mSideStage.getChildCount(); ++i) {
+                tasksLeft.append(i != 0 ? ", " : "");
+                tasksLeft.append(mSideStage.mChildrenTaskInfo.keyAt(i));
+            }
+            Log.w(TAG, "Expected onTaskVanished on " + mSideStage
+                    + " to have been called with [" + tasksLeft.toString()
+                    + "] before startAnimation().");
+        }
+
+        // Update local states.
+        setSplitsVisible(false);
+        // Wait until after animation to update divider
+
+        if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) {
+            // Reset crops so they don't interfere with subsequent launches
+            t.setWindowCrop(mMainStage.mRootLeash, null);
+            t.setWindowCrop(mSideStage.mRootLeash, null);
+        }
+
+        if (mDismissTop == STAGE_TYPE_UNDEFINED) {
+            // Going home (dismissing both splits)
+
+            // TODO: Have a proper remote for this. Until then, though, reset state and use the
+            //       normal animation stuff (which falls back to the normal launcher remote).
+            t.hide(mSplitLayout.getDividerLeash());
+            setDividerVisibility(false);
+            mSplitTransitions.mPendingDismiss = null;
+            return false;
+        }
+
+        addDividerBarToTransition(info, t, false /* show */);
+        // We're dismissing split by moving the other one to fullscreen.
+        // Since we don't have any animations for this yet, just use the internal example
+        // animations.
+        return true;
+    }
+
+    private void addDividerBarToTransition(@NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, boolean show) {
+        final SurfaceControl leash = mSplitLayout.getDividerLeash();
+        final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash);
+        final Rect bounds = mSplitLayout.getDividerBounds();
+        barChange.setStartAbsBounds(bounds);
+        barChange.setEndAbsBounds(bounds);
+        barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK);
+        barChange.setFlags(FLAG_IS_DIVIDER_BAR);
+        // Technically this should be order-0, but this is running after layer assignment
+        // and it's a special case, so just add to end.
+        info.addChange(barChange);
+        // Be default, make it visible. The remote animator can adjust alpha if it plans to animate.
+        if (show) {
+            t.setAlpha(leash, 1.f);
+            t.setLayer(leash, SPLIT_DIVIDER_LAYER);
+            t.setPosition(leash, bounds.left, bounds.top);
+            t.show(leash);
+        }
+    }
+
+    RemoteAnimationTarget getDividerBarLegacyTarget() {
+        final Rect bounds = mSplitLayout.getDividerBounds();
+        return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */,
+                mSplitLayout.getDividerLeash(), false /* isTranslucent */, null /* clipRect */,
+                null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
+                new android.graphics.Point(0, 0) /* position */, bounds, bounds,
+                new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */,
+                null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER);
+    }
+
+    RemoteAnimationTarget getOutlineLegacyTarget() {
+        final Rect bounds = mSideStage.mRootTaskInfo.configuration.windowConfiguration.getBounds();
+        // Leverage TYPE_DOCK_DIVIDER type when wrapping outline remote animation target in order to
+        // distinguish as a split auxiliary target in Launcher.
+        return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */,
+                mSideStage.getOutlineLeash(), false /* isTranslucent */, null /* clipRect */,
+                null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
+                new android.graphics.Point(0, 0) /* position */, bounds, bounds,
+                new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */,
+                null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER);
+    }
+
+    @Override
+    public void dump(@NonNull PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        final String childPrefix = innerPrefix + "  ";
+        pw.println(prefix + TAG + " mDisplayId=" + mDisplayId);
+        pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible);
+        pw.println(innerPrefix + "MainStage");
+        pw.println(childPrefix + "isActive=" + mMainStage.isActive());
+        mMainStageListener.dump(pw, childPrefix);
+        pw.println(innerPrefix + "SideStage");
+        mSideStageListener.dump(pw, childPrefix);
+        pw.println(innerPrefix + "mSplitLayout=" + mSplitLayout);
+    }
+
+    /**
+     * Directly set the visibility of both splits. This assumes hasChildren matches visibility.
+     * This is intended for batch use, so it assumes other state management logic is already
+     * handled.
+     */
+    private void setSplitsVisible(boolean visible) {
+        mMainStageListener.mVisible = mSideStageListener.mVisible = visible;
+        mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible;
+    }
+
+    /**
+     * Sets drag info to be logged when splitscreen is next entered.
+     */
+    public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+        mLogger.enterRequestedByDrag(position, dragSessionId);
+    }
+
+    /**
+     * Logs the exit of splitscreen.
+     */
+    private void logExit(int exitReason) {
+        mLogger.logExit(exitReason,
+                SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */,
+                SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */,
+                mSplitLayout.isLandscape());
+    }
+
+    /**
+     * Logs the exit of splitscreen to a specific stage. This must be called before the exit is
+     * executed.
+     */
+    private void logExitToStage(int exitReason, boolean toMainStage) {
+        mLogger.logExit(exitReason,
+                toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED,
+                toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */,
+                !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED,
+                !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */,
+                mSplitLayout.isLandscape());
+    }
+
+    class StageListenerImpl implements StageTaskListener.StageListenerCallbacks {
+        boolean mHasRootTask = false;
+        boolean mVisible = false;
+        boolean mHasChildren = false;
+
+        @Override
+        public void onRootTaskAppeared() {
+            mHasRootTask = true;
+            StageCoordinator.this.onStageRootTaskAppeared(this);
+        }
+
+        @Override
+        public void onStatusChanged(boolean visible, boolean hasChildren) {
+            if (!mHasRootTask) return;
+
+            if (mHasChildren != hasChildren) {
+                mHasChildren = hasChildren;
+                StageCoordinator.this.onStageHasChildrenChanged(this);
+            }
+            if (mVisible != visible) {
+                mVisible = visible;
+                StageCoordinator.this.onStageVisibilityChanged(this);
+            }
+        }
+
+        @Override
+        public void onChildTaskStatusChanged(int taskId, boolean present, boolean visible) {
+            StageCoordinator.this.onStageChildTaskStatusChanged(this, taskId, present, visible);
+        }
+
+        @Override
+        public void onRootTaskVanished() {
+            reset();
+            StageCoordinator.this.onStageRootTaskVanished(this);
+        }
+
+        @Override
+        public void onNoLongerSupportMultiWindow() {
+            if (mMainStage.isActive()) {
+                StageCoordinator.this.exitSplitScreen(null /* childrenToTop */,
+                        SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW);
+            }
+        }
+
+        private void reset() {
+            mHasRootTask = false;
+            mVisible = false;
+            mHasChildren = false;
+        }
+
+        public void dump(@NonNull PrintWriter pw, String prefix) {
+            pw.println(prefix + "mHasRootTask=" + mHasRootTask);
+            pw.println(prefix + "mVisible=" + mVisible);
+            pw.println(prefix + "mHasChildren=" + mHasChildren);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java
new file mode 100644
index 0000000..8b36c94
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java
@@ -0,0 +1,288 @@
+/*
+ * 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.wm.shell.stagesplit;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
+
+import android.annotation.CallSuper;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.SurfaceControl;
+import android.view.SurfaceSession;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.SurfaceUtils;
+import com.android.wm.shell.common.SyncTransactionQueue;
+
+import java.io.PrintWriter;
+
+/**
+ * Base class that handle common task org. related for split-screen stages.
+ * Note that this class and its sub-class do not directly perform hierarchy operations.
+ * They only serve to hold a collection of tasks and provide APIs like
+ * {@link #setBounds(Rect, WindowContainerTransaction)} for the centralized {@link StageCoordinator}
+ * to perform operations in-sync with other containers.
+ *
+ * @see StageCoordinator
+ */
+class StageTaskListener implements ShellTaskOrganizer.TaskListener {
+    private static final String TAG = StageTaskListener.class.getSimpleName();
+
+    protected static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD};
+    protected static final int[] CONTROLLED_WINDOWING_MODES =
+            {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
+    protected static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE =
+            {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW};
+
+    /** Callback interface for listening to changes in a split-screen stage. */
+    public interface StageListenerCallbacks {
+        void onRootTaskAppeared();
+
+        void onStatusChanged(boolean visible, boolean hasChildren);
+
+        void onChildTaskStatusChanged(int taskId, boolean present, boolean visible);
+
+        void onRootTaskVanished();
+        void onNoLongerSupportMultiWindow();
+    }
+
+    private final StageListenerCallbacks mCallbacks;
+    private final SurfaceSession mSurfaceSession;
+    protected final SyncTransactionQueue mSyncQueue;
+
+    protected ActivityManager.RunningTaskInfo mRootTaskInfo;
+    protected SurfaceControl mRootLeash;
+    protected SurfaceControl mDimLayer;
+    protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>();
+    private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>();
+
+    private final StageTaskUnfoldController mStageTaskUnfoldController;
+
+    StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId,
+            StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
+            SurfaceSession surfaceSession,
+            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+        mCallbacks = callbacks;
+        mSyncQueue = syncQueue;
+        mSurfaceSession = surfaceSession;
+        mStageTaskUnfoldController = stageTaskUnfoldController;
+        taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
+    }
+
+    int getChildCount() {
+        return mChildrenTaskInfo.size();
+    }
+
+    boolean containsTask(int taskId) {
+        return mChildrenTaskInfo.contains(taskId);
+    }
+
+    /**
+     * Returns the top activity uid for the top child task.
+     */
+    int getTopChildTaskUid() {
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
+            final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i);
+            if (info.topActivityInfo == null) {
+                continue;
+            }
+            return info.topActivityInfo.applicationInfo.uid;
+        }
+        return 0;
+    }
+
+    /** @return {@code true} if this listener contains the currently focused task. */
+    boolean isFocused() {
+        if (mRootTaskInfo == null) {
+            return false;
+        }
+
+        if (mRootTaskInfo.isFocused) {
+            return true;
+        }
+
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
+            if (mChildrenTaskInfo.valueAt(i).isFocused) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+        if (mRootTaskInfo == null && !taskInfo.hasParentTask()) {
+            mRootLeash = leash;
+            mRootTaskInfo = taskInfo;
+            mCallbacks.onRootTaskAppeared();
+            sendStatusChanged();
+            mSyncQueue.runInSync(t -> {
+                t.hide(mRootLeash);
+                mDimLayer =
+                        SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession);
+            });
+        } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
+            final int taskId = taskInfo.taskId;
+            mChildrenLeashes.put(taskId, leash);
+            mChildrenTaskInfo.put(taskId, taskInfo);
+            updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */);
+            mCallbacks.onChildTaskStatusChanged(taskId, true /* present */, taskInfo.isVisible);
+            if (ENABLE_SHELL_TRANSITIONS) {
+                // Status is managed/synchronized by the transition lifecycle.
+                return;
+            }
+            sendStatusChanged();
+        } else {
+            throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+                    + "\n mRootTaskInfo: " + mRootTaskInfo);
+        }
+
+        if (mStageTaskUnfoldController != null) {
+            mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash);
+        }
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+        if (!taskInfo.supportsMultiWindow) {
+            // Leave split screen if the task no longer supports multi window.
+            mCallbacks.onNoLongerSupportMultiWindow();
+            return;
+        }
+        if (mRootTaskInfo.taskId == taskInfo.taskId) {
+            mRootTaskInfo = taskInfo;
+        } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
+            mChildrenTaskInfo.put(taskInfo.taskId, taskInfo);
+            mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */,
+                    taskInfo.isVisible);
+            if (!ENABLE_SHELL_TRANSITIONS) {
+                updateChildTaskSurface(
+                        taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */);
+            }
+        } else {
+            throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+                    + "\n mRootTaskInfo: " + mRootTaskInfo);
+        }
+        if (ENABLE_SHELL_TRANSITIONS) {
+            // Status is managed/synchronized by the transition lifecycle.
+            return;
+        }
+        sendStatusChanged();
+    }
+
+    @Override
+    @CallSuper
+    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+        final int taskId = taskInfo.taskId;
+        if (mRootTaskInfo.taskId == taskId) {
+            mCallbacks.onRootTaskVanished();
+            mSyncQueue.runInSync(t -> t.remove(mDimLayer));
+            mRootTaskInfo = null;
+        } else if (mChildrenTaskInfo.contains(taskId)) {
+            mChildrenTaskInfo.remove(taskId);
+            mChildrenLeashes.remove(taskId);
+            mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible);
+            if (ENABLE_SHELL_TRANSITIONS) {
+                // Status is managed/synchronized by the transition lifecycle.
+                return;
+            }
+            sendStatusChanged();
+        } else {
+            throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
+                    + "\n mRootTaskInfo: " + mRootTaskInfo);
+        }
+
+        if (mStageTaskUnfoldController != null) {
+            mStageTaskUnfoldController.onTaskVanished(taskInfo);
+        }
+    }
+
+    @Override
+    public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
+        if (mRootTaskInfo.taskId == taskId) {
+            b.setParent(mRootLeash);
+        } else if (mChildrenLeashes.contains(taskId)) {
+            b.setParent(mChildrenLeashes.get(taskId));
+        } else {
+            throw new IllegalArgumentException("There is no surface for taskId=" + taskId);
+        }
+    }
+
+    void setBounds(Rect bounds, WindowContainerTransaction wct) {
+        wct.setBounds(mRootTaskInfo.token, bounds);
+    }
+
+    void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) {
+        if (!containsTask(taskId)) {
+            return;
+        }
+        wct.reorder(mChildrenTaskInfo.get(taskId).token, onTop /* onTop */);
+    }
+
+    void setVisibility(boolean visible, WindowContainerTransaction wct) {
+        wct.reorder(mRootTaskInfo.token, visible /* onTop */);
+    }
+
+    void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener,
+            @SplitScreen.StageType int stage) {
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
+            int taskId = mChildrenTaskInfo.keyAt(i);
+            listener.onTaskStageChanged(taskId, stage,
+                    mChildrenTaskInfo.get(taskId).isVisible);
+        }
+    }
+
+    private void updateChildTaskSurface(ActivityManager.RunningTaskInfo taskInfo,
+            SurfaceControl leash, boolean firstAppeared) {
+        final Point taskPositionInParent = taskInfo.positionInParent;
+        mSyncQueue.runInSync(t -> {
+            t.setWindowCrop(leash, null);
+            t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y);
+            if (firstAppeared && !ENABLE_SHELL_TRANSITIONS) {
+                t.setAlpha(leash, 1f);
+                t.setMatrix(leash, 1, 0, 0, 1);
+                t.show(leash);
+            }
+        });
+    }
+
+    private void sendStatusChanged() {
+        mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0);
+    }
+
+    @Override
+    @CallSuper
+    public void dump(@NonNull PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        final String childPrefix = innerPrefix + "  ";
+        pw.println(prefix + this);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java
new file mode 100644
index 0000000..62b9da6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.stagesplit;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.animation.RectEvaluator;
+import android.animation.TypeEvaluator;
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
+import com.android.wm.shell.unfold.UnfoldBackgroundController;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Controls transformations of the split screen task surfaces in response
+ * to the unfolding/folding action on foldable devices
+ */
+public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener {
+
+    private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
+    private static final float CROPPING_START_MARGIN_FRACTION = 0.05f;
+
+    private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
+    private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
+    private final DisplayInsetsController mDisplayInsetsController;
+    private final UnfoldBackgroundController mBackgroundController;
+    private final Executor mExecutor;
+    private final int mExpandedTaskBarHeight;
+    private final float mWindowCornerRadiusPx;
+    private final Rect mStageBounds = new Rect();
+    private final TransactionPool mTransactionPool;
+
+    private InsetsSource mTaskbarInsetsSource;
+    private boolean mBothStagesVisible;
+
+    public StageTaskUnfoldController(@NonNull Context context,
+            @NonNull TransactionPool transactionPool,
+            @NonNull ShellUnfoldProgressProvider unfoldProgressProvider,
+            @NonNull DisplayInsetsController displayInsetsController,
+            @NonNull UnfoldBackgroundController backgroundController,
+            @NonNull Executor executor) {
+        mUnfoldProgressProvider = unfoldProgressProvider;
+        mTransactionPool = transactionPool;
+        mExecutor = executor;
+        mBackgroundController = backgroundController;
+        mDisplayInsetsController = displayInsetsController;
+        mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
+        mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.taskbar_frame_height);
+    }
+
+    /**
+     * Initializes the controller, starts listening for the external events
+     */
+    public void init() {
+        mUnfoldProgressProvider.addListener(mExecutor, this);
+        mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
+    }
+
+    @Override
+    public void insetsChanged(InsetsState insetsState) {
+        mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            context.update();
+        }
+    }
+
+    /**
+     * Called when split screen task appeared
+     * @param taskInfo info for the appeared task
+     * @param leash surface leash for the appeared task
+     */
+    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+        AnimationContext context = new AnimationContext(leash);
+        mAnimationContextByTaskId.put(taskInfo.taskId, context);
+    }
+
+    /**
+     * Called when a split screen task vanished
+     * @param taskInfo info for the vanished task
+     */
+    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+        AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId);
+        if (context != null) {
+            final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+            resetSurface(transaction, context);
+            transaction.apply();
+            mTransactionPool.release(transaction);
+        }
+        mAnimationContextByTaskId.remove(taskInfo.taskId);
+    }
+
+    @Override
+    public void onStateChangeProgress(float progress) {
+        if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return;
+
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+        mBackgroundController.ensureBackground(transaction);
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+
+            context.mCurrentCropRect.set(RECT_EVALUATOR
+                    .evaluate(progress, context.mStartCropRect, context.mEndCropRect));
+
+            transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
+                    .setCornerRadius(context.mLeash, mWindowCornerRadiusPx);
+        }
+
+        transaction.apply();
+
+        mTransactionPool.release(transaction);
+    }
+
+    @Override
+    public void onStateChangeFinished() {
+        resetTransformations();
+    }
+
+    /**
+     * Called when split screen visibility changes
+     * @param bothStagesVisible true if both stages of the split screen are visible
+     */
+    public void onSplitVisibilityChanged(boolean bothStagesVisible) {
+        mBothStagesVisible = bothStagesVisible;
+        if (!bothStagesVisible) {
+            resetTransformations();
+        }
+    }
+
+    /**
+     * Called when split screen stage bounds changed
+     * @param bounds new bounds for this stage
+     */
+    public void onLayoutChanged(Rect bounds) {
+        mStageBounds.set(bounds);
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            context.update();
+        }
+    }
+
+    private void resetTransformations() {
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            resetSurface(transaction, context);
+        }
+        mBackgroundController.removeBackground(transaction);
+        transaction.apply();
+
+        mTransactionPool.release(transaction);
+    }
+
+    private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) {
+        transaction
+                .setWindowCrop(context.mLeash, null)
+                .setCornerRadius(context.mLeash, 0.0F);
+    }
+
+    private class AnimationContext {
+        final SurfaceControl mLeash;
+        final Rect mStartCropRect = new Rect();
+        final Rect mEndCropRect = new Rect();
+        final Rect mCurrentCropRect = new Rect();
+
+        private AnimationContext(SurfaceControl leash) {
+            this.mLeash = leash;
+            update();
+        }
+
+        private void update() {
+            mStartCropRect.set(mStageBounds);
+
+            if (mTaskbarInsetsSource != null) {
+                // Only insets the cropping window with taskbar when taskbar is expanded
+                if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) {
+                    mStartCropRect.inset(mTaskbarInsetsSource
+                            .calculateVisibleInsets(mStartCropRect));
+                }
+            }
+
+            // Offset to surface coordinates as layout bounds are in screen coordinates
+            mStartCropRect.offsetTo(0, 0);
+
+            mEndCropRect.set(mStartCropRect);
+
+            int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height());
+            int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION);
+            mStartCropRect.inset(margin, margin, margin, margin);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index 8df7cbb..b191cab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -110,9 +110,9 @@
     @VisibleForTesting
     final ColorCache mColorCache;
 
-    SplashscreenContentDrawer(Context context, TransactionPool pool) {
+    SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool) {
         mContext = context;
-        mIconProvider = new IconProvider(context);
+        mIconProvider = iconProvider;
         mTransactionPool = pool;
 
         // Initialize Splashscreen worker thread
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
index f0685a8..38122ff 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
@@ -38,6 +38,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.Trace;
+import android.util.Log;
 import android.util.PathParser;
 import android.window.SplashScreenView;
 
@@ -50,6 +51,8 @@
  */
 public class SplashscreenIconDrawableFactory {
 
+    private static final String TAG = "SplashscreenIconDrawableFactory";
+
     /**
      * @return An array containing the foreground drawable at index 0 and if needed a background
      * drawable at index 1.
@@ -282,7 +285,12 @@
                     if (startListener != null) {
                         startListener.run();
                     }
-                    mAnimatableIcon.start();
+                    try {
+                        mAnimatableIcon.start();
+                    } catch (Exception ex) {
+                        Log.e(TAG, "Error while running the splash screen animated icon", ex);
+                        animation.cancel();
+                    }
                 }
 
                 @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index ff3428c..bd48696 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -37,7 +37,6 @@
 import android.content.res.TypedArray;
 import android.graphics.Color;
 import android.graphics.PixelFormat;
-import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
 import android.os.IBinder;
 import android.os.RemoteCallback;
@@ -48,7 +47,6 @@
 import android.util.SparseArray;
 import android.view.Choreographer;
 import android.view.Display;
-import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
 import android.view.View;
 import android.view.WindowManager;
@@ -58,10 +56,12 @@
 import android.window.SplashScreenView.SplashScreenViewParcelable;
 import android.window.StartingWindowInfo;
 import android.window.StartingWindowInfo.StartingWindowType;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskSnapshot;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
@@ -118,16 +118,17 @@
     private Choreographer mChoreographer;
     private final WindowManagerGlobal mWindowManagerGlobal;
     private StartingSurface.SysuiProxy mSysuiProxy;
+    private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo();
 
     /**
      * @param splashScreenExecutor The thread used to control add and remove starting window.
      */
     public StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor,
-            TransactionPool pool) {
+            IconProvider iconProvider, TransactionPool pool) {
         mContext = context;
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         mSplashScreenExecutor = splashScreenExecutor;
-        mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, pool);
+        mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool);
         mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance());
         mWindowManagerGlobal = WindowManagerGlobal.getInstance();
         mDisplayManager.getDisplay(DEFAULT_DISPLAY);
@@ -458,12 +459,13 @@
     /**
      * Called when the content of a task is ready to show, starting window can be removed.
      */
-    public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-            boolean playRevealAnimation) {
+    public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
         if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) {
-            Slog.d(TAG, "Task start finish, remove starting surface for task " + taskId);
+            Slog.d(TAG, "Task start finish, remove starting surface for task "
+                    + removalInfo.taskId);
         }
-        removeWindowSynced(taskId, leash, frame, playRevealAnimation);
+        removeWindowSynced(removalInfo);
+
     }
 
     /**
@@ -556,7 +558,8 @@
     }
 
     private void removeWindowNoAnimate(int taskId) {
-        removeWindowSynced(taskId, null, null, false);
+        mTmpRemovalInfo.taskId = taskId;
+        removeWindowSynced(mTmpRemovalInfo);
     }
 
     void onImeDrawnOnTask(int taskId) {
@@ -568,8 +571,8 @@
         mStartingWindowRecords.remove(taskId);
     }
 
-    protected void removeWindowSynced(int taskId, SurfaceControl leash, Rect frame,
-            boolean playRevealAnimation) {
+    protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo) {
+        final int taskId = removalInfo.taskId;
         final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
         if (record != null) {
             if (record.mDecorView != null) {
@@ -580,9 +583,9 @@
                     if (record.mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
                         removeWindowInner(record.mDecorView, false);
                     } else {
-                        if (playRevealAnimation) {
+                        if (removalInfo.playRevealAnimation) {
                             mSplashscreenContentDrawer.applyExitAnimation(record.mContentView,
-                                    leash, frame,
+                                    removalInfo.windowAnimationLeash, removalInfo.mainFrame,
                                     () -> removeWindowInner(record.mDecorView, true));
                         } else {
                             // the SplashScreenView has been copied to client, hide the view to skip
@@ -602,7 +605,7 @@
                     Slog.v(TAG, "Removing task snapshot window for " + taskId);
                 }
                 record.mTaskSnapshotWindow.scheduleRemove(
-                        () -> mStartingWindowRecords.remove(taskId));
+                        () -> mStartingWindowRecords.remove(taskId), removalInfo.deferRemoveForIme);
             }
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
index 4433e27..a86e07a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
@@ -28,16 +28,14 @@
 import android.app.TaskInfo;
 import android.content.Context;
 import android.graphics.Color;
-import android.graphics.Rect;
-import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.Trace;
 import android.util.Slog;
 import android.util.SparseIntArray;
-import android.view.SurfaceControl;
 import android.window.StartingWindowInfo;
 import android.window.StartingWindowInfo.StartingWindowType;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskOrganizer;
 import android.window.TaskSnapshot;
 
@@ -45,6 +43,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.function.TriConsumer;
+import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
@@ -68,7 +67,7 @@
 public class StartingWindowController implements RemoteCallable<StartingWindowController> {
     private static final String TAG = StartingWindowController.class.getSimpleName();
 
-    public static final boolean DEBUG_SPLASH_SCREEN = Build.isDebuggable();
+    public static final boolean DEBUG_SPLASH_SCREEN = false;
     public static final boolean DEBUG_TASK_SNAPSHOT = false;
 
     private static final long TASK_BG_COLOR_RETAIN_TIME_MS = 5000;
@@ -87,9 +86,11 @@
     private final SparseIntArray mTaskBackgroundColors = new SparseIntArray();
 
     public StartingWindowController(Context context, ShellExecutor splashScreenExecutor,
-            StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, TransactionPool pool) {
+            StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider,
+            TransactionPool pool) {
         mContext = context;
-        mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor, pool);
+        mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor,
+                iconProvider, pool);
         mStartingWindowTypeAlgorithm = startingWindowTypeAlgorithm;
         mSplashScreenExecutor = splashScreenExecutor;
     }
@@ -186,13 +187,12 @@
     /**
      * Called when the content of a task is ready to show, starting window can be removed.
      */
-    public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-            boolean playRevealAnimation) {
+    public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
         mSplashScreenExecutor.execute(() -> mStartingSurfaceDrawer.removeStartingWindow(
-                taskId, leash, frame, playRevealAnimation));
+                removalInfo));
         mSplashScreenExecutor.executeDelayed(() -> {
             synchronized (mTaskBackgroundColors) {
-                mTaskBackgroundColors.delete(taskId);
+                mTaskBackgroundColors.delete(removalInfo.taskId);
             }
         }, TASK_BG_COLOR_RETAIN_TIME_MS);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index 979aa1f..3e88c46 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -123,14 +123,13 @@
      * Ideally the delay time will be shorter when receiving
      * {@link StartingSurfaceDrawer#onImeDrawnOnTask(int)}.
      */
-    private static final long MAX_DELAY_REMOVAL_TIME_IME_VISIBLE = 450;
+    private static final long MAX_DELAY_REMOVAL_TIME_IME_VISIBLE = 600;
 
     //tmp vars for unused relayout params
     private static final Point TMP_SURFACE_SIZE = new Point();
 
     private final Window mWindow;
     private final Runnable mClearWindowHandler;
-    private final long mDelayRemovalTime;
     private final ShellExecutor mSplashScreenExecutor;
     private final SurfaceControl mSurfaceControl;
     private final IWindowSession mSession;
@@ -221,13 +220,10 @@
             taskDescription.setBackgroundColor(WHITE);
         }
 
-        final long delayRemovalTime = snapshot.hasImeSurface() ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE
-                : DELAY_REMOVAL_TIME_GENERAL;
-
         final TaskSnapshotWindow snapshotSurface = new TaskSnapshotWindow(
                 surfaceControl, snapshot, layoutParams.getTitle(), taskDescription, appearance,
                 windowFlags, windowPrivateFlags, taskBounds, orientation, activityType,
-                delayRemovalTime, topWindowInsetsState, clearWindowHandler, splashScreenExecutor);
+                topWindowInsetsState, clearWindowHandler, splashScreenExecutor);
         final Window window = snapshotSurface.mWindow;
 
         final InsetsState tmpInsetsState = new InsetsState();
@@ -265,9 +261,8 @@
     public TaskSnapshotWindow(SurfaceControl surfaceControl,
             TaskSnapshot snapshot, CharSequence title, TaskDescription taskDescription,
             int appearance, int windowFlags, int windowPrivateFlags, Rect taskBounds,
-            int currentOrientation, int activityType, long delayRemovalTime,
-            InsetsState topWindowInsetsState, Runnable clearWindowHandler,
-            ShellExecutor splashScreenExecutor) {
+            int currentOrientation, int activityType, InsetsState topWindowInsetsState,
+            Runnable clearWindowHandler, ShellExecutor splashScreenExecutor) {
         mSplashScreenExecutor = splashScreenExecutor;
         mSession = WindowManagerGlobal.getWindowSession();
         mWindow = new Window();
@@ -283,7 +278,6 @@
         mStatusBarColor = taskDescription.getStatusBarColor();
         mOrientationOnCreation = currentOrientation;
         mActivityType = activityType;
-        mDelayRemovalTime = delayRemovalTime;
         mTransaction = new SurfaceControl.Transaction();
         mClearWindowHandler = clearWindowHandler;
         mHasImeSurface = snapshot.hasImeSurface();
@@ -314,7 +308,7 @@
         mSystemBarBackgroundPainter.drawNavigationBarBackground(c);
     }
 
-    void scheduleRemove(Runnable onRemove) {
+    void scheduleRemove(Runnable onRemove, boolean deferRemoveForIme) {
         // Show the latest content as soon as possible for unlocking to home.
         if (mActivityType == ACTIVITY_TYPE_HOME) {
             removeImmediately();
@@ -329,9 +323,12 @@
             TaskSnapshotWindow.this.removeImmediately();
             onRemove.run();
         };
-        mSplashScreenExecutor.executeDelayed(mScheduledRunnable, mDelayRemovalTime);
+        final long delayRemovalTime = mHasImeSurface && deferRemoveForIme
+                ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE
+                : DELAY_REMOVAL_TIME_GENERAL;
+        mSplashScreenExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime);
         if (DEBUG) {
-            Slog.d(TAG, "Defer removing snapshot surface in " + mDelayRemovalTime);
+            Slog.d(TAG, "Defer removing snapshot surface in " + delayRemovalTime);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
index bde2b5f..05ba74a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java
@@ -23,6 +23,7 @@
 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SNAPSHOT;
 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED;
+import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_DRAWN;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_LEGACY_SPLASH_SCREEN;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_NEW_TASK;
@@ -41,7 +42,7 @@
 
 /**
  * Algorithm for determining the type of a new starting window on handheld devices.
- * At the moment also used on Android Auto.
+ * At the moment also used on Android Auto and Wear OS.
  */
 public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgorithm {
     private static final String TAG = PhoneStartingWindowTypeAlgorithm.class.getSimpleName();
@@ -58,6 +59,7 @@
                 (parameter & TYPE_PARAMETER_USE_EMPTY_SPLASH_SCREEN) != 0;
         final boolean legacySplashScreen =
                 ((parameter & TYPE_PARAMETER_LEGACY_SPLASH_SCREEN) != 0);
+        final boolean activityDrawn = (parameter & TYPE_PARAMETER_ACTIVITY_DRAWN) != 0;
         final boolean topIsHome = windowInfo.taskInfo.topActivityType == ACTIVITY_TYPE_HOME;
 
         if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) {
@@ -68,11 +70,14 @@
                     + " activityCreated:" + activityCreated
                     + " useEmptySplashScreen:" + useEmptySplashScreen
                     + " legacySplashScreen:" + legacySplashScreen
+                    + " activityDrawn:" + activityDrawn
                     + " topIsHome:" + topIsHome);
         }
 
         if (!topIsHome) {
-            if (!processRunning || newTask || (taskSwitch && !activityCreated)) {
+            if (!processRunning
+                    || newTask
+                    || (taskSwitch && (!activityCreated || !activityDrawn))) {
                 return useEmptySplashScreen
                         ? STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN
                         : legacySplashScreen
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index b673d48..663d647 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -41,6 +41,7 @@
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
+import static android.window.TransitionInfo.isIndependent;
 
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
 import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE;
@@ -70,6 +71,7 @@
 import android.view.animation.Transformation;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
 import com.android.internal.R;
@@ -82,6 +84,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.util.CounterRotator;
 
 import java.util.ArrayList;
 
@@ -269,9 +272,16 @@
         final ArrayList<Animator> animations = new ArrayList<>();
         mAnimations.put(transition, animations);
 
+        final ArrayMap<WindowContainerToken, CounterRotator> counterRotators = new ArrayMap<>();
+
         final Runnable onAnimFinish = () -> {
             if (!animations.isEmpty()) return;
 
+            for (int i = 0; i < counterRotators.size(); ++i) {
+                counterRotators.valueAt(i).cleanUp(info.getRootLeash());
+            }
+            counterRotators.clear();
+
             if (mRotationAnimation != null) {
                 mRotationAnimation.kill();
                 mRotationAnimation = null;
@@ -285,16 +295,44 @@
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
 
-            if (info.getType() == TRANSIT_CHANGE && change.getMode() == TRANSIT_CHANGE
-                    && (change.getFlags() & FLAG_IS_DISPLAY) != 0) {
-                boolean isSeamless = isRotationSeamless(info, mDisplayController);
-                final int anim = getRotationAnimation(info);
-                if (!(isSeamless || anim == ROTATION_ANIMATION_JUMPCUT)) {
-                    mRotationAnimation = new ScreenRotationAnimation(mContext, mSurfaceSession,
-                            mTransactionPool, startTransaction, change, info.getRootLeash());
-                    mRotationAnimation.startAnimation(animations, onAnimFinish,
-                            mTransitionAnimationScaleSetting, mMainExecutor, mAnimExecutor);
-                    continue;
+            if (change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0) {
+                int rotateDelta = change.getEndRotation() - change.getStartRotation();
+                int displayW = change.getEndAbsBounds().width();
+                int displayH = change.getEndAbsBounds().height();
+                if (info.getType() == TRANSIT_CHANGE) {
+                    boolean isSeamless = isRotationSeamless(info, mDisplayController);
+                    final int anim = getRotationAnimation(info);
+                    if (!(isSeamless || anim == ROTATION_ANIMATION_JUMPCUT)) {
+                        mRotationAnimation = new ScreenRotationAnimation(mContext, mSurfaceSession,
+                                mTransactionPool, startTransaction, change, info.getRootLeash());
+                        mRotationAnimation.startAnimation(animations, onAnimFinish,
+                                mTransitionAnimationScaleSetting, mMainExecutor, mAnimExecutor);
+                        continue;
+                    }
+                } else {
+                    // opening/closing an app into a new orientation. Counter-rotate all
+                    // "going-away" things since they are still in the old orientation.
+                    for (int j = info.getChanges().size() - 1; j >= 0; --j) {
+                        final TransitionInfo.Change innerChange = info.getChanges().get(j);
+                        if (!Transitions.isClosingType(innerChange.getMode())
+                                || !isIndependent(innerChange, info)
+                                || innerChange.getParent() == null) {
+                            continue;
+                        }
+                        CounterRotator crot = counterRotators.get(innerChange.getParent());
+                        if (crot == null) {
+                            crot = new CounterRotator();
+                            crot.setup(startTransaction,
+                                    info.getChange(innerChange.getParent()).getLeash(),
+                                    rotateDelta, displayW, displayH);
+                            if (crot.getSurface() != null) {
+                                int layer = info.getChanges().size() - j;
+                                startTransaction.setLayer(crot.getSurface(), layer);
+                            }
+                            counterRotators.put(innerChange.getParent(), crot);
+                        }
+                        crot.addChild(startTransaction, innerChange.getLeash());
+                    }
                 }
             }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java
new file mode 100644
index 0000000..9faf454
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.unfold;
+
+import static android.graphics.Color.blue;
+import static android.graphics.Color.green;
+import static android.graphics.Color.red;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+
+/**
+ * Controls background color layer for the unfold animations
+ */
+public class UnfoldBackgroundController {
+
+    private static final int BACKGROUND_LAYER_Z_INDEX = -1;
+
+    private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+    private final float[] mBackgroundColor;
+    private SurfaceControl mBackgroundLayer;
+
+    public UnfoldBackgroundController(
+            @NonNull Context context,
+            @NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+        mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
+        mBackgroundColor = getBackgroundColor(context);
+    }
+
+    /**
+     * Ensures that unfold animation background color layer is present,
+     * @param transaction where we should add the background if it is not added
+     */
+    public void ensureBackground(@NonNull SurfaceControl.Transaction transaction) {
+        if (mBackgroundLayer != null) return;
+
+        SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder()
+                .setName("app-unfold-background")
+                .setCallsite("AppUnfoldTransitionController")
+                .setColorLayer();
+        mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder);
+        mBackgroundLayer = colorLayerBuilder.build();
+
+        transaction
+                .setColor(mBackgroundLayer, mBackgroundColor)
+                .show(mBackgroundLayer)
+                .setLayer(mBackgroundLayer, BACKGROUND_LAYER_Z_INDEX);
+    }
+
+    /**
+     * Ensures that the background is not visible
+     * @param transaction as part of which the removal will happen if needed
+     */
+    public void removeBackground(@NonNull SurfaceControl.Transaction transaction) {
+        if (mBackgroundLayer == null) return;
+        if (mBackgroundLayer.isValid()) {
+            transaction.remove(mBackgroundLayer);
+        }
+        mBackgroundLayer = null;
+    }
+
+    private float[] getBackgroundColor(Context context) {
+        int colorInt = context.getResources().getColor(R.color.unfold_transition_background);
+        return new float[]{
+                (float) red(colorInt) / 255.0F,
+                (float) green(colorInt) / 255.0F,
+                (float) blue(colorInt) / 255.0F
+        };
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java
new file mode 100644
index 0000000..b9b6716
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.util;
+
+import android.view.SurfaceControl;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class that takes care of counter-rotating surfaces during a transition animation.
+ */
+public class CounterRotator {
+    SurfaceControl mSurface = null;
+    ArrayList<SurfaceControl> mRotateChildren = null;
+
+    /** Gets the surface with the counter-rotation. */
+    public SurfaceControl getSurface() {
+        return mSurface;
+    }
+
+    /**
+     * Sets up this rotator.
+     *
+     * @param rotateDelta is the forward rotation change (the rotation the display is making).
+     * @param displayW (and H) Is the size of the rotating display.
+     */
+    public void setup(SurfaceControl.Transaction t, SurfaceControl parent, int rotateDelta,
+            float displayW, float displayH) {
+        if (rotateDelta == 0) return;
+        mRotateChildren = new ArrayList<>();
+        // We want to counter-rotate, so subtract from 4
+        rotateDelta = 4 - (rotateDelta + 4) % 4;
+        mSurface = new SurfaceControl.Builder()
+                .setName("Transition Unrotate")
+                .setContainerLayer()
+                .setParent(parent)
+                .build();
+        // column-major
+        if (rotateDelta == 1) {
+            t.setMatrix(mSurface, 0, 1, -1, 0);
+            t.setPosition(mSurface, displayW, 0);
+        } else if (rotateDelta == 2) {
+            t.setMatrix(mSurface, -1, 0, 0, -1);
+            t.setPosition(mSurface, displayW, displayH);
+        } else if (rotateDelta == 3) {
+            t.setMatrix(mSurface, 0, -1, 1, 0);
+            t.setPosition(mSurface, 0, displayH);
+        }
+        t.show(mSurface);
+    }
+
+    /**
+     * Add a surface that needs to be counter-rotate.
+     */
+    public void addChild(SurfaceControl.Transaction t, SurfaceControl child) {
+        if (mSurface == null) return;
+        t.reparent(child, mSurface);
+        mRotateChildren.add(child);
+    }
+
+    /**
+     * Clean-up. This undoes any reparenting and effectively stops the counter-rotation.
+     */
+    public void cleanUp(SurfaceControl rootLeash) {
+        if (mSurface == null) return;
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        for (int i = mRotateChildren.size() - 1; i >= 0; --i) {
+            t.reparent(mRotateChildren.get(i), rootLeash);
+        }
+        t.remove(mSurface);
+        t.apply();
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
index b63d9ff..4d87ec9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
@@ -43,4 +43,4 @@
     } while (SystemClock.uptimeMillis() - startTime < timeout)
 
     return (false to null)
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
index 322d8b5..ef399e4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt
@@ -22,7 +22,6 @@
 import android.content.Context
 import android.os.ServiceManager
 import android.view.Surface
-import androidx.test.filters.FlakyTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiObject2
@@ -34,7 +33,6 @@
 import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE
 import com.android.server.wm.flicker.repetitions
 import com.android.wm.shell.flicker.helpers.LaunchBubbleHelper
-import org.junit.Test
 import org.junit.runners.Parameterized
 
 /**
@@ -86,14 +84,6 @@
         }
     }
 
-    @FlakyTest
-    @Test
-    fun testAppIsAlwaysVisible() {
-        testSpec.assertLayers {
-            this.isVisible(testApp.component)
-        }
-    }
-
     @FlickerBuilderProvider
     fun buildFlicker(): FlickerBuilder {
         return FlickerBuilder(instrumentation).apply {
@@ -112,6 +102,7 @@
         }
 
         const val FIND_OBJECT_TIMEOUT = 2000L
+        const val WINDOW_UPDAT_TIMEOUT = 2000L
         const val SYSTEM_UI_PACKAGE = SYSTEMUI_PACKAGE
         const val BUBBLE_RES_NAME = "bubble_view"
     }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt
index bfdcb36..aec6aa9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt
@@ -20,6 +20,7 @@
 import android.graphics.Point
 import android.util.DisplayMetrics
 import android.view.WindowManager
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
@@ -28,6 +29,7 @@
 import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import org.junit.runner.RunWith
+import org.junit.Test
 import org.junit.runners.Parameterized
 
 /**
@@ -62,4 +64,12 @@
                 showBubble?.run { drag(dist, 1000) } ?: error("Show bubble not found")
             }
         }
+
+    @FlakyTest
+    @Test
+    fun testAppIsAlwaysVisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp.component)
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
index 42eeadf..9287e17 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.flicker.bubble
 
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
@@ -24,6 +25,7 @@
 import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import org.junit.runner.RunWith
+import org.junit.Test
 import org.junit.runners.Parameterized
 
 /**
@@ -53,7 +55,14 @@
                 val showBubble = device.wait(Until.findObject(
                         By.res("com.android.systemui", "bubble_view")), FIND_OBJECT_TIMEOUT)
                 showBubble?.run { showBubble.click() } ?: error("Bubble notify not found")
-                device.pressBack()
             }
         }
+
+    @FlakyTest
+    @Test
+    fun testAppIsAlwaysVisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp.component)
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
new file mode 100644
index 0000000..3aeb5ad
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.bubble
+
+import android.os.SystemClock
+import androidx.test.filters.FlakyTest
+import androidx.test.filters.RequiresDevice
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import org.junit.runner.RunWith
+import org.junit.Test
+import org.junit.runners.Parameterized
+
+/**
+ * Test launching a new activity from bubble.
+ *
+ * To run this test: `atest WMShellFlickerTests:LaunchBubbleFromLockScreen`
+ *
+ * Actions:
+ *     Launch an bubble from notification on lock screen
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@Group4
+class LaunchBubbleFromLockScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) {
+
+    override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit
+        get() = buildTransition() {
+            setup {
+                eachRun {
+                    addBubbleBtn?.click() ?: error("Bubble widget not found")
+                    device.sleep()
+                    SystemClock.sleep(2000)
+                    device.wakeUp()
+                }
+            }
+            transitions {
+                val notification = device.wait(Until.findObject(
+                    By.text("BubbleChat")), FIND_OBJECT_TIMEOUT)
+                notification?.click() ?: error("Notification not found")
+                instrumentation.getUiAutomation().syncInputTransactions()
+                val showBubble = device.wait(Until.findObject(
+                        By.res("com.android.systemui", "bubble_view")), FIND_OBJECT_TIMEOUT)
+                showBubble?.click() ?: error("Bubble notify not found")
+                instrumentation.getUiAutomation().syncInputTransactions()
+                cancelAllBtn?.click() ?: error("Cancel widget not found")
+            }
+        }
+
+    @FlakyTest
+    @Test
+    fun testAppisVisibleAtEnd() {
+        testSpec.assertLayersEnd {
+            this.isVisible(testApp.component)
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
index 47e8c0c..ed68d47 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt
@@ -16,12 +16,14 @@
 
 package com.android.wm.shell.flicker.bubble
 
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import org.junit.runner.RunWith
+import org.junit.Test
 import org.junit.runners.Parameterized
 
 /**
@@ -45,4 +47,12 @@
                 addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Bubble widget not found")
             }
         }
+
+    @FlakyTest
+    @Test
+    fun testAppIsAlwaysVisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp.component)
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
index 194e28f..edddc1e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt
@@ -17,6 +17,7 @@
 package com.android.wm.shell.flicker.bubble
 
 import android.os.SystemClock
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Until
@@ -25,6 +26,7 @@
 import com.android.server.wm.flicker.annotation.Group4
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import org.junit.runner.RunWith
+import org.junit.Test
 import org.junit.runners.Parameterized
 
 /**
@@ -63,4 +65,12 @@
                 }
             }
         }
+
+    @FlakyTest
+    @Test
+    fun testAppIsAlwaysVisible() {
+        testSpec.assertLayers {
+            this.isVisible(testApp.component)
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index b0312e6..091022a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -793,7 +793,7 @@
     }
 
     @Test
-    public void test_expanded_removeLastBubble_collapsesStack() {
+    public void test_expanded_removeLastBubble_showsOverflowIfNotEmpty() {
         // Setup
         sendUpdatedEntryAtTime(mEntryA1, 1000);
         changeExpandedStateAtTime(true, 2000);
@@ -802,6 +802,21 @@
         // Test
         mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
         verifyUpdateReceived();
+        assertThat(mBubbleData.getOverflowBubbles().size()).isGreaterThan(0);
+        assertSelectionChangedTo(mBubbleData.getOverflow());
+    }
+
+    @Test
+    public void test_expanded_removeLastBubble_collapsesIfOverflowEmpty() {
+        // Setup
+        sendUpdatedEntryAtTime(mEntryA1, 1000);
+        changeExpandedStateAtTime(true, 2000);
+        mBubbleData.setListener(mListener);
+
+        // Test
+        mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NO_BUBBLE_UP);
+        verifyUpdateReceived();
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
         assertExpandedChangedTo(false);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
index 1bb5fd1..2bcc45e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
@@ -56,13 +56,14 @@
         MockitoAnnotations.initMocks(this);
         mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
         mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue,
-                mSurfaceSession);
+                mSurfaceSession, null);
         mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash);
     }
 
     @Test
     public void testActiveDeactivate() {
-        mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct);
+        mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct,
+                true /* reparent */);
         assertThat(mMainStage.isActive()).isTrue();
 
         mMainStage.deactivate(mWct);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
index 3a2516e..838aa81 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
@@ -64,7 +64,7 @@
         MockitoAnnotations.initMocks(this);
         mRootTask = new TestRunningTaskInfoBuilder().build();
         mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks,
-                mSyncQueue, mSurfaceSession);
+                mSyncQueue, mSurfaceSession, null);
         mSideStage.onTaskAppeared(mRootTask, mRootLeash);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index 736566e5..f90af23 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -18,7 +18,6 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
-
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 
@@ -39,6 +38,10 @@
 import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.transition.Transitions;
 
+import java.util.Optional;
+
+import javax.inject.Provider;
+
 public class SplitTestUtils {
 
     static SplitLayout createMockSplitLayout() {
@@ -68,10 +71,11 @@
                 MainStage mainStage, SideStage sideStage, DisplayImeController imeController,
                 DisplayInsetsController insetsController, SplitLayout splitLayout,
                 Transitions transitions, TransactionPool transactionPool,
-                SplitscreenEventLogger logger) {
+                SplitscreenEventLogger logger,
+                Provider<Optional<StageTaskUnfoldController>> unfoldController) {
             super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage,
                     sideStage, imeController, insetsController, splitLayout, transitions,
-                    transactionPool, logger);
+                    transactionPool, logger, unfoldController);
 
             // Prepare default TaskDisplayArea for testing.
             mDisplayAreaInfo = new DisplayAreaInfo(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index 8dce454..05496b0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -74,6 +74,8 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.stubbing.Answer;
 
+import java.util.Optional;
+
 /** Tests for {@link StageCoordinator} */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -106,16 +108,16 @@
         doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire();
         mSplitLayout = SplitTestUtils.createMockSplitLayout();
         mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mock(
-                StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession);
+                StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, null);
         mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
-                StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession);
+                StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, null);
         mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
                 mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
                 mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
                 mTransactionPool,
-                mLogger);
+                mLogger, Optional::empty);
         mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
         doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class))
                 .when(mTransitions).startTransition(anyInt(), any(), any());
@@ -314,7 +316,8 @@
                 mock(SurfaceControl.Transaction.class),
                 mock(SurfaceControl.Transaction.class),
                 mock(Transitions.TransitionFinishCallback.class));
-        mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction());
+        mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction(),
+                true /* includingTopTask */);
     }
 
     private boolean containsSplitExit(@NonNull WindowContainerTransaction wct) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index d930d4ca..aeb2849b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -21,15 +21,19 @@
 
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
 import android.graphics.Rect;
+import android.window.DisplayAreaInfo;
 import android.window.WindowContainerTransaction;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -43,6 +47,7 @@
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -51,29 +56,55 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-/** Tests for {@link StageCoordinator} */
+import java.util.Optional;
+
+import javax.inject.Provider;
+
+/**
+ * Tests for {@link StageCoordinator}
+ */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class StageCoordinatorTests extends ShellTestCase {
-    @Mock private ShellTaskOrganizer mTaskOrganizer;
-    @Mock private SyncTransactionQueue mSyncQueue;
-    @Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
-    @Mock private MainStage mMainStage;
-    @Mock private SideStage mSideStage;
-    @Mock private DisplayImeController mDisplayImeController;
-    @Mock private DisplayInsetsController mDisplayInsetsController;
-    @Mock private Transitions mTransitions;
-    @Mock private TransactionPool mTransactionPool;
-    @Mock private SplitscreenEventLogger mLogger;
+    @Mock
+    private ShellTaskOrganizer mTaskOrganizer;
+    @Mock
+    private SyncTransactionQueue mSyncQueue;
+    @Mock
+    private RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
+    @Mock
+    private MainStage mMainStage;
+    @Mock
+    private SideStage mSideStage;
+    @Mock
+    private StageTaskUnfoldController mMainUnfoldController;
+    @Mock
+    private StageTaskUnfoldController mSideUnfoldController;
+    @Mock
+    private SplitLayout mSplitLayout;
+    @Mock
+    private DisplayImeController mDisplayImeController;
+    @Mock
+    private DisplayInsetsController mDisplayInsetsController;
+    @Mock
+    private Transitions mTransitions;
+    @Mock
+    private TransactionPool mTransactionPool;
+    @Mock
+    private SplitscreenEventLogger mLogger;
+
+    private final Rect mBounds1 = new Rect(10, 20, 30, 40);
+    private final Rect mBounds2 = new Rect(5, 10, 15, 20);
+
     private StageCoordinator mStageCoordinator;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
-                mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
-                mDisplayImeController, mDisplayInsetsController, null /* splitLayout */,
-                mTransitions, mTransactionPool, mLogger);
+        mStageCoordinator = createStageCoordinator(/* splitLayout */ null);
+
+        when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
+        when(mSplitLayout.getBounds2()).thenReturn(mBounds2);
     }
 
     @Test
@@ -82,12 +113,45 @@
 
         mStageCoordinator.moveToSideStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT);
 
-        verify(mMainStage).activate(any(Rect.class), any(WindowContainerTransaction.class));
+        verify(mMainStage).activate(any(Rect.class), any(WindowContainerTransaction.class),
+                eq(true /* includingTopTask */));
         verify(mSideStage).addTask(eq(task), any(Rect.class),
                 any(WindowContainerTransaction.class));
     }
 
     @Test
+    public void testDisplayAreaAppeared_initializesUnfoldControllers() {
+        mStageCoordinator.onDisplayAreaAppeared(mock(DisplayAreaInfo.class));
+
+        verify(mMainUnfoldController).init();
+        verify(mSideUnfoldController).init();
+    }
+
+    @Test
+    public void testLayoutChanged_topLeftSplitPosition_updatesUnfoldStageBounds() {
+        mStageCoordinator = createStageCoordinator(mSplitLayout);
+        mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null);
+        clearInvocations(mMainUnfoldController, mSideUnfoldController);
+
+        mStageCoordinator.onLayoutChanged(mSplitLayout);
+
+        verify(mMainUnfoldController).onLayoutChanged(mBounds2);
+        verify(mSideUnfoldController).onLayoutChanged(mBounds1);
+    }
+
+    @Test
+    public void testLayoutChanged_bottomRightSplitPosition_updatesUnfoldStageBounds() {
+        mStageCoordinator = createStageCoordinator(mSplitLayout);
+        mStageCoordinator.setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, null);
+        clearInvocations(mMainUnfoldController, mSideUnfoldController);
+
+        mStageCoordinator.onLayoutChanged(mSplitLayout);
+
+        verify(mMainUnfoldController).onLayoutChanged(mBounds1);
+        verify(mSideUnfoldController).onLayoutChanged(mBounds2);
+    }
+
+    @Test
     public void testRemoveFromSideStage() {
         final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
 
@@ -131,4 +195,27 @@
         verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(true));
         verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false));
     }
+
+    private StageCoordinator createStageCoordinator(SplitLayout splitLayout) {
+        return new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
+                mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
+                mDisplayImeController, mDisplayInsetsController, splitLayout,
+                mTransitions, mTransactionPool, mLogger, new UnfoldControllerProvider());
+    }
+
+    private class UnfoldControllerProvider implements
+            Provider<Optional<StageTaskUnfoldController>> {
+
+        private boolean isMain = true;
+
+        @Override
+        public Optional<StageTaskUnfoldController> get() {
+            if (isMain) {
+                isMain = false;
+                return Optional.of(mMainUnfoldController);
+            } else {
+                return Optional.of(mSideUnfoldController);
+            }
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index 0916dd1..a5746a4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -23,6 +23,7 @@
 
 import static org.junit.Assume.assumeFalse;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -60,6 +61,7 @@
     @Mock private ShellTaskOrganizer mTaskOrganizer;
     @Mock private StageTaskListener.StageListenerCallbacks mCallbacks;
     @Mock private SyncTransactionQueue mSyncQueue;
+    @Mock private StageTaskUnfoldController mStageTaskUnfoldController;
     @Captor private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor;
     private SurfaceSession mSurfaceSession = new SurfaceSession();
     private SurfaceControl mSurfaceControl;
@@ -74,7 +76,8 @@
                 DEFAULT_DISPLAY,
                 mCallbacks,
                 mSyncQueue,
-                mSurfaceSession);
+                mSurfaceSession,
+                mStageTaskUnfoldController);
         mRootTask = new TestRunningTaskInfoBuilder().build();
         mRootTask.parentTaskId = INVALID_TASK_ID;
         mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
@@ -111,6 +114,28 @@
         verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true));
     }
 
+    @Test
+    public void testTaskAppeared_notifiesUnfoldListener() {
+        final ActivityManager.RunningTaskInfo task =
+                new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
+
+        mStageTaskListener.onTaskAppeared(task, mSurfaceControl);
+
+        verify(mStageTaskUnfoldController).onTaskAppeared(eq(task), eq(mSurfaceControl));
+    }
+
+    @Test
+    public void testTaskVanished_notifiesUnfoldListener() {
+        final ActivityManager.RunningTaskInfo task =
+                new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
+        mStageTaskListener.onTaskAppeared(task, mSurfaceControl);
+        clearInvocations(mStageTaskUnfoldController);
+
+        mStageTaskListener.onTaskVanished(task);
+
+        verify(mStageTaskUnfoldController).onTaskVanished(eq(task));
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void testUnknownTaskVanished() {
         final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
index 36722d9..b866bf9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
@@ -57,18 +57,19 @@
 import android.view.IWindowSession;
 import android.view.InsetsState;
 import android.view.Surface;
-import android.view.SurfaceControl;
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.view.WindowMetrics;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskSnapshot;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.common.HandlerExecutor;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
@@ -93,6 +94,8 @@
     @Mock
     private WindowManager mMockWindowManager;
     @Mock
+    private IconProvider mIconProvider;
+    @Mock
     private TransactionPool mTransactionPool;
 
     private final Handler mTestHandler = new Handler(Looper.getMainLooper());
@@ -105,8 +108,8 @@
         int mAddWindowForTask = 0;
 
         TestStartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor,
-                TransactionPool pool) {
-            super(context, splashScreenExecutor, pool);
+                IconProvider iconProvider, TransactionPool pool) {
+            super(context, splashScreenExecutor, iconProvider, pool);
         }
 
         @Override
@@ -119,10 +122,9 @@
         }
 
         @Override
-        protected void removeWindowSynced(int taskId, SurfaceControl leash, Rect frame,
-                boolean playRevealAnimation) {
+        protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo) {
             // listen for removeView
-            if (mAddWindowForTask == taskId) {
+            if (mAddWindowForTask == removalInfo.taskId) {
                 mAddWindowForTask = 0;
             }
         }
@@ -157,7 +159,8 @@
         doNothing().when(mMockWindowManager).addView(any(), any());
         mTestExecutor = new HandlerExecutor(mTestHandler);
         mStartingSurfaceDrawer = spy(
-                new TestStartingSurfaceDrawer(mTestContext, mTestExecutor, mTransactionPool));
+                new TestStartingSurfaceDrawer(mTestContext, mTestExecutor, mIconProvider,
+                        mTransactionPool));
     }
 
     @Test
@@ -172,9 +175,11 @@
                 eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN));
         assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId);
 
-        mStartingSurfaceDrawer.removeStartingWindow(windowInfo.taskInfo.taskId, null, null, false);
+        StartingWindowRemovalInfo removalInfo = new StartingWindowRemovalInfo();
+        removalInfo.taskId = windowInfo.taskInfo.taskId;
+        mStartingSurfaceDrawer.removeStartingWindow(removalInfo);
         waitHandlerIdle(mTestHandler);
-        verify(mStartingSurfaceDrawer).removeWindowSynced(eq(taskId), any(), any(), eq(false));
+        verify(mStartingSurfaceDrawer).removeWindowSynced(any());
         assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, 0);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
index a098a68..aad9528 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java
@@ -83,8 +83,7 @@
                 createTaskDescription(Color.WHITE, Color.RED, Color.BLUE),
                 0 /* appearance */, windowFlags /* windowFlags */, 0 /* privateWindowFlags */,
                 taskBounds, ORIENTATION_PORTRAIT, ACTIVITY_TYPE_STANDARD,
-                100 /* delayRemovalTime */, new InsetsState(),
-                null /* clearWindow */, new TestShellExecutor());
+                new InsetsState(), null /* clearWindow */, new TestShellExecutor());
     }
 
     private TaskSnapshot createTaskSnapshot(int width, int height, Point taskSize,
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index fea1e15..491f5f5 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -569,6 +569,7 @@
                 "renderthread/DrawFrameTask.cpp",
                 "renderthread/EglManager.cpp",
                 "renderthread/ReliableSurface.cpp",
+                "renderthread/RenderEffectCapabilityQuery.cpp",
                 "renderthread/VulkanManager.cpp",
                 "renderthread/VulkanSurface.cpp",
                 "renderthread/RenderProxy.cpp",
@@ -697,6 +698,7 @@
         "tests/unit/MatrixTests.cpp",
         "tests/unit/OpBufferTests.cpp",
         "tests/unit/PathInterpolatorTests.cpp",
+        "tests/unit/RenderEffectCapabilityQueryTests.cpp",
         "tests/unit/RenderNodeDrawableTests.cpp",
         "tests/unit/RenderNodeTests.cpp",
         "tests/unit/RenderPropertiesTests.cpp",
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 513ad9a..bcfe9c3 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -50,7 +50,8 @@
 bool Properties::skipEmptyFrames = true;
 bool Properties::useBufferAge = true;
 bool Properties::enablePartialUpdates = true;
-bool Properties::enableRenderEffectCache = false;
+// Default true unless otherwise specified in RenderThread Configuration
+bool Properties::enableRenderEffectCache = true;
 
 DebugLevel Properties::debugLevel = kDebugDisabled;
 OverdrawColorSet Properties::overdrawColorSet = OverdrawColorSet::Default;
diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp
index 383c79b..c7d7a17 100644
--- a/libs/hwui/renderthread/EglManager.cpp
+++ b/libs/hwui/renderthread/EglManager.cpp
@@ -28,6 +28,7 @@
 
 #include "Frame.h"
 #include "Properties.h"
+#include "RenderEffectCapabilityQuery.h"
 #include "utils/Color.h"
 #include "utils/StringUtils.h"
 
@@ -148,7 +149,11 @@
     mHasWideColorGamutSupport = EglExtensions.glColorSpace && hasWideColorSpaceExtension;
 
     auto* vendor = reinterpret_cast<const char*>(glGetString(GL_VENDOR));
-    Properties::enableRenderEffectCache = (strcmp(vendor, "Qualcomm") != 0);
+    auto* version = reinterpret_cast<const char*>(glGetString(GL_VERSION));
+    Properties::enableRenderEffectCache = supportsRenderEffectCache(
+        vendor, version);
+    ALOGV("RenderEffectCache supported %d on driver version %s",
+          Properties::enableRenderEffectCache, version);
 }
 
 EGLConfig EglManager::load8BitsConfig(EGLDisplay display, EglManager::SwapBehavior swapBehavior) {
diff --git a/libs/hwui/renderthread/RenderEffectCapabilityQuery.cpp b/libs/hwui/renderthread/RenderEffectCapabilityQuery.cpp
new file mode 100644
index 0000000..a003988
--- /dev/null
+++ b/libs/hwui/renderthread/RenderEffectCapabilityQuery.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <stdio.h>
+#include <string.h>
+#include <utils/Log.h>
+
+bool supportsRenderEffectCache(const char* vendor, const char* version) {
+    if (strcmp(vendor, "Qualcomm") != 0) {
+       return true;
+    }
+
+    int major;
+    int minor;
+    int driverMajor;
+    int driverMinor;
+    int n = sscanf(version,"OpenGL ES %d.%d V@%d.%d",
+                           &major,
+                           &minor,
+                           &driverMajor,
+                           &driverMinor);
+    // Ensure we have parsed the vendor string properly and we have either
+    // a newer major driver version, or the minor version is rev'ed
+    // Based on b/198227600#comment5 it appears that the corresponding fix
+    // is in driver version 571.0
+    return n == 4 && driverMajor >= 571;
+}
\ No newline at end of file
diff --git a/libs/hwui/renderthread/RenderEffectCapabilityQuery.h b/libs/hwui/renderthread/RenderEffectCapabilityQuery.h
new file mode 100644
index 0000000..ea673dd
--- /dev/null
+++ b/libs/hwui/renderthread/RenderEffectCapabilityQuery.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+/**
+ * Verify if the provided vendor and version supports RenderEffect caching
+ * behavior.
+ *
+ * Certain Open GL Driver implementations run into blocking scenarios
+ * with Fence::waitForever without a corresponding signal to unblock
+ * This happens during attempts to cache SkImage instances across frames
+ * especially in circumstances using RenderEffect/SkImageFilter internally.
+ * So detect the corresponding GL Vendor and driver version to determine if
+ * caching SkImage instances across frames is supported.
+ * See b/197263715 & b/193145089
+ * @param vendor Vendor of the GL driver
+ * @param version Version of the GL driver from the given vendor
+ * @return True if a RenderEffect result can be cached across frames,
+ * false otherwise
+ */
+bool supportsRenderEffectCache(const char* vendor, const char* version);
diff --git a/libs/hwui/tests/unit/EglManagerTests.cpp b/libs/hwui/tests/unit/EglManagerTests.cpp
index f7f2406..7f2e158 100644
--- a/libs/hwui/tests/unit/EglManagerTests.cpp
+++ b/libs/hwui/tests/unit/EglManagerTests.cpp
@@ -17,6 +17,7 @@
 #include <gtest/gtest.h>
 
 #include "renderthread/EglManager.h"
+#include "renderthread/RenderEffectCapabilityQuery.h"
 #include "tests/common/TestContext.h"
 
 using namespace android;
@@ -41,4 +42,17 @@
     }
 
     eglManager.destroy();
+}
+
+TEST(EglManager, verifyRenderEffectCacheSupported) {
+    EglManager eglManager;
+    eglManager.initialize();
+    auto* vendor = reinterpret_cast<const char*>(glGetString(GL_VENDOR));
+    auto* version = reinterpret_cast<const char*>(glGetString(GL_VERSION));
+    // Make sure that EglManager initializes Properties::enableRenderEffectCache
+    // based on the given gl vendor and version within EglManager->initialize()
+    bool renderEffectCacheSupported = supportsRenderEffectCache(vendor, version);
+    EXPECT_EQ(renderEffectCacheSupported,
+              Properties::enableRenderEffectCache);
+    eglManager.destroy();
 }
\ No newline at end of file
diff --git a/libs/hwui/tests/unit/RenderEffectCapabilityQueryTests.cpp b/libs/hwui/tests/unit/RenderEffectCapabilityQueryTests.cpp
new file mode 100644
index 0000000..0ee6549
--- /dev/null
+++ b/libs/hwui/tests/unit/RenderEffectCapabilityQueryTests.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gtest/gtest.h>
+#include "renderthread/RenderEffectCapabilityQuery.h"
+#include "tests/common/TestContext.h"
+
+TEST(RenderEffectCapabilityQuery, testSupportedVendor) {
+  ASSERT_TRUE(supportsRenderEffectCache("Google", "OpenGL ES 1.4 V@0.0"));
+}
+
+TEST(RenderEffectCapabilityQuery, testSupportedVendorWithDifferentVersion) {
+  ASSERT_TRUE(supportsRenderEffectCache("Google", "OpenGL ES 1.3 V@571.0"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithSupportedVersion) {
+  ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@571.0"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithSupportedPatchVersion) {
+  ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@571.1"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithNewerThanSupportedMajorVersion) {
+  ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@572.0"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithNewerThanSupportedMinorVersion) {
+  ASSERT_TRUE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.5 V@571.2"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithUnsupportedMajorVersion) {
+  ASSERT_FALSE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.0 V@570.1"));
+}
+
+TEST(RenderEffectCapabilityQuery, testVendorWithUnsupportedVersion) {
+  ASSERT_FALSE(supportsRenderEffectCache("Qualcomm", "OpenGL ES 1.1 V@570.0"));
+}
+
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 8822aea..0450a80 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -1613,7 +1613,9 @@
             AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_LEFT |
             AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_CENTER |
             AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_RIGHT |
-            AudioFormat.CHANNEL_OUT_LOW_FREQUENCY_2;
+            AudioFormat.CHANNEL_OUT_LOW_FREQUENCY_2 |
+            AudioFormat.CHANNEL_OUT_FRONT_WIDE_LEFT |
+            AudioFormat.CHANNEL_OUT_FRONT_WIDE_RIGHT;
 
     // Returns a boolean whether the attributes, format, bufferSizeInBytes, mode allow
     // power saving to be automatically enabled for an AudioTrack. Returns false if
@@ -1787,6 +1789,8 @@
                 | AudioFormat.CHANNEL_OUT_TOP_SIDE_RIGHT);
         put("bottom front", AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_LEFT
                 | AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_RIGHT);
+        put("front wide", AudioFormat.CHANNEL_OUT_FRONT_WIDE_LEFT
+                | AudioFormat.CHANNEL_OUT_FRONT_WIDE_RIGHT);
     }};
 
     /**
@@ -1801,9 +1805,15 @@
             return false;
         }
         final int channelCount = AudioFormat.channelCountFromOutChannelMask(channelConfig);
-        final int channelCountLimit = AudioFormat.isEncodingLinearFrames(encoding)
-                ? AudioSystem.OUT_CHANNEL_COUNT_MAX  // PCM limited to OUT_CHANNEL_COUNT_MAX
-                : AudioSystem.FCC_24;                // Compressed limited to 24 channels
+        final int channelCountLimit;
+        try {
+            channelCountLimit = AudioFormat.isEncodingLinearFrames(encoding)
+                    ? AudioSystem.OUT_CHANNEL_COUNT_MAX  // PCM limited to OUT_CHANNEL_COUNT_MAX
+                    : AudioSystem.FCC_24;                // Compressed limited to 24 channels
+        } catch (IllegalArgumentException iae) {
+            loge("Unsupported encoding " + iae);
+            return false;
+        }
         if (channelCount > channelCountLimit) {
             loge("Channel configuration contains too many channels for encoding "
                     + encoding + "(" + channelCount + " > " + channelCountLimit + ")");
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 7604b360..067f8215 100755
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -38,6 +38,7 @@
 import android.media.ISpatializerCallback;
 import android.media.ISpatializerHeadTrackingModeCallback;
 import android.media.ISpatializerHeadToSoundStagePoseCallback;
+import android.media.ISpatializerOutputCallback;
 import android.media.IVolumeController;
 import android.media.IVolumeController;
 import android.media.PlayerBase;
@@ -449,4 +450,10 @@
     void setSpatializerParameter(int key, in byte[] value);
 
     void getSpatializerParameter(int key, inout byte[] value);
+
+    int getSpatializerOutput();
+
+    void registerSpatializerOutputCallback(in ISpatializerOutputCallback cb);
+
+    void unregisterSpatializerOutputCallback(in ISpatializerOutputCallback cb);
 }
diff --git a/media/java/android/media/ISpatializerOutputCallback.aidl b/media/java/android/media/ISpatializerOutputCallback.aidl
new file mode 100644
index 0000000..57572a8
--- /dev/null
+++ b/media/java/android/media/ISpatializerOutputCallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+/**
+ * AIDL for the AudioService to signal Spatializer output changes.
+ *
+ * {@hide}
+ */
+oneway interface ISpatializerOutputCallback {
+
+    void dispatchSpatializerOutputChanged(int output);
+}
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index 9a82ab1..49477b9 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -28,6 +28,7 @@
 import android.os.Build;
 import android.os.Process;
 import android.os.SystemProperties;
+import android.sysprop.MediaProperties;
 import android.util.Log;
 import android.util.Pair;
 import android.util.Range;
@@ -196,13 +197,20 @@
     private static final Range<Rational> POSITIVE_RATIONALS =
             Range.create(new Rational(1, Integer.MAX_VALUE),
                          new Rational(Integer.MAX_VALUE, 1));
-    private static final Range<Integer> SIZE_RANGE =
-            Process.is64Bit() ? Range.create(1, 32768) : Range.create(1, 4096);
     private static final Range<Integer> FRAME_RATE_RANGE = Range.create(0, 960);
     private static final Range<Integer> BITRATE_RANGE = Range.create(0, 500000000);
     private static final int DEFAULT_MAX_SUPPORTED_INSTANCES = 32;
     private static final int MAX_SUPPORTED_INSTANCES_LIMIT = 256;
 
+    private static final class LazyHolder {
+        private static final Range<Integer> SIZE_RANGE = Process.is64Bit()
+                ? Range.create(1, 32768)
+                : Range.create(1, MediaProperties.resolution_limit_32bit().orElse(4096));
+    }
+    private static Range<Integer> getSizeRange() {
+        return LazyHolder.SIZE_RANGE;
+    }
+
     // found stuff that is not supported by framework (=> this should not happen)
     private static final int ERROR_UNRECOGNIZED   = (1 << 0);
     // found profile/level for which we don't have capability estimates
@@ -2234,12 +2242,12 @@
         private void initWithPlatformLimits() {
             mBitrateRange = BITRATE_RANGE;
 
-            mWidthRange  = SIZE_RANGE;
-            mHeightRange = SIZE_RANGE;
+            mWidthRange  = getSizeRange();
+            mHeightRange = getSizeRange();
             mFrameRateRange = FRAME_RATE_RANGE;
 
-            mHorizontalBlockRange = SIZE_RANGE;
-            mVerticalBlockRange   = SIZE_RANGE;
+            mHorizontalBlockRange = getSizeRange();
+            mVerticalBlockRange   = getSizeRange();
 
             // full positive ranges are supported as these get calculated
             mBlockCountRange      = POSITIVE_INTEGERS;
@@ -2253,7 +2261,7 @@
             mHeightAlignment = 2;
             mBlockWidth = 2;
             mBlockHeight = 2;
-            mSmallerDimensionUpperLimit = SIZE_RANGE.getUpper();
+            mSmallerDimensionUpperLimit = getSizeRange().getUpper();
         }
 
         private @Nullable List<PerformancePoint> getPerformancePoints(Map<String, Object> map) {
@@ -2494,10 +2502,10 @@
                 // codec supports profiles that we don't know.
                 // Use supplied values clipped to platform limits
                 if (widths != null) {
-                    mWidthRange = SIZE_RANGE.intersect(widths);
+                    mWidthRange = getSizeRange().intersect(widths);
                 }
                 if (heights != null) {
-                    mHeightRange = SIZE_RANGE.intersect(heights);
+                    mHeightRange = getSizeRange().intersect(heights);
                 }
                 if (counts != null) {
                     mBlockCountRange = POSITIVE_INTEGERS.intersect(
diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java
index 16543b0..08a648a 100644
--- a/media/java/android/media/MediaFormat.java
+++ b/media/java/android/media/MediaFormat.java
@@ -744,6 +744,19 @@
     public static final String KEY_CHANNEL_MASK = "channel-mask";
 
     /**
+     * A key describing the maximum number of channels that can be output by an audio decoder.
+     * By default, the decoder will output the same number of channels as present in the encoded
+     * stream, if supported. Set this value to limit the number of output channels, and use
+     * the downmix information in the stream, if available.
+     * <p>Values larger than the number of channels in the content to decode behave like the number
+     * of channels in the content (if applicable), for instance passing 99 for a 5.1 audio stream
+     * behaves like passing 6.
+     * <p>This key is only used during decoding.
+     */
+    public static final String KEY_MAX_OUTPUT_CHANNEL_COUNT =
+            "max-output-channel_count";
+
+    /**
      * A key describing the number of frames to trim from the start of the decoded audio stream.
      * The associated value is an integer.
      */
diff --git a/media/java/android/media/MediaMetadataRetriever.java b/media/java/android/media/MediaMetadataRetriever.java
index 458821e..60d21c0 100644
--- a/media/java/android/media/MediaMetadataRetriever.java
+++ b/media/java/android/media/MediaMetadataRetriever.java
@@ -1073,16 +1073,24 @@
     @UnsupportedAppUsage
     private native byte[] getEmbeddedPicture(int pictureType);
 
+    /**
+     * Releases any acquired resources. Call it when done with the object.
+     *
+     * @throws IOException When an {@link IOException} is thrown while closing a {@link
+     * MediaDataSource} passed to {@link #setDataSource(MediaDataSource)}.
+     */
     @Override
-    public void close() {
+    public void close() throws IOException {
         release();
     }
 
     /**
-     * Call it when one is done with the object. This method releases the memory
-     * allocated internally.
+     * Releases any acquired resources. Call it when done with the object.
+     *
+     * @throws IOException When an {@link IOException} is thrown while closing a {@link
+     * MediaDataSource} passed to {@link #setDataSource(MediaDataSource)}.
      */
-    public native void release();
+    public native void release() throws IOException;
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private native void native_setup();
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java
index 8b1624b..e6fff39 100644
--- a/media/java/android/media/Spatializer.java
+++ b/media/java/android/media/Spatializer.java
@@ -18,6 +18,7 @@
 
 import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -63,7 +64,9 @@
     /**
      * Returns whether spatialization is enabled or not.
      * A false value can originate for instance from the user electing to
-     * disable the feature.<br>
+     * disable the feature, or when the feature is not supported on the device (indicated
+     * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}).
+     * <br>
      * Note that this state reflects a platform-wide state of the "desire" to use spatialization,
      * but availability of the audio processing is still dictated by the compatibility between
      * the effect and the hardware configuration, as indicated by {@link #isAvailable()}.
@@ -85,7 +88,10 @@
      * incompatible with sound spatialization, such as playback on a monophonic speaker.<br>
      * Note that spatialization can be available, but disabled by the user, in which case this
      * method would still return {@code true}, whereas {@link #isEnabled()}
-     * would return {@code false}.
+     * would return {@code false}.<br>
+     * Also when the feature is not supported on the device (indicated
+     * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}),
+     * the return value will be false.
      * @return {@code true} if the spatializer effect is available and capable
      *         of processing the audio for the current configuration of the device,
      *         {@code false} otherwise.
@@ -293,6 +299,24 @@
                 @HeadTrackingModeSet int mode);
     }
 
+
+    /**
+     * @hide
+     * An interface to be notified of changes to the output stream used by the spatializer
+     * effect.
+     * @see #getOutput()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    public interface OnSpatializerOutputChangedListener {
+        /**
+         * Called when the id of the output stream of the spatializer effect changed.
+         * @param spatializer the {@code Spatializer} instance whose output is updated
+         * @param output the id of the output stream, or 0 when there is no spatializer output
+         */
+        void onSpatializerOutputChanged(@NonNull Spatializer spatializer,
+                @IntRange(from = 0) int output);
+    }
+
     /**
      * @hide
      * An interface to be notified of updates to the head to soundstage pose, as represented by the
@@ -839,6 +863,73 @@
         }
     }
 
+    /**
+     * @hide
+     * Returns the id of the output stream used for the spatializer effect playback
+     * @return id of the output stream, or 0 if no spatializer playback is active
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public @IntRange(from = 0) int getOutput() {
+        try {
+            return mAm.getService().getSpatializerOutput();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error calling getSpatializerOutput", e);
+            return 0;
+        }
+    }
+
+    /**
+     * @hide
+     * Sets the listener to receive spatializer effect output updates
+     * @param executor the {@code Executor} handling the callbacks
+     * @param listener the listener to register
+     * @see #clearOnSpatializerOutputChangedListener()
+     * @see #getOutput()
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void setOnSpatializerOutputChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnSpatializerOutputChangedListener listener) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+        synchronized (mOutputListenerLock) {
+            if (mOutputListener != null) {
+                throw new IllegalStateException("Trying to overwrite existing listener");
+            }
+            mOutputListener =
+                    new ListenerInfo<OnSpatializerOutputChangedListener>(listener, executor);
+            mOutputDispatcher = new SpatializerOutputDispatcherStub();
+            try {
+                mAm.getService().registerSpatializerOutputCallback(mOutputDispatcher);
+            } catch (RemoteException e) {
+                mOutputListener = null;
+                mOutputDispatcher = null;
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Clears the listener for spatializer effect output updates
+     * @see #setOnSpatializerOutputChangedListener(Executor, OnSpatializerOutputChangedListener)
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS)
+    public void clearOnSpatializerOutputChangedListener() {
+        synchronized (mOutputListenerLock) {
+            if (mOutputDispatcher == null) {
+                throw (new IllegalStateException("No listener to clear"));
+            }
+            try {
+                mAm.getService().unregisterSpatializerOutputCallback(mOutputDispatcher);
+            } catch (RemoteException e) { }
+            mOutputListener = null;
+            mOutputDispatcher = null;
+        }
+    }
+
     //-----------------------------------------------------------------------------
     // callback helper definitions
 
@@ -964,4 +1055,35 @@
             }
         }
     }
+
+    //-----------------------------------------------------------------------------
+    // output callback management and stub
+    private final Object mOutputListenerLock = new Object();
+    /**
+     * Listener for output updates
+     */
+    @GuardedBy("mOutputListenerLock")
+    private @Nullable ListenerInfo<OnSpatializerOutputChangedListener> mOutputListener;
+    @GuardedBy("mOutputListenerLock")
+    private @Nullable SpatializerOutputDispatcherStub mOutputDispatcher;
+
+    private final class SpatializerOutputDispatcherStub
+            extends ISpatializerOutputCallback.Stub {
+
+        @Override
+        public void dispatchSpatializerOutputChanged(int output) {
+            // make a copy of ref to listener so callback is not executed under lock
+            final ListenerInfo<OnSpatializerOutputChangedListener> listener;
+            synchronized (mOutputListenerLock) {
+                listener = mOutputListener;
+            }
+            if (listener == null) {
+                return;
+            }
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                listener.mExecutor.execute(() -> listener.mListener
+                        .onSpatializerOutputChanged(Spatializer.this, output));
+            }
+        }
+    }
 }
diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java
index 20fa53d..bc00c40 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/MediaSession.java
@@ -657,8 +657,9 @@
             parcel.setDataPosition(0);
             Bundle out = parcel.readBundle(null);
 
-            // Calling Bundle#size() will trigger Bundle#unparcel().
-            out.size();
+            for (String key : out.keySet()) {
+                out.get(key);
+            }
         } catch (BadParcelableException e) {
             Log.d(TAG, "Custom parcelable in bundle.", e);
             return true;
diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java
index 30a14c8..a0f6fb9 100644
--- a/media/java/android/media/tv/TvContract.java
+++ b/media/java/android/media/tv/TvContract.java
@@ -1658,6 +1658,25 @@
          */
         String COLUMN_CONTENT_ID = "content_id";
 
+        /**
+         * The start time of this TV program, in milliseconds since the epoch.
+         *
+         * <p>Should be empty if this program is not live.
+         *
+         * <p>Type: INTEGER (long)
+         * @see #COLUMN_LIVE
+         */
+        String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis";
+
+        /**
+         * The end time of this TV program, in milliseconds since the epoch.
+         *
+         * <p>Should be empty if this program is not live.
+         *
+         * <p>Type: INTEGER (long)
+         * @see #COLUMN_LIVE
+         */
+        String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
     }
 
     /** Column definitions for the TV channels table. */
diff --git a/media/java/android/media/tv/interactive/ITvIAppManager.aidl b/media/java/android/media/tv/interactive/ITvIAppManager.aidl
new file mode 100644
index 0000000..ef7b1cd
--- /dev/null
+++ b/media/java/android/media/tv/interactive/ITvIAppManager.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.interactive;
+
+/**
+ * Interface to the TV interactive app service.
+ * @hide
+ */
+interface ITvIAppManager {
+    void startIApp(in IBinder sessionToken, int userId);
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/interactive/ITvIAppSession.aidl b/media/java/android/media/tv/interactive/ITvIAppSession.aidl
new file mode 100644
index 0000000..970afc6
--- /dev/null
+++ b/media/java/android/media/tv/interactive/ITvIAppSession.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.interactive;
+
+/**
+ * Sub-interface of ITvIAppService which is created per session and has its own context.
+ * @hide
+ */
+oneway interface ITvIAppSession {
+    void startIApp();
+}
\ No newline at end of file
diff --git a/media/java/android/media/tv/interactive/TvIAppManager.java b/media/java/android/media/tv/interactive/TvIAppManager.java
new file mode 100644
index 0000000..8f01f79
--- /dev/null
+++ b/media/java/android/media/tv/interactive/TvIAppManager.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.interactive;
+
+import android.annotation.SystemService;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * Central system API to the overall TV interactive application framework (TIAF) architecture, which
+ * arbitrates interaction between applications and interactive apps.
+ * @hide
+ */
+@SystemService("tv_interactive_app")
+public final class TvIAppManager {
+    private static final String TAG = "TvIAppManager";
+
+    private final ITvIAppManager mService;
+    private final int mUserId;
+
+    public TvIAppManager(ITvIAppManager service, int userId) {
+        mService = service;
+        mUserId = userId;
+    }
+
+    /**
+     * The Session provides the per-session functionality of interactive app.
+     */
+    public static final class Session {
+        private final IBinder mToken;
+        private final ITvIAppManager mService;
+        private final int mUserId;
+
+        private Session(IBinder token, ITvIAppManager service, int userId) {
+            mToken = token;
+            mService = service;
+            mUserId = userId;
+        }
+
+        void startIApp() {
+            if (mToken == null) {
+                Log.w(TAG, "The session has been already released");
+                return;
+            }
+            try {
+                mService.startIApp(mToken, mUserId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+}
diff --git a/media/java/android/media/tv/interactive/TvIAppService.java b/media/java/android/media/tv/interactive/TvIAppService.java
new file mode 100644
index 0000000..2a851f39
--- /dev/null
+++ b/media/java/android/media/tv/interactive/TvIAppService.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.interactive;
+
+import android.app.Service;
+import android.view.KeyEvent;
+
+/**
+ * The TvIAppService class represents a TV interactive applications RTE.
+ * @hide
+ */
+public abstract class TvIAppService extends Service {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "TvIAppService";
+
+    /**
+     * Base class for derived classes to implement to provide a TV interactive app session.
+     */
+    public abstract static class Session implements KeyEvent.Callback {
+        /**
+         * Starts TvIAppService session.
+         */
+        public void onStartIApp() {
+        }
+
+        void startIApp() {
+            onStartIApp();
+        }
+    }
+
+    /**
+     * Implements the internal ITvIAppSession interface.
+     */
+    public static class ITvIAppSessionWrapper extends ITvIAppSession.Stub {
+        private final Session mSessionImpl;
+
+        public ITvIAppSessionWrapper(Session mSessionImpl) {
+            this.mSessionImpl = mSessionImpl;
+        }
+
+        @Override
+        public void startIApp() {
+            mSessionImpl.startIApp();
+        }
+    }
+}
diff --git a/media/java/android/media/tv/interactive/TvIAppView.java b/media/java/android/media/tv/interactive/TvIAppView.java
new file mode 100644
index 0000000..9af9dae
--- /dev/null
+++ b/media/java/android/media/tv/interactive/TvIAppView.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.tv.interactive;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.ViewGroup;
+
+/**
+ * Displays contents of interactive TV applications.
+ * @hide
+ */
+public class TvIAppView extends ViewGroup {
+    private static final String TAG = "TvIAppView";
+    private static final boolean DEBUG = false;
+
+    // TODO: create session
+    private TvIAppManager.Session mSession;
+
+    public TvIAppView(Context context) {
+        super(context, /* attrs = */null, /* defStyleAttr = */0);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        if (DEBUG) {
+            Log.d(TAG,
+                    "onLayout (left=" + l + ", top=" + t + ", right=" + r + ", bottom=" + b + ",)");
+        }
+    }
+
+    /**
+     * Starts the interactive application.
+     */
+    public void startIApp() {
+        if (DEBUG) {
+            Log.d(TAG, "start");
+        }
+        if (mSession != null) {
+            mSession.startIApp();
+        }
+    }
+}
diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
index d82b00d..62c313a 100644
--- a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
+++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothMidiDevice.java
@@ -211,10 +211,10 @@
         }
 
         @Override
-        public void writePacket(byte[] buffer, int count) {
+        public boolean writePacket(byte[] buffer, int count) {
             if (mCharacteristic == null) {
                 Log.w(TAG, "not ready to send packet yet");
-                return;
+                return false;
             }
 
             // Cache the previous buffer for writePacket so buffers aren't
@@ -223,12 +223,22 @@
                 mCachedBuffer = new byte[count];
             }
             System.arraycopy(buffer, 0, mCachedBuffer, 0, count);
-            mCharacteristic.setValue(mCachedBuffer);
+            if (!mCharacteristic.setValue(mCachedBuffer)) {
+                Log.w(TAG, "could not set characteristic value");
+                return false;
+            }
+
             if (DEBUG) {
                 logByteArray("Sent ", mCharacteristic.getValue(), 0,
                        mCharacteristic.getValue().length);
             }
-            mBluetoothGatt.writeCharacteristic(mCharacteristic);
+
+            if (!mBluetoothGatt.writeCharacteristic(mCharacteristic)) {
+                Log.w(TAG, "could not write characteristic to Bluetooth GATT");
+                return false;
+            }
+
+            return true;
         }
     }
 
diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketEncoder.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketEncoder.java
index 92585ea..3ef9f4c 100644
--- a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketEncoder.java
+++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/BluetoothPacketEncoder.java
@@ -17,11 +17,14 @@
 package com.android.bluetoothmidiservice;
 
 import android.media.midi.MidiReceiver;
+import android.util.Log;
 
 import com.android.internal.midi.MidiConstants;
 import com.android.internal.midi.MidiFramer;
 
 import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Queue;
 
 /**
  * This class accumulates MIDI messages to form a MIDI packet.
@@ -52,6 +55,8 @@
 
     private final Object mLock = new Object();
 
+    private Queue<byte[]> mFailedToSendQueue = new ArrayDeque<byte[]>();
+
     // This receives normalized data from mMidiFramer and accumulates it into a packet buffer
     private final MidiReceiver mFramedDataReceiver = new MidiReceiver() {
         @Override
@@ -59,6 +64,8 @@
                 throws IOException {
 
             synchronized (mLock) {
+                flushFailedToSendQueueLocked();
+
                 int milliTimestamp = (int)(timestamp / MILLISECOND_NANOS) & MILLISECOND_MASK;
                 byte status = msg[offset];
                 boolean isSysExStart = (status == MidiConstants.STATUS_SYSTEM_EXCLUSIVE);
@@ -227,11 +234,44 @@
         }
 
         if (mAccumulatedBytes > 0) {
-            mPacketReceiver.writePacket(mAccumulationBuffer, mAccumulatedBytes);
+            boolean wasSendSuccessful = mPacketReceiver.writePacket(mAccumulationBuffer,
+                    mAccumulatedBytes);
+
+            if (!wasSendSuccessful) {
+                byte[] failedBuffer = new byte[mAccumulatedBytes];
+                System.arraycopy(mAccumulationBuffer, 0, failedBuffer, 0, mAccumulatedBytes);
+                mFailedToSendQueue.add(failedBuffer);
+                Log.d(TAG, "Enqueued data into failed queue.");
+            }
+
             mAccumulatedBytes = 0;
             mPacketTimestamp = 0;
             mRunningStatus = 0;
-            mWritePending = true;
+            mWritePending = wasSendSuccessful;
+        }
+    }
+
+    private void flushFailedToSendQueueLocked() {
+        while (!mFailedToSendQueue.isEmpty()) {
+            while (mWritePending) {
+                try {
+                    mLock.wait();
+                } catch (InterruptedException e) {
+                    // try again
+                    continue;
+                }
+            }
+            byte[] currentBuffer = mFailedToSendQueue.element();
+
+            boolean wasSendSuccessful = mPacketReceiver.writePacket(currentBuffer,
+                    currentBuffer.length);
+            mWritePending = wasSendSuccessful;
+            if (wasSendSuccessful) {
+                mFailedToSendQueue.remove();
+                Log.d(TAG, "Dequeued data from failed queue.");
+            } else {
+                return;
+            }
         }
     }
 }
diff --git a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketEncoder.java b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketEncoder.java
index 12c8b9b..a2c880ac 100644
--- a/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketEncoder.java
+++ b/media/packages/BluetoothMidiService/src/com/android/bluetoothmidiservice/PacketEncoder.java
@@ -30,8 +30,9 @@
         /** Called to write an accumulated packet.
          * @param buffer the packet buffer to write
          * @param count the number of bytes in the packet buffer to write
+         * @return whether the operation was successful
          */
-        public void writePacket(byte[] buffer, int count);
+        boolean writePacket(byte[] buffer, int count);
     }
 
     /**
diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiCodecTest.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiCodecTest.java
index 3285f59..8572efd 100644
--- a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiCodecTest.java
+++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiCodecTest.java
@@ -114,7 +114,7 @@
         // TODO Should this block?
         // Store the packets and then write them from a periodic task.
         @Override
-        public void writePacket(byte[] buffer, int count) {
+        public boolean writePacket(byte[] buffer, int count) {
             Log.d(TAG, "writePacket() passed " + MidiFramer.formatMidiData(buffer, 0, count));
             byte[] packet = new byte[count];
             System.arraycopy(buffer, 0, packet, 0, count);
@@ -124,6 +124,7 @@
                 assertEquals(null, e);
             }
             Log.d(TAG, "writePacket() returns");
+            return true;
         }
 
         void test(final byte[][] midi)
diff --git a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiEncoderTest.java b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiEncoderTest.java
index d48b10a..1f432f6 100644
--- a/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiEncoderTest.java
+++ b/media/packages/BluetoothMidiService/tests/unit/src/com/android/bluetoothmidiservice/BluetoothMidiEncoderTest.java
@@ -42,11 +42,12 @@
     static class AccumulatingPacketReceiver implements PacketEncoder.PacketReceiver {
         ArrayList<byte[]> mBuffers = new ArrayList<byte[]>();
 
-        public void writePacket(byte[] buffer, int count) {
+        public boolean writePacket(byte[] buffer, int count) {
             byte[] actualRow = new byte[count];
             Log.d(TAG, "writePacket() passed " + MidiFramer.formatMidiData(buffer, 0, count));
             System.arraycopy(buffer, 0, actualRow, 0, count);
             mBuffers.add(actualRow);
+            return true;
         }
 
         byte[][] getBuffers() {
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaMetadataTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaMetadataTest.java
index 380de9c..da106be 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaMetadataTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/MediaMetadataTest.java
@@ -19,120 +19,138 @@
 import android.media.MediaMetadataRetriever;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.Suppress;
 import android.util.Log;
 
 import com.android.mediaframeworktest.MediaNames;
 import com.android.mediaframeworktest.MediaProfileReader;
-/**
- * This metadata test suite test the basic functionality of the 
- * MediaMetadataRetriever
- * 
- */
+
+import java.io.IOException;
+
+/** This metadata test suite test the basic functionality of the MediaMetadataRetriever */
 public class MediaMetadataTest extends AndroidTestCase {
-    
+
     private static final String TAG = "MediaMetadataTest";
 
-    public static enum METADATA_EXPECTEDRESULT{
-        FILE_PATH,CD_TRACK, ALBUM,
-        ARTIST, AUTHOR, COMPOSER,
-        DATE, GENRE, TITLE,
-        YEAR, DURATION, NUM_TRACKS, WRITER
+    public enum METADATA_EXPECTEDRESULT {
+        FILE_PATH,
+        CD_TRACK,
+        ALBUM,
+        ARTIST,
+        AUTHOR,
+        COMPOSER,
+        DATE,
+        GENRE,
+        TITLE,
+        YEAR,
+        DURATION,
+        NUM_TRACKS,
+        WRITER
     }
-    
-    public static enum MP3_TEST_FILE{
-        ID3V1V2, ID3V2, ID3V1
+
+    public enum MP3_TEST_FILE {
+        ID3V1V2,
+        ID3V2,
+        ID3V1
     }
-    
+
     public static METADATA_EXPECTEDRESULT meta;
     public static MP3_TEST_FILE mp3_test_file;
-   
+
     @MediumTest
     public static void testID3V1V2Metadata() throws Exception {
         validateMetatData(mp3_test_file.ID3V1V2.ordinal(), MediaNames.META_DATA_MP3);
     }
-    
+
     @MediumTest
     public static void testID3V2Metadata() throws Exception {
         validateMetatData(mp3_test_file.ID3V2.ordinal(), MediaNames.META_DATA_MP3);
     }
-    
+
     @MediumTest
     public static void testID3V1Metadata() throws Exception {
         validateMetatData(mp3_test_file.ID3V1.ordinal(), MediaNames.META_DATA_MP3);
     }
 
-    private static void validateMetatData(int fileIndex, String meta_data_file[][]) {
-        Log.v(TAG, "filePath = "+ meta_data_file[fileIndex][0]);
-        if ((meta_data_file[fileIndex][0].endsWith("wma") && !MediaProfileReader.getWMAEnable()) ||
-            (meta_data_file[fileIndex][0].endsWith("wmv") && !MediaProfileReader.getWMVEnable())) {
+    private static void validateMetatData(int fileIndex, String[][] metadataFile)
+            throws IOException {
+        Log.v(TAG, "filePath = " + metadataFile[fileIndex][0]);
+        if ((metadataFile[fileIndex][0].endsWith("wma") && !MediaProfileReader.getWMAEnable())
+                || (metadataFile[fileIndex][0].endsWith("wmv")
+                        && !MediaProfileReader.getWMVEnable())) {
             return;
         }
         String value = null;
         MediaMetadataRetriever retriever = new MediaMetadataRetriever();
         try {
-            retriever.setDataSource(meta_data_file[fileIndex][0]);
-        } catch(Exception e) {
-            Log.v(TAG, "Failed: "+meta_data_file[fileIndex][0] + " " + e.toString());
-            //Set the test case failure whenever it failed to setDataSource
+            retriever.setDataSource(metadataFile[fileIndex][0]);
+        } catch (Exception e) {
+            Log.v(TAG, "Failed: " + metadataFile[fileIndex][0] + " " + e.toString());
+            // Set the test case failure whenever it failed to setDataSource
             assertTrue("Failed to setDataSource ", false);
         }
-        
-        //METADATA_KEY_CD_TRACK_NUMBER should return the TCRK value
+
+        // METADATA_KEY_CD_TRACK_NUMBER should return the TCRK value
         value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER);
         Log.v(TAG, "CD_TRACK_NUMBER : " + value);
-        assertEquals(TAG, meta_data_file[fileIndex][meta.CD_TRACK.ordinal()], value);
-       
+        assertEquals(TAG, metadataFile[fileIndex][meta.CD_TRACK.ordinal()], value);
+
         value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
-        Log.v(TAG, "Album : "+ value);
-        assertEquals(TAG, meta_data_file[fileIndex][meta.ALBUM.ordinal()], value); 
-        
+        Log.v(TAG, "Album : " + value);
+        assertEquals(TAG, metadataFile[fileIndex][meta.ALBUM.ordinal()], value);
+
         value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
-        Log.v(TAG, "Artist : "+ value);
-        assertEquals(TAG, meta_data_file[fileIndex][meta.ARTIST.ordinal()], value);
-        
+        Log.v(TAG, "Artist : " + value);
+        assertEquals(TAG, metadataFile[fileIndex][meta.ARTIST.ordinal()], value);
+
         value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_AUTHOR);
-        Log.v(TAG, "Author : "+ value);
-        assertEquals(TAG, meta_data_file[fileIndex][meta.AUTHOR.ordinal()], value);
-        
+        Log.v(TAG, "Author : " + value);
+        assertEquals(TAG, metadataFile[fileIndex][meta.AUTHOR.ordinal()], value);
+
         value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER);
-        Log.v(TAG, "Composer : "+ value);
-        assertEquals(TAG, meta_data_file[fileIndex][meta.COMPOSER.ordinal()], value);
-        
+        Log.v(TAG, "Composer : " + value);
+        assertEquals(TAG, metadataFile[fileIndex][meta.COMPOSER.ordinal()], value);
+
         value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
-        Log.v(TAG, "Date : "+ value);
-        assertEquals(TAG, meta_data_file[fileIndex][meta.DATE.ordinal()], value);
-        
+        Log.v(TAG, "Date : " + value);
+        assertEquals(TAG, metadataFile[fileIndex][meta.DATE.ordinal()], value);
+
         value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE);
-        Log.v(TAG, "Genre : "+ value);
-        assertEquals(TAG, meta_data_file[fileIndex][meta.GENRE.ordinal()], value);
-        
+        Log.v(TAG, "Genre : " + value);
+        assertEquals(TAG, metadataFile[fileIndex][meta.GENRE.ordinal()], value);
+
         value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
-        Log.v(TAG, "Title : "+ value);
-        assertEquals(TAG, meta_data_file[fileIndex][meta.TITLE.ordinal()], value);
-        
+        Log.v(TAG, "Title : " + value);
+        assertEquals(TAG, metadataFile[fileIndex][meta.TITLE.ordinal()], value);
+
         value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR);
-        Log.v(TAG, "Year : "+ value);
-        assertEquals(TAG, meta_data_file[fileIndex][meta.YEAR.ordinal()], value);
-        
+        Log.v(TAG, "Year : " + value);
+        assertEquals(TAG, metadataFile[fileIndex][meta.YEAR.ordinal()], value);
+
         value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
-        Log.v(TAG, "Expected = " + meta_data_file[fileIndex][meta.DURATION.ordinal()] + "reult = " + value);
+        Log.v(
+                TAG,
+                "Expected = "
+                        + metadataFile[fileIndex][meta.DURATION.ordinal()]
+                        + "reult = "
+                        + value);
         // Only require that the returned duration is within 100ms of the expected
         // one as PV and stagefright differ slightly in their implementation.
-        assertTrue(TAG, Math.abs(Integer.parseInt(
-                        meta_data_file[fileIndex][meta.DURATION.ordinal()])
-                            - Integer.parseInt(value)) < 100);
-        
-        //METADATA_KEY_NUM_TRACKS should return the total number of tracks in the media
-        //include the video and audio
-        value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS);
-        Log.v(TAG, "Track : "+ value);
-        assertEquals(TAG,meta_data_file[fileIndex][meta.NUM_TRACKS.ordinal()], value);
-     
-        value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_WRITER);
-        Log.v(TAG, "Writer : "+ value);
-        assertEquals(TAG,meta_data_file[fileIndex][meta.WRITER.ordinal()], value);
+        int durationDifferenceMs =
+                Math.abs(
+                        Integer.parseInt(metadataFile[fileIndex][meta.DURATION.ordinal()])
+                                - Integer.parseInt(value));
+        assertTrue(TAG, durationDifferenceMs < 100);
 
-        retriever.release();        
+        // METADATA_KEY_NUM_TRACKS should return the total number of tracks in the media
+        // include the video and audio
+        value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS);
+        Log.v(TAG, "Track : " + value);
+        assertEquals(TAG, metadataFile[fileIndex][meta.NUM_TRACKS.ordinal()], value);
+
+        value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_WRITER);
+        Log.v(TAG, "Writer : " + value);
+        assertEquals(TAG, metadataFile[fileIndex][meta.WRITER.ordinal()], value);
+
+        retriever.release();
     }
 }
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaMetadataRetrieverTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaMetadataRetrieverTest.java
index 8eb75f3..bdca474 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaMetadataRetrieverTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaMetadataRetrieverTest.java
@@ -16,19 +16,23 @@
 
 package com.android.mediaframeworktest.unit;
 
-import android.util.Log;
-import android.media.MediaMetadataRetriever;
 import android.graphics.Bitmap;
-import java.io.FileOutputStream;
+import android.media.MediaMetadataRetriever;
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
 import com.android.mediaframeworktest.MediaNames;
 import com.android.mediaframeworktest.MediaProfileReader;
-import android.test.suitebuilder.annotation.*;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
 
 public class MediaMetadataRetrieverTest extends AndroidTestCase {
-    
-    private static final String TAG         = "MediaMetadataRetrieverTest";
-   
+
+    private static final String TAG = "MediaMetadataRetrieverTest";
+
     // Test album art extraction.
     @MediumTest
     public static void testGetEmbeddedPicture() throws Exception {
@@ -40,10 +44,12 @@
         for (int i = 0, n = MediaNames.ALBUMART_TEST_FILES.length; i < n; ++i) {
             try {
                 Log.v(TAG, "File " + i + ": " + MediaNames.ALBUMART_TEST_FILES[i]);
-                if ((MediaNames.ALBUMART_TEST_FILES[i].endsWith(".wma") && !supportWMA) ||
-                    (MediaNames.ALBUMART_TEST_FILES[i].endsWith(".wmv") && !supportWMV)
-                   ) {
-                    Log.v(TAG, "windows media is not supported and thus we will skip the test for this file");
+                if ((MediaNames.ALBUMART_TEST_FILES[i].endsWith(".wma") && !supportWMA)
+                        || (MediaNames.ALBUMART_TEST_FILES[i].endsWith(".wmv") && !supportWMV)) {
+                    Log.v(
+                            TAG,
+                            "windows media is not supported and thus we will skip the test for this"
+                                    + " file");
                     continue;
                 }
                 retriever.setDataSource(MediaNames.ALBUMART_TEST_FILES[i]);
@@ -52,15 +58,18 @@
                 // TODO:
                 // A better test would be to compare the retrieved album art with the
                 // known result.
-                if (albumArt == null) {  // Do we have expect in JUnit?
-                    Log.e(TAG, "Fails to get embedded picture for " + MediaNames.ALBUMART_TEST_FILES[i]);
+                if (albumArt == null) { // Do we have expect in JUnit?
+                    Log.e(
+                            TAG,
+                            "Fails to get embedded picture for "
+                                    + MediaNames.ALBUMART_TEST_FILES[i]);
                     hasFailed = true;
                 }
-            } catch(Exception e) {
+            } catch (Exception e) {
                 Log.e(TAG, "Fails to setDataSource for " + MediaNames.ALBUMART_TEST_FILES[i]);
                 hasFailed = true;
             }
-            Thread.yield();  // Don't be evil
+            Thread.yield(); // Don't be evil
         }
         retriever.release();
         Log.v(TAG, "testGetEmbeddedPicture completes.");
@@ -76,61 +85,82 @@
         boolean hasFailed = false;
         Log.v(TAG, "Thumbnail processing starts");
         long startedAt = System.currentTimeMillis();
-        for(int i = 0, n = MediaNames.THUMBNAIL_METADATA_TEST_FILES.length; i < n; ++i) {
+        for (int i = 0, n = MediaNames.THUMBNAIL_METADATA_TEST_FILES.length; i < n; ++i) {
             try {
                 Log.v(TAG, "File " + i + ": " + MediaNames.THUMBNAIL_METADATA_TEST_FILES[i]);
-                if ((MediaNames.THUMBNAIL_METADATA_TEST_FILES[i].endsWith(".wma") && !supportWMA) ||
-                    (MediaNames.THUMBNAIL_METADATA_TEST_FILES[i].endsWith(".wmv") && !supportWMV)
-                   ) {
-                    Log.v(TAG, "windows media is not supported and thus we will skip the test for this file");
+                if ((MediaNames.THUMBNAIL_METADATA_TEST_FILES[i].endsWith(".wma") && !supportWMA)
+                        || (MediaNames.THUMBNAIL_METADATA_TEST_FILES[i].endsWith(".wmv")
+                                && !supportWMV)) {
+                    Log.v(
+                            TAG,
+                            "windows media is not supported and thus we will skip the test for this"
+                                    + " file");
                     continue;
                 }
                 retriever.setDataSource(MediaNames.THUMBNAIL_METADATA_TEST_FILES[i]);
                 Bitmap bitmap = retriever.getFrameAtTime(-1);
                 assertTrue(bitmap != null);
                 try {
-                    java.io.OutputStream stream = new FileOutputStream(MediaNames.THUMBNAIL_METADATA_TEST_FILES[i] + ".jpg");
+                    java.io.OutputStream stream =
+                            new FileOutputStream(
+                                    MediaNames.THUMBNAIL_METADATA_TEST_FILES[i] + ".jpg");
                     bitmap.compress(Bitmap.CompressFormat.JPEG, 75, stream);
                     stream.close();
                 } catch (Exception e) {
-                    Log.e(TAG, "Fails to convert the bitmap to a JPEG file for " + MediaNames.THUMBNAIL_METADATA_TEST_FILES[i]);
+                    Log.e(
+                            TAG,
+                            "Fails to convert the bitmap to a JPEG file for "
+                                    + MediaNames.THUMBNAIL_METADATA_TEST_FILES[i]);
                     hasFailed = true;
                     Log.e(TAG, e.toString());
                 }
-            } catch(Exception e) {
-                Log.e(TAG, "Fails to setDataSource for file " + MediaNames.THUMBNAIL_METADATA_TEST_FILES[i]);
+            } catch (Exception e) {
+                Log.e(
+                        TAG,
+                        "Fails to setDataSource for file "
+                                + MediaNames.THUMBNAIL_METADATA_TEST_FILES[i]);
                 hasFailed = true;
             }
-            Thread.yield();  // Don't be evil
+            Thread.yield(); // Don't be evil
         }
         long endedAt = System.currentTimeMillis();
         retriever.release();
         assertTrue(!hasFailed);
-        Log.v(TAG, "Average processing time per thumbnail: " + (endedAt - startedAt)/MediaNames.THUMBNAIL_METADATA_TEST_FILES.length + " ms");
+        Log.v(
+                TAG,
+                "Average processing time per thumbnail: "
+                        + (endedAt - startedAt) / MediaNames.THUMBNAIL_METADATA_TEST_FILES.length
+                        + " ms");
     }
-    
+
     @LargeTest
     public static void testMetadataRetrieval() throws Exception {
         boolean supportWMA = MediaProfileReader.getWMAEnable();
         boolean supportWMV = MediaProfileReader.getWMVEnable();
         boolean hasFailed = false;
         MediaMetadataRetriever retriever = new MediaMetadataRetriever();
-        for(int i = 0, n = MediaNames.THUMBNAIL_METADATA_TEST_FILES.length; i < n; ++i) {
+        for (int i = 0, n = MediaNames.THUMBNAIL_METADATA_TEST_FILES.length; i < n; ++i) {
             try {
                 Log.v(TAG, "File " + i + ": " + MediaNames.THUMBNAIL_METADATA_TEST_FILES[i]);
-                if ((MediaNames.THUMBNAIL_METADATA_TEST_FILES[i].endsWith(".wma") && !supportWMA) ||
-                    (MediaNames.THUMBNAIL_METADATA_TEST_FILES[i].endsWith(".wmv") && !supportWMV)
-                   ) {
-                    Log.v(TAG, "windows media is not supported and thus we will skip the test for this file");
+                if ((MediaNames.THUMBNAIL_METADATA_TEST_FILES[i].endsWith(".wma") && !supportWMA)
+                        || (MediaNames.THUMBNAIL_METADATA_TEST_FILES[i].endsWith(".wmv")
+                                && !supportWMV)) {
+                    Log.v(
+                            TAG,
+                            "windows media is not supported and thus we will skip the test for this"
+                                    + " file");
                     continue;
                 }
                 retriever.setDataSource(MediaNames.THUMBNAIL_METADATA_TEST_FILES[i]);
                 extractAllSupportedMetadataValues(retriever);
-            } catch(Exception e) {
-                Log.e(TAG, "Fails to setDataSource for file " + MediaNames.THUMBNAIL_METADATA_TEST_FILES[i]);
+            } catch (Exception e) {
+                Log.e(
+                        TAG,
+                        "Fails to setDataSource for file "
+                                + MediaNames.THUMBNAIL_METADATA_TEST_FILES[i]);
                 hasFailed = true;
             }
-            Thread.yield();  // Don't be evil
+            Thread.yield(); // Don't be evil
         }
         retriever.release();
         assertTrue(!hasFailed);
@@ -151,10 +181,12 @@
                 bitmap.compress(Bitmap.CompressFormat.JPEG, 75, stream);
                 stream.close();
             } catch (Exception e) {
-                throw new Exception("Fails to convert the bitmap to a JPEG file for " + MediaNames.TEST_PATH_1, e);
+                throw new Exception(
+                        "Fails to convert the bitmap to a JPEG file for " + MediaNames.TEST_PATH_1,
+                        e);
             }
             extractAllSupportedMetadataValues(retriever);
-        } catch(Exception e) {
+        } catch (Exception e) {
             Log.e(TAG, "Fails to setDataSource for " + MediaNames.TEST_PATH_1, e);
             hasFailed = true;
         }
@@ -181,7 +213,7 @@
 
     // Test setDataSource()
     @MediumTest
-    public static void testSetDataSource() {
+    public static void testSetDataSource() throws IOException {
         MediaMetadataRetriever retriever = new MediaMetadataRetriever();
         boolean hasFailed = false;
 
@@ -191,7 +223,7 @@
             retriever.setDataSource(path);
             Log.e(TAG, "IllegalArgumentException failed to be thrown.");
             hasFailed = true;
-        } catch(Exception e) {
+        } catch (Exception e) {
             if (!(e instanceof IllegalArgumentException)) {
                 Log.e(TAG, "Expected a IllegalArgumentException, but got a different exception");
                 hasFailed = true;
@@ -203,7 +235,7 @@
             retriever.setDataSource(MediaNames.TEST_PATH_5);
             Log.e(TAG, "IllegalArgumentException failed to be thrown.");
             hasFailed = true;
-        } catch(Exception e) {
+        } catch (Exception e) {
             if (!(e instanceof IllegalArgumentException)) {
                 Log.e(TAG, "Expected a IllegalArgumentException, but got a different exception");
                 hasFailed = true;
@@ -215,7 +247,7 @@
             retriever.setDataSource(MediaNames.TEST_PATH_4);
             Log.e(TAG, "RuntimeException failed to be thrown.");
             hasFailed = true;
-        } catch(Exception e) {
+        } catch (Exception e) {
             if (!(e instanceof RuntimeException)) {
                 Log.e(TAG, "Expected a RuntimeException, but got a different exception");
                 hasFailed = true;
@@ -228,13 +260,13 @@
             retriever.setDataSource(MediaNames.TEST_PATH_3);
             Log.e(TAG, "RuntimeException failed to be thrown.");
             hasFailed = true;
-        } catch(Exception e) {
+        } catch (Exception e) {
             if (!(e instanceof RuntimeException)) {
                 Log.e(TAG, "Expected a RuntimeException, but got a different exception");
                 hasFailed = true;
             }
         }
-        
+
         retriever.release();
         assertTrue(!hasFailed);
     }
@@ -244,17 +276,23 @@
     // We should be able to compare the actual returned metadata with the expected metadata
     // with each given sample test file.
     private static void extractAllSupportedMetadataValues(MediaMetadataRetriever retriever) {
-        String value = null;
-        Log.v(TAG, (value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER)) == null? "not found": value);
-        Log.v(TAG, (value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) == null? "not found": value);
-        Log.v(TAG, (value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS)) == null? "not found": value);
-        Log.v(TAG, (value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM)) == null? "not found": value);
-        Log.v(TAG, (value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)) == null? "not found": value);
-        Log.v(TAG, (value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_AUTHOR)) == null? "not found": value);
-        Log.v(TAG, (value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER)) == null? "not found": value);
-        Log.v(TAG, (value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE)) == null? "not found": value);
-        Log.v(TAG, (value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE)) == null? "not found": value);
-        Log.v(TAG, (value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)) == null? "not found": value);
-        Log.v(TAG, (value = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_YEAR)) == null? "not found": value);
+        int[] metadataRetrieverKeys =
+                new int[] {
+                    MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER,
+                    MediaMetadataRetriever.METADATA_KEY_DURATION,
+                    MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS,
+                    MediaMetadataRetriever.METADATA_KEY_ALBUM,
+                    MediaMetadataRetriever.METADATA_KEY_ARTIST,
+                    MediaMetadataRetriever.METADATA_KEY_AUTHOR,
+                    MediaMetadataRetriever.METADATA_KEY_COMPOSER,
+                    MediaMetadataRetriever.METADATA_KEY_DATE,
+                    MediaMetadataRetriever.METADATA_KEY_GENRE,
+                    MediaMetadataRetriever.METADATA_KEY_TITLE,
+                    MediaMetadataRetriever.METADATA_KEY_YEAR
+                };
+        for (int metadataRetrieverKey : metadataRetrieverKeys) {
+            String value = retriever.extractMetadata(metadataRetrieverKey);
+            Log.v(TAG, value == null ? "not found" : value);
+        }
     }
 }
diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp
index 0b3f9bb..68b6091 100644
--- a/packages/CtsShim/build/Android.bp
+++ b/packages/CtsShim/build/Android.bp
@@ -48,13 +48,13 @@
 }
 
 genrule {
-  name: "generate_priv_manifest",
-  srcs: [
-    "shim_priv/AndroidManifest.xml",
-    ":CtsShimPrivUpgrade"
-  ],
-  out: ["AndroidManifest.xml"],
-  cmd: "sed -e s/__HASH__/`sha512sum -b $(location :CtsShimPrivUpgrade) | cut -d' ' -f1`/ $(location shim_priv/AndroidManifest.xml) > $(out)",
+    name: "generate_priv_manifest",
+    srcs: [
+        "shim_priv/AndroidManifest.xml",
+        ":CtsShimPrivUpgrade",
+    ],
+    out: ["AndroidManifest.xml"],
+    cmd: "sed -e s/__HASH__/`sha512sum -b $(location :CtsShimPrivUpgrade) | cut -d' ' -f1`/ $(location shim_priv/AndroidManifest.xml) > $(out)",
 }
 
 //##########################################################
@@ -141,6 +141,34 @@
 }
 
 //##########################################################
+// Variant: System app upgrade
+
+android_app {
+    name: "CtsShimUpgrade",
+
+    sdk_version: "current",
+    optimize: {
+        enabled: false,
+    },
+    dex_preopt: {
+        enabled: false,
+    },
+
+    manifest: "shim/AndroidManifestUpgrade.xml",
+    min_sdk_version: "24",
+}
+
+genrule {
+    name: "generate_shim_manifest",
+    srcs: [
+        "shim/AndroidManifest.xml",
+        ":CtsShimUpgrade",
+    ],
+    out: ["AndroidManifest.xml"],
+    cmd: "sed -e s/__HASH__/`sha512sum -b $(location :CtsShimUpgrade) | cut -d' ' -f1`/ $(location shim/AndroidManifest.xml) > $(out)",
+}
+
+//##########################################################
 // Variant: System app
 
 android_app {
@@ -154,7 +182,7 @@
         enabled: false,
     },
 
-    manifest: "shim/AndroidManifest.xml",
+    manifest: ":generate_shim_manifest",
     apex_available: [
         "//apex_available:platform",
         "com.android.apex.cts.shim.v1",
diff --git a/packages/CtsShim/build/shim/AndroidManifest.xml b/packages/CtsShim/build/shim/AndroidManifest.xml
index 1ffe56c..3b8276b6 100644
--- a/packages/CtsShim/build/shim/AndroidManifest.xml
+++ b/packages/CtsShim/build/shim/AndroidManifest.xml
@@ -17,13 +17,15 @@
 <!-- Manifest for the system CTS shim -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    package="com.android.cts.ctsshim" >
+    package="com.android.cts.ctsshim"
+    android:sharedUserId="com.android.cts.ctsshim" >
 
-    <uses-sdk android:minSdkVersion="24"
+    <uses-sdk
+        android:minSdkVersion="24"
         android:targetSdkVersion="28" />
 
     <restrict-update
-        android:hash="__CAN_NOT_BE_UPDATED__" />
+        android:hash="__HASH__" />
 
     <application
         android:hasCode="false"
diff --git a/packages/CtsShim/build/shim/AndroidManifestUpgrade.xml b/packages/CtsShim/build/shim/AndroidManifestUpgrade.xml
new file mode 100644
index 0000000..7f3644a
--- /dev/null
+++ b/packages/CtsShim/build/shim/AndroidManifestUpgrade.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<!-- Manifest for the system CTS shim -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.android.cts.ctsshim" >
+
+    <uses-sdk
+        android:minSdkVersion="24"
+        android:targetSdkVersion="28" />
+
+    <application
+        android:hasCode="false"
+        tools:ignore="AllowBackup,MissingApplicationIcon" />
+</manifest>
+
diff --git a/packages/SettingsLib/res/drawable/ic_bt_le_audio.xml b/packages/SettingsLib/res/drawable/ic_bt_le_audio.xml
new file mode 100644
index 0000000..5b52a04
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_bt_le_audio.xml
@@ -0,0 +1,31 @@
+<!--
+     Copyright 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    android:tint="?android:attr/colorControlNormal" >
+    <path
+        android:pathData="M18.2,1L9.8,1C8.81,1 8,1.81 8,2.8v14.4c0,0.99 0.81,1.79 1.8,1.79l8.4,0.01c0.99,0 1.8,-0.81 1.8,-1.8L20,2.8c0,-0.99 -0.81,-1.8 -1.8,-1.8zM14,3c1.1,0 2,0.89 2,2s-0.9,2 -2,2 -2,-0.89 -2,-2 0.9,-2 2,-2zM14,16.5c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"
+        android:fillColor="#FFFFFFFF"/>
+    <path
+        android:pathData="M14,12.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
+        android:fillColor="#FFFFFFFF"/>
+    <path
+        android:pathData="M6,5H4v16c0,1.1 0.89,2 2,2h10v-2H6V5z"
+        android:fillColor="#FFFFFFFF"/>
+</vector>
diff --git a/packages/SettingsLib/res/values-eu/strings.xml b/packages/SettingsLib/res/values-eu/strings.xml
index 3ae9fa3..915db9c 100644
--- a/packages/SettingsLib/res/values-eu/strings.xml
+++ b/packages/SettingsLib/res/values-eu/strings.xml
@@ -331,7 +331,7 @@
     <string name="adb_warning_message" msgid="8145270656419669221">"USB bidezko arazketa garapen-xedeetarako soilik dago diseinatuta. Erabil ezazu ordenagailuaren eta gailuaren artean datuak kopiatzeko, aplikazioak gailuan jakinarazi gabe instalatzeko eta erregistro-datuak irakurtzeko."</string>
     <string name="adbwifi_warning_title" msgid="727104571653031865">"Hari gabeko arazketa baimendu nahi duzu?"</string>
     <string name="adbwifi_warning_message" msgid="8005936574322702388">"Hari gabeko arazketa garapen-xedeetarako soilik dago diseinatuta. Erabil ezazu ordenagailuaren eta gailuaren artean datuak kopiatzeko, gailuan aplikazioak jakinarazi gabe instalatzeko eta erregistroko datuak irakurtzeko."</string>
-    <string name="adb_keys_warning_message" msgid="2968555274488101220">"Aurretik baimendutako ordenagailu guztiei USB bidezko arazketarako sarbidea baliogabetu nahi diezu?"</string>
+    <string name="adb_keys_warning_message" msgid="2968555274488101220">"Aurretik baimendutako ordenagailu guztiei USB bidezko arazketarako sarbidea kendu nahi diezu?"</string>
     <string name="dev_settings_warning_title" msgid="8251234890169074553">"Baimendu garapenerako ezarpenak?"</string>
     <string name="dev_settings_warning_message" msgid="37741686486073668">"Ezarpen hauek garapen-xedeetarako pentsatu dira soilik. Baliteke ezarpenen eraginez gailua matxuratzea edo funtzionamendu okerra izatea."</string>
     <string name="verify_apps_over_usb_title" msgid="6031809675604442636">"Egiaztatu USB bidezko aplik."</string>
diff --git a/packages/SettingsLib/res/values-ky/strings.xml b/packages/SettingsLib/res/values-ky/strings.xml
index 74585da..319e61a 100644
--- a/packages/SettingsLib/res/values-ky/strings.xml
+++ b/packages/SettingsLib/res/values-ky/strings.xml
@@ -332,7 +332,7 @@
     <string name="adbwifi_warning_title" msgid="727104571653031865">"Мүчүлүштүктөрдү Wi-Fi аркылуу оңдоого уруксат бересизби?"</string>
     <string name="adbwifi_warning_message" msgid="8005936574322702388">"Мүчүлүштүктөрдү Wi-Fi аркылуу аныктоо – өндүрүү максатында гана түзүлгөн. Аны компьютериңиз менен түзмөгүңүздүн ортосунда маалыматты алмашуу, колдонмолорду түзмөгүңүзгө эскертүүсүз орнотуу жана маалыматтар таржымалын окуу үчүн колдонсоңуз болот."</string>
     <string name="adb_keys_warning_message" msgid="2968555274488101220">"Сиз мурун USB жөндөөлөрүнө уруксат берген бардык компүтерлердин жеткиси жокко чыгарылсынбы?"</string>
-    <string name="dev_settings_warning_title" msgid="8251234890169074553">"Жөндөөлөрдү өзгөртүү"</string>
+    <string name="dev_settings_warning_title" msgid="8251234890169074553">"Параметрлерди өзгөртүү"</string>
     <string name="dev_settings_warning_message" msgid="37741686486073668">"Бул орнотуулар өндүрүүчүлөр үчүн гана берилген. Булар түзмөгүңүздүн колдонмолорун бузулушуна же туура эмес иштешине алып келиши мүмкүн."</string>
     <string name="verify_apps_over_usb_title" msgid="6031809675604442636">"Орнотулуучу колдонмону текшерүү"</string>
     <string name="verify_apps_over_usb_summary" msgid="1317933737581167839">"ADB/ADT аркылуу орнотулган колдонмолордун коопсуздугу текшерилет."</string>
diff --git a/packages/SettingsLib/res/values-or/strings.xml b/packages/SettingsLib/res/values-or/strings.xml
index dd0b1f2..6faf614 100644
--- a/packages/SettingsLib/res/values-or/strings.xml
+++ b/packages/SettingsLib/res/values-or/strings.xml
@@ -182,7 +182,7 @@
     <string name="running_process_item_user_label" msgid="3988506293099805796">"ଉପଯୋଗକର୍ତ୍ତା: <xliff:g id="USER_NAME">%1$s</xliff:g>"</string>
     <string name="launch_defaults_some" msgid="3631650616557252926">"କିଛି ପୂର୍ବ-ନିର୍ଦ୍ଧାରିତ ମାନ ସେଟ୍‌ ହୋଇଛି"</string>
     <string name="launch_defaults_none" msgid="8049374306261262709">"କୌଣସି ଡିଫଲ୍ଟ ସେଟ୍‍ ହୋଇନାହିଁ"</string>
-    <string name="tts_settings" msgid="8130616705989351312">"ଟେକ୍ସଟ-ରୁ-ସ୍ପିଚ୍ ସେଟିଂସ୍"</string>
+    <string name="tts_settings" msgid="8130616705989351312">"ଟେକ୍ସଟ୍-ଟୁ-ସ୍ପିଚ୍ ସେଟିଂସ"</string>
     <string name="tts_settings_title" msgid="7602210956640483039">"ଟେକ୍ସଟ୍‍-ଟୁ-ସ୍ପିଚ୍‍ ଆଉଟ୍‍ପୁଟ୍‌"</string>
     <string name="tts_default_rate_title" msgid="3964187817364304022">"ସ୍ପିଚ୍‌ ରେଟ୍"</string>
     <string name="tts_default_rate_summary" msgid="3781937042151716987">"ଲେଖା ପଢ଼ିବାର ବେଗ"</string>
diff --git a/packages/SettingsLib/res/values-pa/strings.xml b/packages/SettingsLib/res/values-pa/strings.xml
index ccdfec3..475ce35 100644
--- a/packages/SettingsLib/res/values-pa/strings.xml
+++ b/packages/SettingsLib/res/values-pa/strings.xml
@@ -191,11 +191,11 @@
     <string name="tts_default_lang_title" msgid="4698933575028098940">"ਭਾਸ਼ਾ"</string>
     <string name="tts_lang_use_system" msgid="6312945299804012406">"ਸਿਸਟਮ ਭਾਸ਼ਾ ਵਰਤੋ"</string>
     <string name="tts_lang_not_selected" msgid="7927823081096056147">"ਭਾਸ਼ਾ ਨਹੀਂ ਚੁਣੀ"</string>
-    <string name="tts_default_lang_summary" msgid="9042620014800063470">"ਬੋਲੇ ਗਏ ਟੈਕਸਟ ਲਈ ਭਾਸ਼ਾ-ਵਿਸ਼ੇਸ਼ ਵੌਇਸ ਸੈਟ ਕਰਦਾ ਹੈ"</string>
+    <string name="tts_default_lang_summary" msgid="9042620014800063470">"ਬੋਲੀ ਗਈ ਲਿਖਤ ਲਈ ਭਾਸ਼ਾ-ਵਿਸ਼ੇਸ਼ ਅਵਾਜ਼ ਸੈੱਟ ਕਰਦਾ ਹੈ"</string>
     <string name="tts_play_example_title" msgid="1599468547216481684">"ਇੱਕ ਉਦਾਹਰਨ ਲਈ ਸੁਣੋ"</string>
     <string name="tts_play_example_summary" msgid="634044730710636383">"ਸਪੀਚ ਸਿੰਥੈਸਿਸ ਦਾ ਇੱਕ ਛੋਟਾ ਪ੍ਰਦਰਸ਼ਨ ਪਲੇ ਕਰੋ"</string>
-    <string name="tts_install_data_title" msgid="1829942496472751703">"ਵੌਇਸ ਡਾਟਾ ਸਥਾਪਤ ਕਰੋ"</string>
-    <string name="tts_install_data_summary" msgid="3608874324992243851">"ਸਪੀਚ ਸਿੰਥੈਸਿਸ ਲਈ ਲੋੜੀਂਦਾ ਵੌਇਸ ਡਾਟਾ ਸਥਾਪਤ ਕਰੋ"</string>
+    <string name="tts_install_data_title" msgid="1829942496472751703">"ਅਵਾਜ਼ੀ ਡਾਟਾ ਸਥਾਪਤ ਕਰੋ"</string>
+    <string name="tts_install_data_summary" msgid="3608874324992243851">"ਸਪੀਚ ਸਿੰਥੈਸਿਸ ਲਈ ਲੋੜੀਂਦਾ ਅਵਾਜ਼ੀ ਡਾਟਾ ਸਥਾਪਤ ਕਰੋ"</string>
     <string name="tts_engine_security_warning" msgid="3372432853837988146">"ਇਹ ਸਪੀਚ ਸਿੰਥੈਸਿਸ ਇੰਜਣ ਉਹ ਸਭ ਲਿਖਤ ਇਕੱਤਰ ਕਰਨ ਵਿੱਚ ਸਮਰੱਥ ਹੋ ਸਕਦਾ ਹੈ, ਜੋ ਬੋਲਿਆ ਜਾਏਗਾ, ਨਿੱਜੀ ਡਾਟਾ ਸਮੇਤ ਜਿਵੇਂ ਪਾਸਵਰਡ ਅਤੇ ਕ੍ਰੈਡਿਟ ਕਾਰਡ ਨੰਬਰ। ਇਹ <xliff:g id="TTS_PLUGIN_ENGINE_NAME">%s</xliff:g> ਇੰਜਣ ਤੋਂ ਆਉਂਦਾ ਹੈ। ਕੀ ਇਸ ਸਪੀਚ ਸਿੰਥੈਸਿਸ ਇੰਜਣ ਦੀ ਵਰਤੋਂ ਨੂੰ ਚਾਲੂ ਕਰਨਾ ਹੈੈ?"</string>
     <string name="tts_engine_network_required" msgid="8722087649733906851">"ਇਸ ਭਾਸ਼ਾ ਲਈ ਲਿਖਤ ਤੋਂ ਬੋਲੀ ਆਊਟਪੁੱਟ ਲਈ ਇੱਕ ਚਾਲੂ ਨੈੱਟਵਰਕ ਕਨੈਕਸ਼ਨ ਦੀ ਲੋੜ ਹੈ।"</string>
     <string name="tts_default_sample_string" msgid="6388016028292967973">"ਇਹ ਸਪੀਚ ਸਿੰਥੈਸਿਸ ਦਾ ਇੱਕ ਉਦਾਹਰਨ ਹੈ"</string>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index cab7cee..8e20e02 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -257,8 +257,12 @@
 
     <!-- Bluetooth settings.  The user-visible string that is used whenever referring to the Hearing Aid profile. -->
     <string name="bluetooth_profile_hearing_aid">Hearing Aids</string>
+    <!-- Bluetooth settings.  The user-visible string that is used whenever referring to the LE_AUDIO profile. -->
+    <string name="bluetooth_profile_le_audio">LE_AUDIO</string>
     <!-- Bluetooth settings.  Connection options screen.  The summary for the Hearing Aid checkbox preference when Hearing Aid is connected. -->
     <string name="bluetooth_hearing_aid_profile_summary_connected">Connected to Hearing Aids</string>
+    <!-- Bluetooth settings.  Connection options screen.  The summary for the LE_AUDIO checkbox preference when LE_AUDIO is connected. -->
+    <string name="bluetooth_le_audio_profile_summary_connected">Connected to LE_AUDIO</string>
 
     <!-- Bluetooth settings.  Connection options screen.  The summary for the A2DP checkbox preference when A2DP is connected. -->
     <string name="bluetooth_a2dp_profile_summary_connected">Connected to media audio</string>
@@ -299,6 +303,8 @@
     <string name="bluetooth_hid_profile_summary_use_for">Use for input</string>
     <!-- Bluetooth settings.  Connection options screen.  The summary for the Hearing Aid checkbox preference that describes how checking it will set the Hearing Aid profile as preferred. -->
     <string name="bluetooth_hearing_aid_profile_summary_use_for">Use for Hearing Aids</string>
+    <!-- Bluetooth settings.  Connection options screen.  The summary for the LE_AUDIO checkbox preference that describes how checking it will set the LE_AUDIO profile as preferred. -->
+    <string name="bluetooth_le_audio_profile_summary_use_for">Use for LE_AUDIO</string>
 
     <!-- Button text for accepting an incoming pairing request. [CHAR LIMIT=20] -->
     <string name="bluetooth_pairing_accept">Pair</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
index c4cbc2b..5f2bef7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
@@ -16,10 +16,15 @@
 
 package com.android.settingslib.applications;
 
+import android.annotation.SuppressLint;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 
+/**
+ * A class for applying config changes and determing if doing so resulting in any "interesting"
+ * changes.
+ */
 public class InterestingConfigChanges {
     private final Configuration mLastConfiguration = new Configuration();
     private final int mFlags;
@@ -35,6 +40,14 @@
         mFlags = flags;
     }
 
+    /**
+     * Applies the given config change and returns whether an "interesting" change happened.
+     *
+     * @param res The source of the new config to apply
+     *
+     * @return Whether interesting changes occurred
+     */
+    @SuppressLint("NewApi")
     public boolean applyNewConfig(Resources res) {
         int configChanges = mLastConfiguration.updateFrom(
                 Configuration.generateDelta(mLastConfiguration, res.getConfiguration()));
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index 8750309..58d2185 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -22,6 +22,7 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -116,6 +117,8 @@
         addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
         addHandler(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,
                 new ActiveDeviceChangedHandler());
+        addHandler(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED,
+                   new ActiveDeviceChangedHandler());
 
         // Headset state changed broadcasts
         addHandler(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
@@ -127,9 +130,6 @@
         addHandler(BluetoothDevice.ACTION_ACL_CONNECTED, new AclStateChangedHandler());
         addHandler(BluetoothDevice.ACTION_ACL_DISCONNECTED, new AclStateChangedHandler());
 
-        addHandler(BluetoothCsipSetCoordinator.ACTION_CSIS_SET_MEMBER_AVAILABLE,
-                new SetMemberAvailableHandler());
-
         registerAdapterIntentReceiver();
     }
 
@@ -455,6 +455,9 @@
                 bluetoothProfile = BluetoothProfile.HEADSET;
             } else if (Objects.equals(action, BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED)) {
                 bluetoothProfile = BluetoothProfile.HEARING_AID;
+            } else if (Objects.equals(action,
+                        BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED)) {
+                bluetoothProfile = BluetoothProfile.LE_AUDIO;
             } else {
                 Log.w(TAG, "ActiveDeviceChangedHandler: unknown action " + action);
                 return;
@@ -515,29 +518,4 @@
             dispatchAudioModeChanged();
         }
     }
-
-    private class SetMemberAvailableHandler implements Handler {
-        @Override
-        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
-            final String action = intent.getAction();
-            if (device == null) {
-                Log.e(TAG, "SetMemberAvailableHandler: device is null");
-                return;
-            }
-
-            if (action == null) {
-                Log.e(TAG, "SetMemberAvailableHandler: action is null");
-                return;
-            }
-
-            final int groupId = intent.getIntExtra(BluetoothCsipSetCoordinator.EXTRA_CSIS_GROUP_ID,
-                    BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
-            if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
-                Log.e(TAG, "SetMemberAvailableHandler: Invalid group id");
-                return;
-            }
-
-            mDeviceManager.onSetMemberAppear(device, groupId);
-        }
-    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index db6b23b..912b468 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -21,6 +21,7 @@
 import android.bluetooth.BluetoothCsipSetCoordinator;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothUuid;
 import android.content.Context;
@@ -69,6 +70,7 @@
     // Some Hearing Aids (especially the 2nd device) needs more time to do service discovery
     private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 15000;
     private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
+    private static final long MAX_LEAUDIO_DELAY_FOR_AUTO_CONNECT = 30000;
     private static final long MAX_MEDIA_PROFILE_CONNECT_DELAY = 60000;
 
     private final Context mContext;
@@ -108,11 +110,14 @@
     private boolean mIsActiveDeviceA2dp = false;
     private boolean mIsActiveDeviceHeadset = false;
     private boolean mIsActiveDeviceHearingAid = false;
+    private boolean mIsActiveDeviceLeAudio = false;
     // Media profile connect state
     private boolean mIsA2dpProfileConnectedFail = false;
     private boolean mIsHeadsetProfileConnectedFail = false;
     private boolean mIsHearingAidProfileConnectedFail = false;
+    private boolean mIsLeAudioProfileConnectedFail = false;
     private boolean mUnpairing;
+
     // Group second device for Hearing Aid
     private CachedBluetoothDevice mSubDevice;
     // Group member devices for the coordinated set
@@ -133,6 +138,9 @@
                 case BluetoothProfile.HEARING_AID:
                     mIsHearingAidProfileConnectedFail = true;
                     break;
+                case BluetoothProfile.LE_AUDIO:
+                    mIsLeAudioProfileConnectedFail = true;
+                    break;
                 default:
                     Log.w(TAG, "handleMessage(): unknown message : " + msg.what);
                     break;
@@ -267,6 +275,9 @@
             case BluetoothProfile.HEARING_AID:
                 mIsHearingAidProfileConnectedFail = isFailed;
                 break;
+            case BluetoothProfile.LE_AUDIO:
+                mIsLeAudioProfileConnectedFail = isFailed;
+                break;
             default:
                 Log.w(TAG, "setProfileConnectedStatus(): unknown profile id : " + profileId);
                 break;
@@ -543,6 +554,13 @@
                 result = true;
             }
         }
+        LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile();
+        if ((leAudioProfile != null) && isConnectedProfile(leAudioProfile)) {
+            if (leAudioProfile.setActiveDevice(getDevice())) {
+                Log.i(TAG, "OnPreferenceClickListener: LeAudio active device=" + this);
+                result = true;
+            }
+        }
         return result;
     }
 
@@ -621,6 +639,10 @@
             changed = (mIsActiveDeviceHearingAid != isActive);
             mIsActiveDeviceHearingAid = isActive;
             break;
+        case BluetoothProfile.LE_AUDIO:
+            changed = (mIsActiveDeviceLeAudio != isActive);
+            mIsActiveDeviceLeAudio = isActive;
+            break;
         default:
             Log.w(TAG, "onActiveDeviceChanged: unknown profile " + bluetoothProfile +
                     " isActive " + isActive);
@@ -652,6 +674,8 @@
                 return mIsActiveDeviceHeadset;
             case BluetoothProfile.HEARING_AID:
                 return mIsActiveDeviceHearingAid;
+            case BluetoothProfile.LE_AUDIO:
+                return mIsActiveDeviceLeAudio;
             default:
                 Log.w(TAG, "getActiveDevice: unknown profile " + bluetoothProfile);
                 break;
@@ -746,6 +770,10 @@
         if (hearingAidProfile != null) {
             mIsActiveDeviceHearingAid = hearingAidProfile.getActiveDevices().contains(mDevice);
         }
+        LeAudioProfile leAudio = mProfileManager.getLeAudioProfile();
+        if (leAudio != null) {
+            mIsActiveDeviceLeAudio = leAudio.getActiveDevices().contains(mDevice);
+        }
     }
 
     /**
@@ -760,6 +788,8 @@
             timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT;
         } else if (ArrayUtils.contains(uuids, BluetoothUuid.HEARING_AID)) {
             timeout = MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT;
+        } else if (ArrayUtils.contains(uuids, BluetoothUuid.LE_AUDIO)) {
+            timeout = MAX_LEAUDIO_DELAY_FOR_AUTO_CONNECT;
         }
 
         if (BluetoothUtils.D) {
@@ -984,6 +1014,7 @@
         boolean a2dpConnected = true;        // A2DP is connected
         boolean hfpConnected = true;         // HFP is connected
         boolean hearingAidConnected = true;  // Hearing Aid is connected
+        boolean leAudioConnected = true;        // LeAudio is connected
         int leftBattery = -1;
         int rightBattery = -1;
 
@@ -1015,6 +1046,8 @@
                                 hfpConnected = false;
                             } else if (profile instanceof HearingAidProfile) {
                                 hearingAidConnected = false;
+                            } else if (profile instanceof LeAudioProfile) {
+                                leAudioConnected = false;
                             }
                         }
                         break;
@@ -1057,7 +1090,8 @@
             //    1. Hearing Aid device active.
             //    2. Headset device active with in-calling state.
             //    3. A2DP device active without in-calling state.
-            if (a2dpConnected || hfpConnected || hearingAidConnected) {
+            //    4. Le Audio device active
+            if (a2dpConnected || hfpConnected || hearingAidConnected || leAudioConnected) {
                 final boolean isOnCall = Utils.isAudioModeOngoingCall(mContext);
                 if ((mIsActiveDeviceHearingAid)
                         || (mIsActiveDeviceHeadset && isOnCall)
@@ -1092,7 +1126,8 @@
 
     private boolean isProfileConnectedFail() {
         return mIsA2dpProfileConnectedFail || mIsHearingAidProfileConnectedFail
-                || (!isConnectedSapDevice() && mIsHeadsetProfileConnectedFail);
+                || (!isConnectedSapDevice() && mIsHeadsetProfileConnectedFail)
+                || mIsLeAudioProfileConnectedFail;
     }
 
     /**
@@ -1103,6 +1138,7 @@
         boolean a2dpNotConnected = false;       // A2DP is preferred but not connected
         boolean hfpNotConnected = false;        // HFP is preferred but not connected
         boolean hearingAidNotConnected = false; // Hearing Aid is preferred but not connected
+        boolean leAudioNotConnected = false;       // LeAudio is preferred but not connected
 
         synchronized (mProfileLock) {
             for (LocalBluetoothProfile profile : getProfiles()) {
@@ -1128,6 +1164,8 @@
                                 hfpNotConnected = true;
                             } else if (profile instanceof HearingAidProfile) {
                                 hearingAidNotConnected = true;
+                            } else if (profile instanceof  LeAudioProfile) {
+                                leAudioNotConnected = true;
                             }
                         }
                         break;
@@ -1166,6 +1204,11 @@
             return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
         }
 
+        if (!leAudioNotConnected && mIsActiveDeviceLeAudio) {
+            activeDeviceString = activeDeviceStringsArray[1];
+            return mContext.getString(R.string.bluetooth_connected, activeDeviceString);
+        }
+
         if (profileConnected) {
             if (a2dpNotConnected && hfpNotConnected) {
                 if (batteryLevelPercentageString != null) {
@@ -1235,6 +1278,15 @@
                 BluetoothProfile.STATE_CONNECTED;
     }
 
+    /**
+     * @return {@code true} if {@code cachedBluetoothDevice} is LeAudio device
+     */
+    public boolean isConnectedLeAudioDevice() {
+        LeAudioProfile leAudio = mProfileManager.getLeAudioProfile();
+        return leAudio != null && leAudio.getConnectionStatus(mDevice) ==
+                BluetoothProfile.STATE_CONNECTED;
+    }
+
     private boolean isConnectedSapDevice() {
         SapProfile sapProfile = mProfileManager.getSapProfile();
         return sapProfile != null && sapProfile.getConnectionStatus(mDevice)
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 1f75ae3..b429fe6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -340,22 +340,24 @@
 
     /**
      * Called when we found a set member of a group. The function will check the {@code groupId} if
-     * it exists and if there is a ongoing pair, the device would be ignored.
+     * it exists and the bond state of the device is BOND_NOE, and if there isn't any ongoing pair
+     * , and then return {@code true} to pair the device automatically.
      *
      * @param device The found device
      * @param groupId The group id of the found device
+     *
+     * @return {@code true}, if the device should pair automatically; Otherwise, return
+     * {@code false}.
      */
-    public synchronized void onSetMemberAppear(BluetoothDevice device, int groupId) {
-        Log.d(TAG, "onSetMemberAppear, groupId: " + groupId + " device: " + device.toString());
-
-        if (mOngoingSetMemberPair != null) {
-            Log.d(TAG, "Ongoing set memberPairing in process, drop it!");
-            return;
+    public synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) {
+        if (mOngoingSetMemberPair != null || device.getBondState() != BluetoothDevice.BOND_NONE
+                || !mCsipDeviceManager.isExistedGroupId(groupId)) {
+            return false;
         }
 
-        if (mCsipDeviceManager.onSetMemberAppear(device, groupId)) {
-            mOngoingSetMemberPair = device;
-        }
+        Log.d(TAG, "Bond " + device.getName() + " by CSIP");
+        mOngoingSetMemberPair = device;
+        return true;
     }
 
     /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index 347e14b..1d29966 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -240,22 +240,15 @@
     }
 
     /**
-     * Called when we found a set member of a group. The function will check bond state, and
-     * the {@code groupId} if it exists, and then create the bond.
+     * Check if the {@code groupId} is existed.
      *
-     * @param device The found device
-     * @param groupId The group id of the found device
+     * @param groupId The group id
      *
-     * @return {@code true}, if the we create bond with the device. Otherwise, return
-     * {@code false}.
+     * @return {@code true}, if we could find a device with this {@code groupId}; Otherwise,
+     * return {@code false}.
      */
-    public boolean onSetMemberAppear(BluetoothDevice device, int groupId) {
-        if (device.getBondState() != BluetoothDevice.BOND_NONE) {
-            return false;
-        }
-
+    public boolean isExistedGroupId(int groupId) {
         if (getCachedDevice(groupId) != null) {
-            device.createBond(BluetoothDevice.TRANSPORT_LE);
             return true;
         }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
new file mode 100644
index 0000000..209507a
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
@@ -0,0 +1,264 @@
+/*   Copyright 2021 HIMSA II K/S - www.himsa.com. Represented by EHIMA
+- www.ehima.com
+*/
+
+/* 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.bluetooth;
+
+import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL;
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+
+import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothCodecConfig;
+import android.bluetooth.BluetoothCodecStatus;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.content.Context;
+import android.os.Build;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settingslib.R;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class LeAudioProfile implements LocalBluetoothProfile {
+    private static final String TAG = "LeAudioProfile";
+    private static boolean DEBUG = true;
+
+    private Context mContext;
+
+    private BluetoothLeAudio mService;
+    private boolean mIsProfileReady;
+
+    private final CachedBluetoothDeviceManager mDeviceManager;
+
+    static final String NAME = "LE_AUDIO";
+    private final LocalBluetoothProfileManager mProfileManager;
+    private final BluetoothAdapter mBluetoothAdapter;
+
+    // Order of this profile in device profiles list
+    private static final int ORDINAL = 1;
+
+    // These callbacks run on the main thread.
+    private final class LeAudioServiceListener
+            implements BluetoothProfile.ServiceListener {
+
+        @RequiresApi(Build.VERSION_CODES.S)
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            if (DEBUG) {
+                Log.d(TAG,"Bluetooth service connected");
+            }
+            mService = (BluetoothLeAudio) proxy;
+            // We just bound to the service, so refresh the UI for any connected LeAudio devices.
+            List<BluetoothDevice> deviceList = mService.getConnectedDevices();
+            while (!deviceList.isEmpty()) {
+                BluetoothDevice nextDevice = deviceList.remove(0);
+                CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
+                // we may add a new device here, but generally this should not happen
+                if (device == null) {
+                    if (DEBUG) {
+                        Log.d(TAG, "LeAudioProfile found new device: " + nextDevice);
+                    }
+                    device = mDeviceManager.addDevice(nextDevice);
+                }
+                device.onProfileStateChanged(LeAudioProfile.this,
+                        BluetoothProfile.STATE_CONNECTED);
+                device.refresh();
+            }
+
+            mProfileManager.callServiceConnectedListeners();
+            mIsProfileReady = true;
+        }
+
+        public void onServiceDisconnected(int profile) {
+            if (DEBUG) {
+                 Log.d(TAG,"Bluetooth service disconnected");
+            }
+            mProfileManager.callServiceDisconnectedListeners();
+            mIsProfileReady = false;
+        }
+    }
+
+    public boolean isProfileReady() {
+        return mIsProfileReady;
+    }
+
+    @Override
+    public int getProfileId() {
+        return BluetoothProfile.LE_AUDIO;
+    }
+
+    LeAudioProfile(Context context, CachedBluetoothDeviceManager deviceManager,
+            LocalBluetoothProfileManager profileManager) {
+        mContext = context;
+        mDeviceManager = deviceManager;
+        mProfileManager = profileManager;
+
+        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        mBluetoothAdapter.getProfileProxy(
+                context, new LeAudioServiceListener(),
+                BluetoothProfile.LE_AUDIO);
+    }
+
+    public boolean accessProfileEnabled() {
+        return true;
+    }
+
+    public boolean isAutoConnectable() {
+        return true;
+    }
+
+    public List<BluetoothDevice> getConnectedDevices() {
+        if (mService == null) {
+            return new ArrayList<BluetoothDevice>(0);
+        }
+        return mService.getDevicesMatchingConnectionStates(
+              new int[] {BluetoothProfile.STATE_CONNECTED,
+                         BluetoothProfile.STATE_CONNECTING,
+                         BluetoothProfile.STATE_DISCONNECTING});
+    }
+
+    /*
+    * @hide
+    */
+    public boolean connect(BluetoothDevice device) {
+       if (mService == null) {
+           return false;
+       }
+       return mService.connect(device);
+    }
+
+    /*
+    * @hide
+    */
+    public boolean disconnect(BluetoothDevice device) {
+       if (mService == null) {
+           return false;
+       }
+       return mService.disconnect(device);
+    }
+
+    public int getConnectionStatus(BluetoothDevice device) {
+        if (mService == null) {
+            return BluetoothProfile.STATE_DISCONNECTED;
+        }
+        return mService.getConnectionState(device);
+    }
+
+    public boolean setActiveDevice(BluetoothDevice device) {
+        if (mBluetoothAdapter == null) {
+            return false;
+        }
+        return device == null
+                ? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_ALL)
+                : mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_ALL);
+    }
+
+    public List<BluetoothDevice> getActiveDevices() {
+        if (mService == null) {
+            return new ArrayList<>();
+        }
+        return mService.getActiveDevices();
+    }
+
+    @Override
+    public boolean isEnabled(BluetoothDevice device) {
+        if (mService == null || device == null) {
+            return false;
+        }
+        return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
+    }
+
+    @Override
+    public int getConnectionPolicy(BluetoothDevice device) {
+        if (mService == null || device == null) {
+            return CONNECTION_POLICY_FORBIDDEN;
+        }
+        return mService.getConnectionPolicy(device);
+    }
+
+    @Override
+    public boolean setEnabled(BluetoothDevice device, boolean enabled) {
+        boolean isEnabled = false;
+        if (mService == null || device == null) {
+            return false;
+        }
+        if (enabled) {
+            if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
+                isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
+            }
+        } else {
+            isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
+        }
+
+        return isEnabled;
+    }
+
+    public String toString() {
+        return NAME;
+    }
+
+    public int getOrdinal() {
+        return ORDINAL;
+    }
+
+    public int getNameResource(BluetoothDevice device) {
+        return R.string.bluetooth_profile_le_audio;
+    }
+
+    public int getSummaryResourceForDevice(BluetoothDevice device) {
+        int state = getConnectionStatus(device);
+        switch (state) {
+            case BluetoothProfile.STATE_DISCONNECTED:
+                return R.string.bluetooth_le_audio_profile_summary_use_for;
+
+            case BluetoothProfile.STATE_CONNECTED:
+                return R.string.bluetooth_le_audio_profile_summary_connected;
+
+            default:
+                return BluetoothUtils.getConnectionStateSummary(state);
+        }
+    }
+
+    public int getDrawableResource(BluetoothClass btClass) {
+        return R.drawable.ic_bt_le_audio;
+    }
+
+    @RequiresApi(Build.VERSION_CODES.S)
+    protected void finalize() {
+        if (DEBUG) {
+            Log.d(TAG, "finalize()");
+        }
+        if (mService != null) {
+            try {
+                BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.LE_AUDIO,
+                        mService);
+                mService = null;
+            }catch (Throwable t) {
+                Log.w(TAG, "Error cleaning up LeAudio proxy", t);
+            }
+        }
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index bcb3455..3347920 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -24,6 +24,7 @@
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothHeadsetClient;
 import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothHidDevice;
 import android.bluetooth.BluetoothHidHost;
 import android.bluetooth.BluetoothMap;
@@ -102,6 +103,7 @@
     private PbapServerProfile mPbapProfile;
     private HearingAidProfile mHearingAidProfile;
     private CsipSetCoordinatorProfile mCsipSetCoordinatorProfile;
+    private LeAudioProfile mLeAudioProfile;
     private SapProfile mSapProfile;
     private VolumeControlProfile mVolumeControlProfile;
 
@@ -232,6 +234,14 @@
             // Note: no event handler for VCP, only for being connectable.
             mProfileNameMap.put(VolumeControlProfile.NAME, mVolumeControlProfile);
         }
+        if (mLeAudioProfile == null && supportedList.contains(BluetoothProfile.LE_AUDIO)) {
+            if (DEBUG) {
+                Log.d(TAG, "Adding local LE_AUDIO profile");
+            }
+            mLeAudioProfile = new LeAudioProfile(mContext, mDeviceManager, this);
+            addProfile(mLeAudioProfile, LeAudioProfile.NAME,
+                       BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
+        }
         if (mCsipSetCoordinatorProfile == null
                 && supportedList.contains(BluetoothProfile.CSIP_SET_COORDINATOR)) {
             if (DEBUG) {
@@ -487,6 +497,10 @@
         return mHearingAidProfile;
     }
 
+    public LeAudioProfile getLeAudioProfile() {
+        return mLeAudioProfile;
+    }
+
     SapProfile getSapProfile() {
         return mSapProfile;
     }
@@ -614,6 +628,11 @@
             removedProfiles.remove(mHearingAidProfile);
         }
 
+        if (ArrayUtils.contains(uuids, BluetoothUuid.LE_AUDIO) && mLeAudioProfile != null) {
+            profiles.add(mLeAudioProfile);
+            removedProfiles.remove(mLeAudioProfile);
+        }
+
         if (mSapProfile != null && ArrayUtils.contains(uuids, BluetoothUuid.SAP)) {
             profiles.add(mSapProfile);
             removedProfiles.remove(mSapProfile);
diff --git a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncher.java b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncher.java
index f9d3aaf..ba63a8e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncher.java
+++ b/packages/SettingsLib/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncher.java
@@ -102,7 +102,7 @@
     protected boolean isSameProfileGroup(Context context, int enforcementAdminUserId) {
         UserManager um = context.getSystemService(UserManager.class);
 
-        return um.isSameProfileGroup(enforcementAdminUserId, um.getUserHandle());
+        return um.isSameProfileGroup(enforcementAdminUserId, um.getProcessUserId());
     }
 
     private boolean isEnforcedByDeviceOwnerOnSystemUserMode(
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncherTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncherTest.java
index c2063c6..47556da 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncherTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/enterprise/ActionDisabledLearnMoreButtonLauncherTest.java
@@ -80,7 +80,7 @@
     @Before
     public void setUp() {
         when(mContext.getUserId()).thenReturn(CONTEXT_USER_ID);
-        when(mUserManager.getUserHandle()).thenReturn(CONTEXT_USER_ID);
+        when(mUserManager.getProcessUserId()).thenReturn(CONTEXT_USER_ID);
         when(mContext.getSystemService(DevicePolicyManager.class)).thenReturn(mDevicePolicyManager);
         when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
     }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 4ac1938..cbee982 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -805,12 +805,6 @@
                 Settings.Global.HDMI_CONTROL_ENABLED,
                 GlobalSettingsProto.Hdmi.CONTROL_ENABLED);
         dumpSetting(s, p,
-                Settings.Global.HDMI_SYSTEM_AUDIO_CONTROL_ENABLED,
-                GlobalSettingsProto.Hdmi.SYSTEM_AUDIO_CONTROL_ENABLED);
-        dumpSetting(s, p,
-                Settings.Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED,
-                GlobalSettingsProto.Hdmi.CONTROL_AUTO_WAKEUP_ENABLED);
-        dumpSetting(s, p,
                 Settings.Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED,
                 GlobalSettingsProto.Hdmi.CONTROL_AUTO_DEVICE_OFF_ENABLED);
         p.end(hdmiToken);
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index df7bf20..3c700dc 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -299,13 +299,8 @@
                     Settings.Global.GNSS_HAL_LOCATION_REQUEST_DURATION_MILLIS,
                     Settings.Global.GNSS_SATELLITE_BLOCKLIST,
                     Settings.Global.GPRS_REGISTER_CHECK_PERIOD_MS,
-                    Settings.Global.HDMI_CEC_SWITCH_ENABLED,
                     Settings.Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED,
-                    Settings.Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED,
                     Settings.Global.HDMI_CONTROL_ENABLED,
-                    Settings.Global.HDMI_CONTROL_SEND_STANDBY_ON_SLEEP,
-                    Settings.Global.HDMI_CONTROL_VOLUME_CONTROL_ENABLED,
-                    Settings.Global.HDMI_SYSTEM_AUDIO_CONTROL_ENABLED,
                     Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED,
                     Settings.Global.HIDDEN_API_POLICY,
                     Settings.Global.FORCE_NON_DEBUGGABLE_FINAL_BUILD_FOR_COMPAT,
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index b9eec6e..7d06620 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -460,10 +460,17 @@
     <!-- Permission needed for CTS test - MatchContentFrameRateTest -->
     <uses-permission android:name="android.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE" />
 
+    <!-- Permission needed for CTS test - DefaultDisplayModeTest -->
+    <uses-permission android:name="android.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE" />
+
     <!-- Permissions needed for CTS test - TimeManagerTest -->
     <uses-permission android:name="android.permission.MANAGE_TIME_AND_ZONE_DETECTION" />
     <uses-permission android:name="android.permission.SUGGEST_EXTERNAL_TIME" />
 
+    <!-- Permissions needed for testing locale manager service -->
+    <!-- todo(b/201957547): Add CTS test name when available-->
+    <uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
+
     <!-- Permission required for CTS test - android.server.biometrics -->
     <uses-permission android:name="android.permission.USE_BIOMETRIC" />
 
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d7e4d72..9569cf9 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -143,6 +143,17 @@
     path: "tests/src",
 }
 
+java_library {
+    name: "SystemUI-tests-concurrency",
+    srcs: [
+        "src/com/android/systemui/util/concurrency/DelayableExecutor.java",
+        "src/com/android/systemui/util/time/SystemClock.java",
+        "tests/src/com/android/systemui/util/concurrency/FakeExecutor.java",
+        "tests/src/com/android/systemui/util/time/FakeSystemClock.java",
+    ],
+    jarjar_rules: ":jarjar-rules-shared",
+}
+
 android_library {
     name: "SystemUI-tests",
     manifest: "tests/AndroidManifest-base.xml",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 8d02af1..eb5ad25 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -149,6 +149,9 @@
     <uses-permission android:name="android.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS" />
     <uses-permission android:name="android.permission.GET_RUNTIME_PERMISSIONS" />
 
+    <!-- Communal mode -->
+    <uses-permission android:name="android.permission.WRITE_COMMUNAL_STATE" />
+
     <!-- Needed for WallpaperManager.clear in ImageWallpaper.updateWallpaperLocked -->
     <uses-permission android:name="android.permission.SET_WALLPAPER"/>
 
@@ -183,6 +186,9 @@
     <permission android:name="com.android.systemui.permission.PLUGIN"
             android:protectionLevel="signature" />
 
+    <permission android:name="com.android.systemui.permission.FLAGS"
+                android:protectionLevel="signature" />
+
     <!-- Adding Quick Settings tiles -->
     <uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" />
 
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java b/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
index 2765882..a3d924f 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/Interpolators.java
@@ -200,10 +200,10 @@
     /**
      * Interpolate alpha for notifications background scrim during shade expansion.
      * @param fraction Shade expansion fraction
-     * @param forNotification If we want the alpha of the notification shade or the scrim.
+     * @param forUiContent If we want the alpha of the scrims, or ui that's on top of them.
      */
-    public static float getNotificationScrimAlpha(float fraction, boolean forNotification) {
-        if (forNotification) {
+    public static float getNotificationScrimAlpha(float fraction, boolean forUiContent) {
+        if (forUiContent) {
             fraction = MathUtils.constrainedMap(0f, 1f, 0.3f, 1f, fraction);
         } else {
             fraction = MathUtils.constrainedMap(0f, 1f, 0f, 0.5f, fraction);
diff --git a/packages/SystemUI/docs/keyguard.md b/packages/SystemUI/docs/keyguard.md
new file mode 100644
index 0000000..5e7bc1c
--- /dev/null
+++ b/packages/SystemUI/docs/keyguard.md
@@ -0,0 +1,46 @@
+# Keyguard (aka Lockscreen)
+
+Keyguard is responsible for:
+
+1. Handling authentication to allow the user to unlock the device, via biometrics or [KeyguardBouncer][1]
+2. Displaying informational content such as the time, notifications, and smartspace
+3. Always-on Display (AOD)
+
+Keyguard is the first screen available when turning on the device, as long as the user has not specified a security method of NONE.
+
+## Critical User Journeys
+
+The journeys below generally refer to Keyguard's portion of the overall flow, especially regarding use of the power button. Power button key interpretation (short press, long press, very long press, multi press) is done in [PhoneWindowManager][4], with calls to [PowerManagerService][2] to sleep or wake up, if needed.
+
+### Power On - AOD enabled or disabled
+
+Begins with the device in low power mode, with the display active for [AOD][3] or inactive. [PowerManagerService][2] can be directed to wake up on various user-configurable signals, such as lift to wake, screen taps, among others. [AOD][2], whether visibly enabled or not, handles these signals to transition AOD to full Lockscreen content. See more in [AOD][3].
+
+### Power Off
+
+An indication to power off the device most likely comes from one of two signals: the user presses the power button or the screen timeout has passed. This may [lock the device](#How-the-device-locks)
+
+#### On Lockscreen
+
+#### On Lockscreen, occluded by an activity
+
+#### Device unlocked, Keyguard has gone away
+
+### Pulsing (Incoming notifications while dozing)
+
+### How the device locks
+
+More coming
+* Screen timeout
+* Smart lock
+* Device policy
+* Power button instantly locks setting
+* Lock timeout after screen timeout setting
+
+
+[1]: /frameworks/base/packages/SystemUI/docs/keyguard/bouncer.md
+[2]: /frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
+[3]: /frameworks/base/packages/SystemUI/docs/keyguard/aod.md
+[4]: /frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
+
+
diff --git a/packages/SystemUI/docs/keyguard/aod.md b/packages/SystemUI/docs/keyguard/aod.md
new file mode 100644
index 0000000..6d76ed55
--- /dev/null
+++ b/packages/SystemUI/docs/keyguard/aod.md
@@ -0,0 +1 @@
+# Always-on Display (AOD)
diff --git a/packages/SystemUI/docs/keyguard/bouncer.md b/packages/SystemUI/docs/keyguard/bouncer.md
new file mode 100644
index 0000000..51f8516
--- /dev/null
+++ b/packages/SystemUI/docs/keyguard/bouncer.md
@@ -0,0 +1,18 @@
+# Bouncer
+
+[KeyguardBouncer][1] is the component responsible for displaying the security method set by the user (password, PIN, pattern) as well as SIM-related security methods, allowing the user to unlock the device or SIM.
+
+## Components
+
+The bouncer contains a hierarchy of controllers/views to render the user's security method and to manage the authentication attempts.
+
+1. [KeyguardBouncer][1] - Entrypoint for managing the bouncer visibility.
+    1. [KeyguardHostViewController][2] - Intercepts media keys. Can most likely be merged with the next item.
+        1. [KeyguardSecurityContainerController][3] - Manages unlock attempt responses, one-handed use
+            1. [KeyguardSecurityViewFlipperController][4] - Based upon the [KeyguardSecurityModel#SecurityMode][5], will instantiate the required view and controller. PIN, Pattern, etc.
+
+[1]: /frameworks/base/packages/SystemUI/com/android/systemui/statusbar/phone/KeyguardBouncer
+[2]: /frameworks/base/packages/SystemUI/com/android/keyguard/KeyguardHostViewController
+[3]: /frameworks/base/packages/SystemUI/com/android/keyguard/KeyguardSecurityContainerController
+[4]: /frameworks/base/packages/SystemUI/com/android/keyguard/KeyguardSecurityViewFlipperController
+[5]: /frameworks/base/packages/SystemUI/com/android/keyguard/KeyguardSecurityModel
diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
index e026012..1844288 100644
--- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
+++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
@@ -249,6 +249,9 @@
                 val population = populationByColor[entry.key]!!
                 val cam = camByColor[entry.key]!!
                 val hue = cam.hue.roundToInt() % 360
+                if (cam.chroma <= MIN_CHROMA) {
+                    continue
+                }
                 huePopulation[hue] = huePopulation[hue] + population
             }
 
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/plugin/src/com/android/systemui/flags/Flags.java
index d5b9243..17b13a2 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/flags/Flags.java
@@ -16,8 +16,53 @@
 
 package com.android.systemui.flags;
 
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * List of {@link Flag} objects for use in SystemUI.
+ *
+ * Flag Ids are integers. They must be unique.
+ *
+ * On public release builds, flags will always return their default value. There is no way to
+ * change their value on release builds.
  */
 public class Flags {
+    public static final BooleanFlag THE_FIRST_FLAG = new BooleanFlag(1, false);
+
+
+    // Pay no attention to the reflection behind the curtain.
+    // ========================== Curtain ==========================
+    // |                                                           |
+    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
+    private static Map<Integer, Flag<?>> sFlagMap;
+    static Map<Integer, Flag<?>> collectFlags() {
+        if (sFlagMap != null) {
+            return sFlagMap;
+        }
+        Map<Integer, Flag<?>> flags = new HashMap<>();
+
+        Field[] fields = Flags.class.getFields();
+
+        for (Field field : fields) {
+            Class<?> t = field.getType();
+            if (Flag.class.isAssignableFrom(t)) {
+                try {
+                    Flag<?> flag = (Flag<?>) field.get(null);
+                    flags.put(flag.getId(), flag);
+                } catch (IllegalAccessException e) {
+                    // no-op
+                }
+            }
+        }
+
+        sFlagMap = flags;
+
+        return sFlagMap;
+    }
+    // |  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
+    // |                                                           |
+    // \_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/\_/
+
 }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
index 0424382..757dc2e 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
@@ -16,7 +16,6 @@
 
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.view.ViewGroup;
 
 import com.android.systemui.plugins.FragmentBase;
 import com.android.systemui.plugins.annotations.DependsOn;
@@ -53,10 +52,19 @@
     boolean isShowingDetail();
     void closeDetail();
     void animateHeaderSlidingOut();
-    void setQsExpansion(float qsExpansionFraction, float headerTranslation);
+
+    /**
+     * Asks QS to update its presentation, according to {@code NotificationPanelViewController}.
+     *
+     * @param qsExpansionFraction How much each UI element in QS should be expanded (QQS to QS.)
+     * @param panelExpansionFraction Whats the expansion of the whole shade.
+     * @param headerTranslation How much we should vertically translate QS.
+     */
+    void setQsExpansion(float qsExpansionFraction, float panelExpansionFraction,
+            float headerTranslation);
     void setHeaderListening(boolean listening);
     void notifyCustomizeChanged();
-    void setContainer(ViewGroup container);
+    void setContainerController(QSContainerController controller);
     void setExpandClickListener(OnClickListener onClickListener);
 
     View getHeader();
@@ -76,13 +84,13 @@
     /**
      * If QS should translate as we pull it down, or if it should be static.
      */
-    void setTranslateWhileExpanding(boolean shouldTranslate);
+    void setInSplitShade(boolean shouldTranslate);
 
     /**
      * Set the amount of pixels we have currently dragged down if we're transitioning to the full
      * shade. 0.0f means we're not transitioning yet.
      */
-    default void setTransitionToFullShadeAmount(float pxAmount, boolean animated) {}
+    default void setTransitionToFullShadeAmount(float pxAmount, float progress) {}
 
     /**
      * A rounded corner clipping that makes QS feel as if it were behind everything.
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainerController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainerController.kt
new file mode 100644
index 0000000..8bf982d
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSContainerController.kt
@@ -0,0 +1,9 @@
+package com.android.systemui.plugins.qs
+
+interface QSContainerController {
+    fun setCustomizerAnimating(animating: Boolean)
+
+    fun setCustomizerShowing(showing: Boolean)
+
+    fun setDetailShowing(showing: Boolean)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
index 6c5c4ef..9829918 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/StatusBarStateController.java
@@ -109,9 +109,8 @@
          * Callback to be notified when the fullscreen or immersive state changes.
          *
          * @param isFullscreen if any of the system bar is hidden by the focused window.
-         * @param isImmersive if the navigation bar can stay hidden when the display gets tapped.
          */
-        default void onFullscreenStateChanged(boolean isFullscreen, boolean isImmersive) {}
+        default void onFullscreenStateChanged(boolean isFullscreen) {}
 
         /**
          * Callback to be notified when the pulsing state changes
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
index 28c6166..87a9825 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
@@ -51,7 +51,7 @@
         android:id="@+id/lockscreen_clock_view_large"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_below="@id/keyguard_status_area"
+        android:layout_below="@id/keyguard_slice_view"
         android:visibility="gone">
         <com.android.keyguard.AnimatableClockView
             android:id="@+id/animatable_clock_view_large"
@@ -68,19 +68,28 @@
             lockScreenWeight="400"
         />
     </FrameLayout>
-    <include layout="@layout/keyguard_status_area"
+
+    <!-- Not quite optimal but needed to translate these items as a group. The
+         NotificationIconContainer has its own logic for translation. -->
+    <LinearLayout
         android:id="@+id/keyguard_status_area"
+        android:orientation="vertical"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_alignParentStart="true"
-        android:layout_below="@id/lockscreen_clock_view" />
+        android:layout_below="@id/lockscreen_clock_view">
 
-    <com.android.systemui.statusbar.phone.NotificationIconContainer
-        android:id="@+id/left_aligned_notification_icon_container"
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/notification_shelf_height"
-        android:layout_below="@id/keyguard_status_area"
-        android:paddingStart="@dimen/below_clock_padding_start_icons"
-        android:visibility="invisible"
-    />
+      <include layout="@layout/keyguard_slice_view"
+               android:id="@+id/keyguard_slice_view"
+               android:layout_width="match_parent"
+               android:layout_height="wrap_content" />
+
+      <com.android.systemui.statusbar.phone.NotificationIconContainer
+          android:id="@+id/left_aligned_notification_icon_container"
+          android:layout_width="match_parent"
+          android:layout_height="@dimen/notification_shelf_height"
+          android:paddingStart="@dimen/below_clock_padding_start_icons"
+          android:visibility="invisible"
+          />
+    </LinearLayout>
 </com.android.keyguard.KeyguardClockSwitch>
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_status_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml
similarity index 89%
rename from packages/SystemUI/res-keyguard/layout/keyguard_status_area.xml
rename to packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml
index 95eb5c1..1863d11 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_status_area.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml
@@ -22,11 +22,10 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_gravity="center_horizontal"
+    android:layout_gravity="start"
     android:clipToPadding="false"
     android:orientation="vertical"
-    android:paddingStart="@dimen/below_clock_padding_start"
-    android:layout_centerHorizontal="true">
+    android:paddingStart="@dimen/below_clock_padding_start">
     <TextView
               android:id="@+id/title"
               android:layout_width="match_parent"
@@ -42,6 +41,6 @@
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:orientation="horizontal"
-              android:gravity="center"
+              android:gravity="start"
     />
 </com.android.keyguard.KeyguardSliceView>
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_button_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_button_background.xml
new file mode 100644
index 0000000..363a022
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_button_background.xml
@@ -0,0 +1,29 @@
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+       android:shape="rectangle">
+    <stroke
+        android:color="?androidprv:attr/colorAccentPrimaryVariant"
+        android:width="1dp"/>
+    <corners android:radius="20dp"/>
+    <padding
+        android:left="16dp"
+        android:right="16dp"
+        android:top="8dp"
+        android:bottom="8dp" />
+    <solid android:color="@android:color/transparent" />
+</shape>
diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml
index b338894..cd6bc11 100644
--- a/packages/SystemUI/res/layout/media_output_dialog.xml
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -24,41 +24,44 @@
 
     <LinearLayout
         android:layout_width="match_parent"
-        android:layout_height="94dp"
+        android:layout_height="96dp"
         android:gravity="start|center_vertical"
         android:paddingStart="16dp"
         android:orientation="horizontal">
         <ImageView
             android:id="@+id/header_icon"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:paddingEnd="@dimen/media_output_dialog_header_icon_padding"
+            android:layout_width="48dp"
+            android:layout_height="48dp"
             android:importantForAccessibility="no"/>
 
         <LinearLayout
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginEnd="16dp"
+            android:layout_height="match_parent"
+            android:paddingStart="16dp"
+            android:paddingTop="20dp"
+            android:paddingBottom="24dp"
+            android:paddingEnd="24dp"
             android:orientation="vertical">
             <TextView
                 android:id="@+id/header_title"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:ellipsize="end"
+                android:gravity="center_vertical"
                 android:maxLines="1"
                 android:textColor="?android:attr/textColorPrimary"
                 android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
                 android:textSize="20sp"/>
-
             <TextView
                 android:id="@+id/header_subtitle"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:gravity="center_vertical"
                 android:ellipsize="end"
                 android:maxLines="1"
+                android:textColor="?android:attr/textColorTertiary"
                 android:fontFamily="roboto-regular"
-                android:textSize="14sp"/>
-
+                android:textSize="16sp"/>
         </LinearLayout>
     </LinearLayout>
 
@@ -81,21 +84,21 @@
             android:overScrollMode="never"/>
     </LinearLayout>
 
-    <View
-        android:layout_width="match_parent"
-        android:layout_height="1dp"
-        android:background="?android:attr/listDivider"/>
-
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:layout_marginStart="24dp"
+        android:layout_marginBottom="18dp"
+        android:layout_marginEnd="24dp"
         android:orientation="horizontal">
 
         <Button
             android:id="@+id/stop"
-            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+            style="@style/MediaOutputRoundedOutlinedButton"
             android:layout_width="wrap_content"
-            android:layout_height="64dp"
+            android:layout_height="36dp"
+            android:minWidth="0dp"
             android:text="@string/keyboard_key_media_stop"
             android:visibility="gone"/>
 
@@ -106,10 +109,10 @@
 
         <Button
             android:id="@+id/done"
-            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+            style="@style/MediaOutputRoundedOutlinedButton"
             android:layout_width="wrap_content"
-            android:layout_height="64dp"
-            android:layout_marginEnd="0dp"
+            android:layout_height="36dp"
+            android:minWidth="0dp"
             android:text="@string/inline_done_button"/>
     </LinearLayout>
 </LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_output_list_item.xml b/packages/SystemUI/res/layout/media_output_list_item.xml
index 16c03e1..a5a7efa 100644
--- a/packages/SystemUI/res/layout/media_output_list_item.xml
+++ b/packages/SystemUI/res/layout/media_output_list_item.xml
@@ -23,17 +23,20 @@
     android:orientation="vertical">
     <FrameLayout
         android:layout_width="match_parent"
-        android:layout_height="64dp">
+        android:layout_height="88dp"
+        android:paddingTop="24dp"
+        android:paddingBottom="16dp"
+        android:paddingStart="24dp"
+        android:paddingEnd="8dp">
 
         <FrameLayout
-            android:layout_width="36dp"
-            android:layout_height="36dp"
-            android:layout_gravity="center_vertical|start"
-            android:layout_marginStart="16dp">
+            android:layout_width="48dp"
+            android:layout_height="48dp"
+            android:layout_gravity="center_vertical|start">
             <ImageView
                 android:id="@+id/title_icon"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
+                android:layout_width="48dp"
+                android:layout_height="48dp"
                 android:layout_gravity="center"/>
         </FrameLayout>
 
@@ -42,49 +45,69 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical|start"
-            android:layout_marginStart="68dp"
+            android:layout_marginStart="64dp"
             android:ellipsize="end"
             android:maxLines="1"
             android:textColor="?android:attr/textColorPrimary"
-            android:textSize="14sp"/>
+            android:textSize="16sp"/>
 
         <RelativeLayout
             android:id="@+id/two_line_layout"
             android:layout_width="wrap_content"
             android:layout_height="48dp"
-            android:layout_marginStart="52dp"
-            android:layout_marginEnd="69dp"
-            android:layout_marginTop="10dp">
+            android:layout_marginStart="48dp">
             <TextView
                 android:id="@+id/two_line_title"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_marginStart="16dp"
-                android:layout_marginEnd="15dp"
+                android:layout_marginEnd="48dp"
                 android:ellipsize="end"
                 android:maxLines="1"
                 android:textColor="?android:attr/textColorPrimary"
-                android:textSize="14sp"/>
+                android:textSize="16sp"/>
             <TextView
                 android:id="@+id/subtitle"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_marginStart="16dp"
                 android:layout_marginEnd="15dp"
-                android:layout_marginBottom="7dp"
+                android:layout_marginTop="4dp"
                 android:layout_alignParentBottom="true"
                 android:ellipsize="end"
                 android:maxLines="1"
                 android:textColor="?android:attr/textColorSecondary"
-                android:textSize="12sp"
+                android:textSize="14sp"
                 android:fontFamily="roboto-regular"
                 android:visibility="gone"/>
             <SeekBar
                 android:id="@+id/volume_seekbar"
+                android:layout_marginTop="16dp"
+                android:layout_marginEnd="8dp"
                 style="@*android:style/Widget.DeviceDefault.SeekBar"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_alignParentBottom="true"/>
+            <ImageView
+                android:id="@+id/add_icon"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_gravity="right"
+                android:layout_marginEnd="24dp"
+                android:layout_alignParentRight="true"
+                android:src="@drawable/ic_add"
+                android:tint="?android:attr/colorAccent"
+            />
+            <CheckBox
+                android:id="@+id/check_box"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:layout_gravity="right"
+                android:layout_marginEnd="24dp"
+                android:layout_alignParentRight="true"
+                android:button="@drawable/ic_check_box"
+                android:visibility="gone"
+                />
         </RelativeLayout>
 
         <ProgressBar
@@ -92,47 +115,17 @@
             style="@*android:style/Widget.Material.ProgressBar.Horizontal"
             android:layout_width="258dp"
             android:layout_height="18dp"
-            android:layout_marginStart="68dp"
-            android:layout_marginTop="40dp"
+            android:layout_marginStart="64dp"
+            android:layout_marginTop="28dp"
             android:indeterminate="true"
             android:indeterminateOnly="true"
             android:visibility="gone"/>
-
-        <View
-            android:id="@+id/end_divider"
-            android:layout_width="1dp"
-            android:layout_height="36dp"
-            android:layout_marginEnd="68dp"
-            android:layout_gravity="right|center_vertical"
-            android:background="?android:attr/listDivider"
-            android:visibility="gone"/>
-
-        <ImageView
-            android:id="@+id/add_icon"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:layout_gravity="right|center_vertical"
-            android:layout_marginEnd="24dp"
-            android:src="@drawable/ic_add"
-            android:tint="?android:attr/colorAccent"
-            android:visibility="gone"/>
-
-        <CheckBox
-            android:id="@+id/check_box"
-            android:layout_width="24dp"
-            android:layout_height="24dp"
-            android:layout_gravity="right|center_vertical"
-            android:layout_marginEnd="24dp"
-            android:button="@drawable/ic_check_box"
-            android:visibility="gone"/>
     </FrameLayout>
 
     <View
         android:id="@+id/bottom_divider"
         android:layout_width="match_parent"
         android:layout_height="1dp"
-        android:layout_marginTop="12dp"
-        android:layout_marginBottom="12dp"
         android:layout_gravity="bottom"
         android:background="?android:attr/listDivider"
         android:visibility="gone"/>
diff --git a/packages/SystemUI/res/layout/qs_user_dialog_content.xml b/packages/SystemUI/res/layout/qs_user_dialog_content.xml
index 321fe68..543b7d7 100644
--- a/packages/SystemUI/res/layout/qs_user_dialog_content.xml
+++ b/packages/SystemUI/res/layout/qs_user_dialog_content.xml
@@ -16,74 +16,78 @@
   ~ limitations under the License.
   -->
 
-<androidx.constraintlayout.widget.ConstraintLayout
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:sysui="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:padding="24dp"
-    android:layout_marginStart="16dp"
-    android:layout_marginEnd="16dp"
-    android:background="@drawable/qs_dialog_bg"
->
-    <TextView
-        android:id="@+id/title"
+    android:layout_height="wrap_content">
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_width="0dp"
-        android:textAlignment="center"
-        android:text="@string/qs_user_switch_dialog_title"
-        android:textAppearance="@style/TextAppearance.QSDialog.Title"
-        android:layout_marginBottom="32dp"
-        sysui:layout_constraintTop_toTopOf="parent"
-        sysui:layout_constraintStart_toStartOf="parent"
-        sysui:layout_constraintEnd_toEndOf="parent"
-        sysui:layout_constraintBottom_toTopOf="@id/grid"
+        android:padding="24dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginEnd="16dp"
+    >
+        <TextView
+            android:id="@+id/title"
+            android:layout_height="wrap_content"
+            android:layout_width="0dp"
+            android:textAlignment="center"
+            android:text="@string/qs_user_switch_dialog_title"
+            android:textAppearance="@style/TextAppearance.QSDialog.Title"
+            android:layout_marginBottom="32dp"
+            sysui:layout_constraintTop_toTopOf="parent"
+            sysui:layout_constraintStart_toStartOf="parent"
+            sysui:layout_constraintEnd_toEndOf="parent"
+            sysui:layout_constraintBottom_toTopOf="@id/grid"
+            />
+
+        <com.android.systemui.qs.PseudoGridView
+            android:id="@+id/grid"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="28dp"
+            sysui:verticalSpacing="4dp"
+            sysui:horizontalSpacing="4dp"
+            sysui:fixedChildWidth="80dp"
+            sysui:layout_constraintTop_toBottomOf="@id/title"
+            sysui:layout_constraintStart_toStartOf="parent"
+            sysui:layout_constraintEnd_toEndOf="parent"
+            sysui:layout_constraintBottom_toTopOf="@id/barrier"
         />
 
-    <com.android.systemui.qs.PseudoGridView
-        android:id="@+id/grid"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginBottom="28dp"
-        sysui:verticalSpacing="4dp"
-        sysui:horizontalSpacing="4dp"
-        sysui:fixedChildWidth="80dp"
-        sysui:layout_constraintTop_toBottomOf="@id/title"
-        sysui:layout_constraintStart_toStartOf="parent"
-        sysui:layout_constraintEnd_toEndOf="parent"
-        sysui:layout_constraintBottom_toTopOf="@id/barrier"
-    />
+        <androidx.constraintlayout.widget.Barrier
+            android:id="@+id/barrier"
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            sysui:barrierDirection="top"
+            sysui:constraint_referenced_ids="settings,done"
+            />
 
-    <androidx.constraintlayout.widget.Barrier
-        android:id="@+id/barrier"
-        android:layout_height="wrap_content"
-        android:layout_width="wrap_content"
-        sysui:barrierDirection="top"
-        sysui:constraint_referenced_ids="settings,done"
-        />
+        <Button
+            android:id="@+id/settings"
+            android:layout_width="wrap_content"
+            android:layout_height="48dp"
+            android:text="@string/quick_settings_more_user_settings"
+            sysui:layout_constraintTop_toBottomOf="@id/barrier"
+            sysui:layout_constraintBottom_toBottomOf="parent"
+            sysui:layout_constraintStart_toStartOf="parent"
+            sysui:layout_constraintEnd_toStartOf="@id/done"
+            sysui:layout_constraintHorizontal_chainStyle="spread_inside"
+            style="@style/Widget.QSDialog.Button.BorderButton"
+            />
 
-    <Button
-        android:id="@+id/settings"
-        android:layout_width="wrap_content"
-        android:layout_height="48dp"
-        android:text="@string/quick_settings_more_user_settings"
-        sysui:layout_constraintTop_toBottomOf="@id/barrier"
-        sysui:layout_constraintBottom_toBottomOf="parent"
-        sysui:layout_constraintStart_toStartOf="parent"
-        sysui:layout_constraintEnd_toStartOf="@id/done"
-        sysui:layout_constraintHorizontal_chainStyle="spread_inside"
-        style="@style/Widget.QSDialog.Button.BorderButton"
-        />
+        <Button
+            android:id="@+id/done"
+            android:layout_width="wrap_content"
+            android:layout_height="48dp"
+            android:text="@string/quick_settings_done"
+            sysui:layout_constraintTop_toBottomOf="@id/barrier"
+            sysui:layout_constraintBottom_toBottomOf="parent"
+            sysui:layout_constraintStart_toEndOf="@id/settings"
+            sysui:layout_constraintEnd_toEndOf="parent"
+            style="@style/Widget.QSDialog.Button"
+            />
 
-    <Button
-        android:id="@+id/done"
-        android:layout_width="wrap_content"
-        android:layout_height="48dp"
-        android:text="@string/quick_settings_done"
-        sysui:layout_constraintTop_toBottomOf="@id/barrier"
-        sysui:layout_constraintBottom_toBottomOf="parent"
-        sysui:layout_constraintStart_toEndOf="@id/settings"
-        sysui:layout_constraintEnd_toEndOf="parent"
-        style="@style/Widget.QSDialog.Button"
-        />
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index a58e12f..702a354 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -69,18 +69,6 @@
             android:layout_gravity="center"
             android:scaleType="centerCrop"/>
 
-        <!-- Fingerprint -->
-        <!-- AOD dashed fingerprint icon with moving dashes -->
-        <com.airbnb.lottie.LottieAnimationView
-            android:id="@+id/lock_udfps_aod_fp"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:padding="@dimen/lock_icon_padding"
-            android:layout_gravity="center"
-            android:scaleType="centerCrop"
-            systemui:lottie_autoPlay="false"
-            systemui:lottie_loop="true"
-            systemui:lottie_rawRes="@raw/udfps_aod_fp"/>
     </com.android.keyguard.LockIconView>
 
     <com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index d9a5670..e02a1767 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -97,7 +97,7 @@
             android:singleLine="true"
             android:ellipsize="marquee"
             android:focusable="true" />
-        <FrameLayout android:id="@+id/keyboard_bouncer_container"
+        <FrameLayout android:id="@+id/keyguard_bouncer_container"
                      android:layout_height="0dp"
                      android:layout_width="match_parent"
                      android:layout_weight="1" />
diff --git a/packages/SystemUI/res/layout/udfps_aod_lock_icon.xml b/packages/SystemUI/res/layout/udfps_aod_lock_icon.xml
new file mode 100644
index 0000000..f5bfa49
--- /dev/null
+++ b/packages/SystemUI/res/layout/udfps_aod_lock_icon.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<com.airbnb.lottie.LottieAnimationView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:systemui="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/lock_udfps_aod_fp"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="@dimen/lock_icon_padding"
+    android:layout_gravity="center"
+    android:scaleType="centerCrop"
+    systemui:lottie_autoPlay="false"
+    systemui:lottie_loop="true"
+    systemui:lottie_rawRes="@raw/udfps_aod_fp"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-es/strings.xml b/packages/SystemUI/res/values-es/strings.xml
index ac93bf5..7a97f1ea 100644
--- a/packages/SystemUI/res/values-es/strings.xml
+++ b/packages/SystemUI/res/values-es/strings.xml
@@ -124,8 +124,8 @@
     <string name="screenrecord_permission_error" msgid="7856841237023137686">"No se han podido obtener los permisos"</string>
     <string name="screenrecord_start_error" msgid="2200660692479682368">"No se ha podido empezar a grabar la pantalla"</string>
     <string name="usb_preference_title" msgid="1439924437558480718">"Opciones de transferencia de archivos por USB"</string>
-    <string name="use_mtp_button_title" msgid="5036082897886518086">"Activar como reproductor de medios (MTP)"</string>
-    <string name="use_ptp_button_title" msgid="7676427598943446826">"Activar como cámara (PTP)"</string>
+    <string name="use_mtp_button_title" msgid="5036082897886518086">"Montar como reproductor de medios (MTP)"</string>
+    <string name="use_ptp_button_title" msgid="7676427598943446826">"Montar como cámara (PTP)"</string>
     <string name="installer_cd_button_title" msgid="5499998592841984743">"Instalar Android File Transfer para Mac"</string>
     <string name="accessibility_back" msgid="6530104400086152611">"Atrás"</string>
     <string name="accessibility_home" msgid="5430449841237966217">"Inicio"</string>
diff --git a/packages/SystemUI/res/values-mr/strings.xml b/packages/SystemUI/res/values-mr/strings.xml
index eb2d4c2..485ed67 100644
--- a/packages/SystemUI/res/values-mr/strings.xml
+++ b/packages/SystemUI/res/values-mr/strings.xml
@@ -848,7 +848,7 @@
     <string name="keyboard_shortcut_group_applications_sms" msgid="6912633831752843566">"SMS"</string>
     <string name="keyboard_shortcut_group_applications_music" msgid="9032078456666204025">"संगीत"</string>
     <string name="keyboard_shortcut_group_applications_youtube" msgid="5078136084632450333">"YouTube"</string>
-    <string name="keyboard_shortcut_group_applications_calendar" msgid="4229602992120154157">"Calendar"</string>
+    <string name="keyboard_shortcut_group_applications_calendar" msgid="4229602992120154157">"कॅलेंडर"</string>
     <string name="tuner_full_zen_title" msgid="5120366354224404511">"आवाज नियंत्रणांसह दर्शवा"</string>
     <string name="volume_and_do_not_disturb" msgid="502044092739382832">"व्यत्यय आणू नका"</string>
     <string name="volume_dnd_silent" msgid="4154597281458298093">"आवाजाच्या बटणांचा शार्टकट"</string>
diff --git a/packages/SystemUI/res/values-nb/strings.xml b/packages/SystemUI/res/values-nb/strings.xml
index e5206c0..fb3c9de 100644
--- a/packages/SystemUI/res/values-nb/strings.xml
+++ b/packages/SystemUI/res/values-nb/strings.xml
@@ -602,8 +602,8 @@
     <string name="volume_odi_captions_tip" msgid="8825655463280990941">"Automatisk medieteksting"</string>
     <string name="accessibility_volume_close_odi_captions_tip" msgid="8924753283621160480">"Verktøytips for teksting"</string>
     <string name="volume_odi_captions_content_description" msgid="4172765742046013630">"Overlegg med teksting"</string>
-    <string name="volume_odi_captions_hint_enable" msgid="2073091194012843195">"slå på"</string>
-    <string name="volume_odi_captions_hint_disable" msgid="2518846326748183407">"slå av"</string>
+    <string name="volume_odi_captions_hint_enable" msgid="2073091194012843195">"aktivér"</string>
+    <string name="volume_odi_captions_hint_disable" msgid="2518846326748183407">"deaktiver"</string>
     <string name="accessibility_output_chooser" msgid="7807898688967194183">"Bytt enhet for lydutgang"</string>
     <string name="screen_pinning_title" msgid="9058007390337841305">"Appen er festet"</string>
     <string name="screen_pinning_description" msgid="8699395373875667743">"Gjør at den vises til du løsner den. Trykk og hold inne Tilbake og Oversikt for å løsne den."</string>
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index ffcc3a8..07e28b6 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -16,7 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
-    <style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Dialog" />
+    <style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Dialog">
+        <item name="android:buttonCornerRadius">28dp</item>
+    </style>
 
     <style name="Theme.SystemUI.Dialog.Alert" parent="@*android:style/Theme.DeviceDefault.Dialog.Alert" />
 
diff --git a/packages/SystemUI/res/values-sw720dp-land/config.xml b/packages/SystemUI/res/values-sw720dp-land/config.xml
new file mode 100644
index 0000000..e4573c6
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-land/config.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<resources>
+    <!-- Max number of columns for quick controls area -->
+    <integer name="controls_max_columns">2</integer>
+
+    <!-- The maximum number of rows in the QuickQSPanel -->
+    <integer name="quick_qs_panel_max_rows">4</integer>
+
+    <!-- The maximum number of tiles in the QuickQSPanel -->
+    <integer name="quick_qs_panel_max_tiles">8</integer>
+
+    <!-- Whether to use the split 2-column notification shade -->
+    <bool name="config_use_split_notification_shade">true</bool>
+
+    <!-- The number of columns in the QuickSettings -->
+    <integer name="quick_settings_num_columns">2</integer>
+
+    <!-- Notifications are sized to match the width of two (of 4) qs tiles in landscape. -->
+    <bool name="config_skinnyNotifsInLandscape">false</bool>
+</resources>
diff --git a/packages/SystemUI/res/values-sw720dp-port/config.xml b/packages/SystemUI/res/values-sw720dp-port/config.xml
new file mode 100644
index 0000000..1225086
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-port/config.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2021, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<!-- These resources are around just to allow their values to be customized
+     for different hardware and product builds. -->
+<resources>
+    <!-- The maximum number of tiles in the QuickQSPanel -->
+    <integer name="quick_qs_panel_max_tiles">6</integer>
+
+    <!-- The number of columns in the QuickSettings -->
+    <integer name="quick_settings_num_columns">3</integer>
+
+    <!-- The maximum number of rows in the QuickSettings -->
+    <integer name="quick_settings_max_rows">3</integer>
+
+</resources>
+
diff --git a/packages/SystemUI/res/values-sw720dp/config.xml b/packages/SystemUI/res/values-sw720dp/config.xml
index ac44251..436f8d0 100644
--- a/packages/SystemUI/res/values-sw720dp/config.xml
+++ b/packages/SystemUI/res/values-sw720dp/config.xml
@@ -22,14 +22,5 @@
 <resources>
     <integer name="status_bar_config_maxNotificationIcons">5</integer>
 
-    <!-- The maximum number of tiles in the QuickQSPanel -->
-    <integer name="quick_qs_panel_max_tiles">6</integer>
-
-    <!-- The number of columns in the QuickSettings -->
-    <integer name="quick_settings_num_columns">3</integer>
-
-    <!-- The maximum number of rows in the QuickSettings -->
-    <integer name="quick_settings_max_rows">3</integer>
-
 </resources>
 
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 786b371..c2901a5 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -224,15 +224,23 @@
          display brightness, suitable to listen to while the device is asleep (e.g. during
          always-on display) -->
     <string-array name="doze_brightness_sensor_name_posture_mapping" translatable="false">
-        <item></item> <!-- UNKNOWN -->
-        <item></item> <!-- CLOSED -->
-        <item></item> <!-- HALF_OPENED -->
-        <item></item> <!-- OPENED -->
+        <!-- UNKNOWN -->
+        <!-- CLOSED -->
+        <!-- HALF_OPENED -->
+        <!-- OPENED -->
     </string-array>
 
     <!-- Override value to use for proximity sensor.  -->
     <string name="proximity_sensor_type" translatable="false"></string>
 
+    <!-- Sensor type per posture state to use for proximity sensor -->
+    <string-array name="proximity_sensor_posture_mapping" translatable="false">
+        <!-- UNKNOWN -->
+        <!-- CLOSED -->
+        <!-- HALF_OPENED -->
+        <!-- OPENED -->
+    </string-array>
+
     <!-- If using proximity_sensor_type, specifies a threshold value to distinguish near and
          far break points. A sensor value less than this is considered "near". -->
     <item name="proximity_sensor_threshold" translatable="false" format="float" type="dimen"></item>
@@ -246,6 +254,15 @@
     <!-- Override value to use for proximity sensor as confirmation for proximity_sensor_type. -->
     <string name="proximity_sensor_secondary_type" translatable="false"></string>
 
+    <!-- Sensor type per posture state to use for proximity sensor as a confirmation for
+        proximity_sensor_type. -->
+    <string-array name="proximity_sensor_secondary_posture_mapping" translatable="false">
+        <!-- UNKNOWN -->
+        <!-- CLOSED -->
+        <!-- HALF_OPENED -->
+        <!-- OPENED -->
+    </string-array>
+
     <!-- If using proximity_sensor_secondary_type, specifies a threshold value to distinguish
          near and far break points. A sensor value less than this is considered "near". -->
     <item name="proximity_sensor_secondary_threshold" translatable="false" format="float"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 8109e23..a108b42 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1436,10 +1436,10 @@
     <!-- Output switcher panel related dimensions -->
     <dimen name="media_output_dialog_list_margin">12dp</dimen>
     <dimen name="media_output_dialog_list_max_height">364dp</dimen>
-    <dimen name="media_output_dialog_header_album_icon_size">52dp</dimen>
-    <dimen name="media_output_dialog_header_back_icon_size">36dp</dimen>
+    <dimen name="media_output_dialog_header_album_icon_size">48dp</dimen>
+    <dimen name="media_output_dialog_header_back_icon_size">32dp</dimen>
     <dimen name="media_output_dialog_header_icon_padding">16dp</dimen>
-    <dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
+    <dimen name="media_output_dialog_icon_corner_radius">8dp</dimen>
     <dimen name="media_output_dialog_title_anim_y_delta">12.5dp</dimen>
 
     <!-- Distance that the full shade transition takes in order for qs to fully transition to the
@@ -1599,7 +1599,7 @@
 
     <!-- Internet panel related dimensions -->
     <dimen name="internet_dialog_list_margin">12dp</dimen>
-    <dimen name="internet_dialog_list_max_height">646dp</dimen>
+    <dimen name="internet_dialog_list_max_height">662dp</dimen>
 
     <!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) -->
     <dimen name="large_dialog_width">@dimen/match_parent</dimen>
diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml
index 0386217..db7a0ec 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -24,9 +24,6 @@
 
     <bool name="flag_monet">true</bool>
 
-    <!-- AOD/Lockscreen alternate layout -->
-    <bool name="flag_keyguard_layout">true</bool>
-
     <!-- People Tile flag -->
     <bool name="flag_conversations">false</bool>
 
@@ -51,6 +48,10 @@
 
     <bool name="flag_ongoing_call_status_bar_chip">true</bool>
 
+    <bool name="flag_ongoing_call_in_immersive">true</bool>
+
+    <bool name="flag_ongoing_call_in_immersive_chip_tap">true</bool>
+
     <bool name="flag_smartspace">false</bool>
 
     <bool name="flag_smartspace_deduping">true</bool>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index b3bdffd..1d0660e 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2830,6 +2830,8 @@
     <string name="controls_media_settings_button">Settings</string>
     <!-- Description for media control's playing media item, including information for the media's title, the artist, and source app [CHAR LIMIT=NONE]-->
     <string name="controls_media_playing_item_description"><xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
+    <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] -->
+    <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string>
 
     <!-- Title for Smartspace recommendation card within media controls. The "Play" means the action to play a media [CHAR_LIMIT=10] -->
     <string name="controls_media_smartspace_rec_title">Play</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 6ca368c..b6ef258 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -417,7 +417,9 @@
         <item name="android:windowIsFloating">true</item>
     </style>
 
-    <style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog" />
+    <style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog">
+        <item name="android:buttonCornerRadius">28dp</item>
+    </style>
 
     <style name="Theme.SystemUI.Dialog.Alert" parent="@*android:style/Theme.DeviceDefault.Light.Dialog.Alert" />
 
@@ -513,6 +515,10 @@
         <item name="android:background">@drawable/btn_borderless_rect</item>
     </style>
 
+    <style name="MediaOutputRoundedOutlinedButton" parent="@android:style/Widget.Material.Button">
+        <item name="android:background">@drawable/media_output_dialog_button_background</item>
+    </style>
+
     <style name="TunerSettings" parent="@android:style/Theme.DeviceDefault.Settings">
         <item name="android:windowActionBar">false</item>
         <item name="preferenceTheme">@style/TunerPreferenceTheme</item>
@@ -939,26 +945,14 @@
         <item name="actionDividerHeight">32dp</item>
     </style>
 
-    <style name="Theme.SystemUI.Dialog.QSDialog">
-        <item name="android:windowIsTranslucent">true</item>
-        <item name="android:windowBackground">@android:color/transparent</item>
-        <item name="android:windowIsFloating">true</item>
-        <item name="android:backgroundDimEnabled">true</item>
-        <item name="android:windowCloseOnTouchOutside">true</item>
-        <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
-        <item name="android:dialogCornerRadius">28dp</item>
-        <item name="android:buttonCornerRadius">28dp</item>
-        <item name="android:colorBackground">@color/prv_color_surface</item>
-    </style>
-
-    <style name="TextAppearance.QSDialog.Title" parent="Theme.SystemUI.Dialog.QSDialog">
+    <style name="TextAppearance.QSDialog.Title" parent="Theme.SystemUI.Dialog">
         <item name="android:textColor">?android:attr/textColorPrimary</item>
         <item name="android:textSize">24sp</item>
         <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item>
         <item name="android:lineHeight">32sp</item>
     </style>
 
-    <style name="Widget.QSDialog.Button" parent = "Theme.SystemUI.Dialog.QSDialog">
+    <style name="Widget.QSDialog.Button" parent = "Theme.SystemUI.Dialog">
         <item name="android:background">@drawable/qs_dialog_btn_filled</item>
         <item name="android:textColor">@color/prv_text_color_on_accent</item>
         <item name="android:textSize">14sp</item>
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 3a23094..4880b12 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -43,6 +43,7 @@
         "src/**/*.kt",
         "src/**/*.aidl",
         ":wm_shell-aidls",
+        ":wm_shell_util-sources",
     ],
 
     static_libs: [
@@ -50,5 +51,5 @@
         "androidx.dynamicanimation_dynamicanimation",
     ],
     java_version: "1.8",
-    min_sdk_version: "26",
+    min_sdk_version: "current",
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 11557ad..5b7e500 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -143,5 +143,12 @@
     /** Notifies when taskbar status updated */
     oneway void notifyTaskbarStatus(boolean visible, boolean stashed) = 47;
 
-    // Next id = 48
+    /**
+     * Notifies sysui when taskbar requests autoHide to stop auto-hiding
+     * If called to suspend, caller is also responsible for calling this method to un-suspend
+     * @param suspend should be true to stop auto-hide, false to resume normal behavior
+     */
+    oneway void notifyTaskbarAutohideSuspend(boolean suspend) = 48;
+
+    // Next id = 49
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
index 7aee721..dcc4ea1 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
@@ -40,7 +40,7 @@
 import android.window.RemoteTransition;
 import android.window.TransitionInfo;
 
-import java.util.ArrayList;
+import com.android.wm.shell.util.CounterRotator;
 
 /**
  * @see RemoteAnimationAdapter
@@ -109,52 +109,6 @@
         };
     }
 
-    private static class CounterRotator {
-        SurfaceControl mSurface = null;
-        ArrayList<SurfaceControl> mRotateChildren = null;
-
-        void setup(SurfaceControl.Transaction t, SurfaceControl parent, int rotateDelta,
-                float displayW, float displayH) {
-            if (rotateDelta == 0) return;
-            mRotateChildren = new ArrayList<>();
-            // We want to counter-rotate, so subtract from 4
-            rotateDelta = 4 - (rotateDelta + 4) % 4;
-            mSurface = new SurfaceControl.Builder()
-                    .setName("Transition Unrotate")
-                    .setContainerLayer()
-                    .setParent(parent)
-                    .build();
-            // column-major
-            if (rotateDelta == 1) {
-                t.setMatrix(mSurface, 0, 1, -1, 0);
-                t.setPosition(mSurface, displayW, 0);
-            } else if (rotateDelta == 2) {
-                t.setMatrix(mSurface, -1, 0, 0, -1);
-                t.setPosition(mSurface, displayW, displayH);
-            } else if (rotateDelta == 3) {
-                t.setMatrix(mSurface, 0, -1, 1, 0);
-                t.setPosition(mSurface, 0, displayH);
-            }
-            t.show(mSurface);
-        }
-
-        void addChild(SurfaceControl.Transaction t, SurfaceControl child) {
-            if (mSurface == null) return;
-            t.reparent(child, mSurface);
-            mRotateChildren.add(child);
-        }
-
-        void cleanUp(SurfaceControl rootLeash) {
-            if (mSurface == null) return;
-            SurfaceControl.Transaction t = new SurfaceControl.Transaction();
-            for (int i = mRotateChildren.size() - 1; i >= 0; --i) {
-                t.reparent(mRotateChildren.get(i), rootLeash);
-            }
-            t.remove(mSurface);
-            t.apply();
-        }
-    }
-
     private static IRemoteTransition.Stub wrapRemoteTransition(
             final RemoteAnimationRunnerCompat remoteAnimationAdapter) {
         return new IRemoteTransition.Stub() {
@@ -204,14 +158,14 @@
                 if (launcherTask != null && rotateDelta != 0 && launcherTask.getParent() != null) {
                     counterLauncher.setup(t, info.getChange(launcherTask.getParent()).getLeash(),
                             rotateDelta, displayW, displayH);
-                    if (counterLauncher.mSurface != null) {
-                        t.setLayer(counterLauncher.mSurface, launcherLayer);
+                    if (counterLauncher.getSurface() != null) {
+                        t.setLayer(counterLauncher.getSurface(), launcherLayer);
                     }
                 }
 
                 if (isReturnToHome) {
-                    if (counterLauncher.mSurface != null) {
-                        t.setLayer(counterLauncher.mSurface, info.getChanges().size() * 3);
+                    if (counterLauncher.getSurface() != null) {
+                        t.setLayer(counterLauncher.getSurface(), info.getChanges().size() * 3);
                     }
                     // Need to "boost" the closing things since that's what launcher expects.
                     for (int i = info.getChanges().size() - 1; i >= 0; --i) {
@@ -237,8 +191,8 @@
                     if (wallpaper != null && rotateDelta != 0 && wallpaper.getParent() != null) {
                         counterWallpaper.setup(t, info.getChange(wallpaper.getParent()).getLeash(),
                                 rotateDelta, displayW, displayH);
-                        if (counterWallpaper.mSurface != null) {
-                            t.setLayer(counterWallpaper.mSurface, -1);
+                        if (counterWallpaper.getSurface() != null) {
+                            t.setLayer(counterWallpaper.getSurface(), -1);
                             counterWallpaper.addChild(t, leashMap.get(wallpaper.getLeash()));
                         }
                     }
diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FeatureFlagManager.java b/packages/SystemUI/src-debug/com/android/systemui/flags/FeatureFlagManager.java
index b3a6699..2ed6328 100644
--- a/packages/SystemUI/src-debug/com/android/systemui/flags/FeatureFlagManager.java
+++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FeatureFlagManager.java
@@ -16,37 +16,142 @@
 
 package com.android.systemui.flags;
 
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.util.Log;
+
 import com.android.systemui.dagger.SysUISingleton;
 
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Map;
+
 import javax.inject.Inject;
 
 /**
  * Concrete implementation of the a Flag manager that returns default values for debug builds
+ *
+ * Flags can be set (or unset) via the following adb command:
+ *
+ *   adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id <id> [--ez value <0|1>]
+ *
+ * To restore a flag back to its default, leave the `--ez value <0|1>` off of the command.
  */
 @SysUISingleton
 public class FeatureFlagManager implements FlagReader, FlagWriter {
+    private static final String TAG = "SysUIFlags";
+
+    private static final String SYSPROP_PREFIX = "persist.systemui.flag_";
+    private static final String FIELD_TYPE = "type";
+    private static final String FIELD_ID = "id";
+    private static final String FIELD_VALUE = "value";
+    private static final String TYPE_BOOLEAN = "boolean";
+    private static final String ACTION_SET_FLAG = "com.android.systemui.action.SET_FLAG";
+    private static final String FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS";
+    private final SystemPropertiesHelper mSystemPropertiesHelper;
+
     @Inject
-    public FeatureFlagManager() {}
+    public FeatureFlagManager(SystemPropertiesHelper systemPropertiesHelper, Context context) {
+        mSystemPropertiesHelper = systemPropertiesHelper;
 
-    public boolean isEnabled(int key, boolean defaultValue) {
-        return isEnabled(Integer.toString(key), defaultValue);
+        IntentFilter filter = new IntentFilter(ACTION_SET_FLAG);
+        context.registerReceiver(mReceiver, filter, FLAGS_PERMISSION, null);
     }
 
-    public boolean isEnabled(String key, boolean defaultValue) {
-        // TODO
-        return false;
+    /** Return a {@link BooleanFlag}'s value. */
+    public boolean isEnabled(int id, boolean defaultValue) {
+        String data = mSystemPropertiesHelper.get(keyToSysPropKey(id));
+        if (data.isEmpty()) {
+            return defaultValue;
+        }
+        JSONObject json;
+        try {
+            json = new JSONObject(data);
+            if (!assertType(json, TYPE_BOOLEAN)) {
+                return defaultValue;
+            }
+            return json.getBoolean(FIELD_VALUE);
+        } catch (JSONException e) {
+            eraseFlag(id);
+            return defaultValue;
+        }
     }
 
-    public void setEnabled(int key, boolean value) {
-        setEnabled(Integer.toString(key), value);
+    /** Set whether a given {@link BooleanFlag} is enabled or not. */
+    public void setEnabled(int id, boolean value) {
+        JSONObject json = new JSONObject();
+        try {
+            json.put(FIELD_TYPE, TYPE_BOOLEAN);
+            json.put(FIELD_VALUE, value);
+            mSystemPropertiesHelper.set(keyToSysPropKey(id), json.toString());
+            Log.i(TAG, "Set id " + id + " to  " + value);
+        } catch (JSONException e) {
+            // no-op
+        }
     }
 
-    public void setEnabled(String key, boolean value) {
-        // TODO
+    /** Erase a flag's overridden value if there is one. */
+    public void eraseFlag(int id) {
+        // We can't actually "erase" things from sysprops, but we can set them to empty!
+        mSystemPropertiesHelper.set(keyToSysPropKey(id), "");
+        Log.i(TAG, "Erase id " + id);
     }
 
     public void addListener(Listener run) {}
 
     public void removeListener(Listener run) {}
 
+    private static String keyToSysPropKey(int key) {
+        return SYSPROP_PREFIX + key;
+    }
+
+    private static boolean assertType(JSONObject json, String type) {
+        try {
+            return json.getString(FIELD_TYPE).equals(TYPE_BOOLEAN);
+        } catch (JSONException e) {
+            return false;
+        }
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action == null) {
+                return;
+            }
+
+            if (ACTION_SET_FLAG.equals(action)) {
+                handleSetFlag(intent.getExtras());
+            }
+        }
+
+        private void handleSetFlag(Bundle extras) {
+            int id = extras.getInt(FIELD_ID);
+            if (id <= 0) {
+                Log.w(TAG, "ID not set or less than  or equal to 0: " + id);
+                return;
+            }
+
+            Map<Integer, Flag<?>> flagMap = Flags.collectFlags();
+            if (!flagMap.containsKey(id)) {
+                Log.w(TAG, "Tried to set unknown id: " + id);
+                return;
+            }
+            Flag<?> flag = flagMap.get(id);
+
+            if (!extras.containsKey(FIELD_VALUE)) {
+                eraseFlag(id);
+                return;
+            }
+
+            if (flag instanceof BooleanFlag) {
+                setEnabled(id, extras.getBoolean(FIELD_VALUE));
+            }
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
index efcf40a..a383cab 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
@@ -20,12 +20,16 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.icu.text.NumberFormat;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.settingslib.Utils;
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -67,20 +71,20 @@
             BroadcastDispatcher broadcastDispatcher,
             BatteryController batteryController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
-            KeyguardBypassController bypassController) {
+            KeyguardBypassController bypassController,
+            @Main Resources resources
+    ) {
         super(view);
         mStatusBarStateController = statusBarStateController;
-        mIsDozing = mStatusBarStateController.isDozing();
-        mDozeAmount = mStatusBarStateController.getDozeAmount();
         mBroadcastDispatcher = broadcastDispatcher;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mBypassController = bypassController;
         mBatteryController = batteryController;
 
         mBurmeseNumerals = mBurmeseNf.format(FORMAT_NUMBER);
-        mBurmeseLineSpacing = getContext().getResources().getFloat(
+        mBurmeseLineSpacing = resources.getFloat(
                 R.dimen.keyguard_clock_line_spacing_scale_burmese);
-        mDefaultLineSpacing = getContext().getResources().getFloat(
+        mDefaultLineSpacing = resources.getFloat(
                 R.dimen.keyguard_clock_line_spacing_scale);
     }
 
@@ -106,7 +110,7 @@
         }
     };
 
-    private final StatusBarStateController.StateListener mStatusBarStatePersistentListener =
+    private final StatusBarStateController.StateListener mStatusBarStateListener =
             new StatusBarStateController.StateListener() {
                 @Override
                 public void onDozeAmountChanged(float linear, float eased) {
@@ -144,11 +148,11 @@
         mBroadcastDispatcher.registerReceiver(mLocaleBroadcastReceiver,
                 new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
         mDozeAmount = mStatusBarStateController.getDozeAmount();
+        mIsDozing = mStatusBarStateController.isDozing() || mDozeAmount != 0;
         mBatteryController.addCallback(mBatteryCallback);
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
 
-        mStatusBarStateController.removeCallback(mStatusBarStatePersistentListener);
-        mStatusBarStateController.addCallback(mStatusBarStatePersistentListener);
+        mStatusBarStateController.addCallback(mStatusBarStateListener);
 
         refreshTime();
         initColors();
@@ -160,7 +164,7 @@
         mBroadcastDispatcher.unregisterReceiver(mLocaleBroadcastReceiver);
         mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback);
         mBatteryController.removeCallback(mBatteryCallback);
-        mStatusBarStateController.removeCallback(mStatusBarStatePersistentListener);
+        mStatusBarStateController.removeCallback(mStatusBarStateListener);
     }
 
     /** Animate the clock appearance */
@@ -189,6 +193,14 @@
         mView.refreshFormat();
     }
 
+    /**
+     * Return locallly stored dozing state.
+     */
+    @VisibleForTesting
+    public boolean isDozing() {
+        return mIsDozing;
+    }
+
     private void updateLocale() {
         Locale currLocale = Locale.getDefault();
         if (!Objects.equals(currLocale, mLocale)) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index f81f0b9..6fd0c82 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -41,7 +41,7 @@
 
     private static final long CLOCK_OUT_MILLIS = 150;
     private static final long CLOCK_IN_MILLIS = 200;
-    private static final long SMARTSPACE_MOVE_MILLIS = 350;
+    private static final long STATUS_AREA_MOVE_MILLIS = 350;
 
     @IntDef({LARGE, SMALL})
     @Retention(RetentionPolicy.SOURCE)
@@ -63,13 +63,7 @@
     private AnimatableClockView mClockView;
     private AnimatableClockView mLargeClockView;
 
-    /**
-     * Status area (date and other stuff) shown below the clock. Plugin can decide whether or not to
-     * show it below the alternate clock.
-     */
-    private View mKeyguardStatusArea;
-    /** Mutually exclusive with mKeyguardStatusArea */
-    private View mSmartspaceView;
+    private View mStatusArea;
     private int mSmartspaceTopOffset;
 
     /**
@@ -85,7 +79,7 @@
 
     @VisibleForTesting AnimatorSet mClockInAnim = null;
     @VisibleForTesting AnimatorSet mClockOutAnim = null;
-    private ObjectAnimator mSmartspaceAnim = null;
+    private ObjectAnimator mStatusAreaAnim = null;
 
     /**
      * If the Keyguard Slice has a header (big center-aligned text.)
@@ -131,7 +125,7 @@
         mClockView = findViewById(R.id.animatable_clock_view);
         mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
         mLargeClockView = findViewById(R.id.animatable_clock_view_large);
-        mKeyguardStatusArea = findViewById(R.id.keyguard_status_area);
+        mStatusArea = findViewById(R.id.keyguard_status_area);
 
         onDensityOrFontScaleChanged();
     }
@@ -200,22 +194,22 @@
     private void animateClockChange(boolean useLargeClock) {
         if (mClockInAnim != null) mClockInAnim.cancel();
         if (mClockOutAnim != null) mClockOutAnim.cancel();
-        if (mSmartspaceAnim != null) mSmartspaceAnim.cancel();
+        if (mStatusAreaAnim != null) mStatusAreaAnim.cancel();
 
         View in, out;
         int direction = 1;
-        float smartspaceYTranslation;
+        float statusAreaYTranslation;
         if (useLargeClock) {
             out = mClockFrame;
             in = mLargeClockFrame;
             if (indexOfChild(in) == -1) addView(in);
             direction = -1;
-            smartspaceYTranslation = mSmartspaceView == null ? 0
-                    : mClockFrame.getTop() - mSmartspaceView.getTop() + mSmartspaceTopOffset;
+            statusAreaYTranslation = mClockFrame.getTop() - mStatusArea.getTop()
+                    + mSmartspaceTopOffset;
         } else {
             in = mClockFrame;
             out = mLargeClockFrame;
-            smartspaceYTranslation = 0f;
+            statusAreaYTranslation = 0f;
 
             // Must remove in order for notifications to appear in the proper place
             removeView(out);
@@ -251,18 +245,16 @@
         mClockInAnim.start();
         mClockOutAnim.start();
 
-        if (mSmartspaceView != null) {
-            mSmartspaceAnim = ObjectAnimator.ofFloat(mSmartspaceView, View.TRANSLATION_Y,
-                    smartspaceYTranslation);
-            mSmartspaceAnim.setDuration(SMARTSPACE_MOVE_MILLIS);
-            mSmartspaceAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
-            mSmartspaceAnim.addListener(new AnimatorListenerAdapter() {
-                public void onAnimationEnd(Animator animation) {
-                    mSmartspaceAnim = null;
-                }
-            });
-            mSmartspaceAnim.start();
-        }
+        mStatusAreaAnim = ObjectAnimator.ofFloat(mStatusArea, View.TRANSLATION_Y,
+                statusAreaYTranslation);
+        mStatusAreaAnim.setDuration(STATUS_AREA_MOVE_MILLIS);
+        mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mStatusAreaAnim.addListener(new AnimatorListenerAdapter() {
+            public void onAnimationEnd(Animator animation) {
+                mStatusAreaAnim = null;
+            }
+        });
+        mStatusAreaAnim.start();
     }
 
     /**
@@ -352,10 +344,6 @@
         }
     }
 
-    void setSmartspaceView(View smartspaceView) {
-        mSmartspaceView = smartspaceView;
-    }
-
     void updateColors(ColorExtractor.GradientColors colors) {
         mSupportsDarkText = colors.supportsDarkText();
         mColorPalette = colors.getColorPalette();
@@ -369,8 +357,7 @@
         pw.println("  mClockPlugin: " + mClockPlugin);
         pw.println("  mClockFrame: " + mClockFrame);
         pw.println("  mLargeClockFrame: " + mLargeClockFrame);
-        pw.println("  mKeyguardStatusArea: " + mKeyguardStatusArea);
-        pw.println("  mSmartspaceView: " + mSmartspaceView);
+        pw.println("  mStatusArea: " + mStatusArea);
         pw.println("  mDarkAmount: " + mDarkAmount);
         pw.println("  mSupportsDarkText: " + mSupportsDarkText);
         pw.println("  mColorPalette: " + Arrays.toString(mColorPalette));
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 260b393..01976b4 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -22,9 +22,12 @@
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 
 import android.app.WallpaperManager;
+import android.content.res.Resources;
 import android.text.TextUtils;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.FrameLayout;
+import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
 
 import com.android.internal.colorextraction.ColorExtractor;
@@ -32,6 +35,7 @@
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.plugins.ClockPlugin;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -67,6 +71,7 @@
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final BatteryController mBatteryController;
     private final LockscreenSmartspaceController mSmartspaceController;
+    private final Resources mResources;
 
     /**
      * Clock for both small and large sizes
@@ -96,7 +101,8 @@
 
     private final ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin;
 
-    // If set, will replace keyguard_status_area
+    private ViewGroup mStatusArea;
+    // If set will replace keyguard_slice_view
     private View mSmartspaceView;
 
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
@@ -118,7 +124,8 @@
             KeyguardBypassController bypassController,
             LockscreenSmartspaceController smartspaceController,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
-            SmartspaceTransitionController smartspaceTransitionController) {
+            SmartspaceTransitionController smartspaceTransitionController,
+            @Main Resources resources) {
         super(keyguardClockSwitch);
         mStatusBarStateController = statusBarStateController;
         mColorExtractor = colorExtractor;
@@ -130,6 +137,7 @@
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mBypassController = bypassController;
         mSmartspaceController = smartspaceController;
+        mResources = resources;
 
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
         mSmartspaceTransitionController = smartspaceTransitionController;
@@ -159,7 +167,8 @@
                         mBroadcastDispatcher,
                         mBatteryController,
                         mKeyguardUpdateMonitor,
-                        mBypassController);
+                        mBypassController,
+                        mResources);
         mClockViewController.init();
 
         mLargeClockViewController =
@@ -169,7 +178,8 @@
                         mBroadcastDispatcher,
                         mBatteryController,
                         mKeyguardUpdateMonitor,
-                        mBypassController);
+                        mBypassController,
+                        mResources);
         mLargeClockViewController.init();
     }
 
@@ -184,8 +194,8 @@
                 mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
 
         if (mOnlyClock) {
-            View ksa = mView.findViewById(R.id.keyguard_status_area);
-            ksa.setVisibility(View.GONE);
+            View ksv = mView.findViewById(R.id.keyguard_slice_view);
+            ksv.setVisibility(View.GONE);
 
             View nic = mView.findViewById(
                     R.id.left_aligned_notification_icon_container);
@@ -194,19 +204,18 @@
         }
         updateAodIcons();
 
+        mStatusArea = mView.findViewById(R.id.keyguard_status_area);
+
         if (mSmartspaceController.isEnabled()) {
             mSmartspaceView = mSmartspaceController.buildAndConnectView(mView);
+            View ksv = mView.findViewById(R.id.keyguard_slice_view);
+            int ksvIndex = mStatusArea.indexOfChild(ksv);
+            ksv.setVisibility(View.GONE);
 
-            View ksa = mView.findViewById(R.id.keyguard_status_area);
-            int ksaIndex = mView.indexOfChild(ksa);
-            ksa.setVisibility(View.GONE);
-
-            // Place smartspace view below normal clock...
-            RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
+            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                     MATCH_PARENT, WRAP_CONTENT);
-            lp.addRule(RelativeLayout.BELOW, R.id.lockscreen_clock_view);
 
-            mView.addView(mSmartspaceView, ksaIndex, lp);
+            mStatusArea.addView(mSmartspaceView, ksvIndex, lp);
             int startPadding = getContext().getResources()
                     .getDimensionPixelSize(R.dimen.below_clock_padding_start);
             int endPadding = getContext().getResources()
@@ -214,14 +223,6 @@
             mSmartspaceView.setPaddingRelative(startPadding, 0, endPadding, 0);
 
             updateClockLayout();
-
-            View nic = mView.findViewById(
-                    R.id.left_aligned_notification_icon_container);
-            lp = (RelativeLayout.LayoutParams) nic.getLayoutParams();
-            lp.addRule(RelativeLayout.BELOW, mSmartspaceView.getId());
-            nic.setLayoutParams(lp);
-
-            mView.setSmartspaceView(mSmartspaceView);
             mSmartspaceTransitionController.setLockscreenSmartspace(mSmartspaceView);
         }
     }
@@ -237,8 +238,6 @@
         }
         mColorExtractor.removeOnColorsChangedListener(mColorsListener);
         mView.setClockPlugin(null, mStatusBarStateController.getState());
-
-        mSmartspaceController.disconnect();
     }
 
     /**
@@ -321,8 +320,8 @@
         PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_Y,
                 scale, props, animate);
 
-        if (mSmartspaceView != null) {
-            PropertyAnimator.setProperty(mSmartspaceView, AnimatableProperty.TRANSLATION_X,
+        if (mStatusArea != null) {
+            PropertyAnimator.setProperty(mStatusArea, AnimatableProperty.TRANSLATION_X,
                     x, props, animate);
 
             // If we're unlocking with the SmartSpace shared element transition, let the controller
@@ -333,7 +332,6 @@
         }
 
         mKeyguardSliceViewController.updatePosition(x, props, animate);
-        mNotificationIconAreaController.updatePosition(x, props, animate);
     }
 
     /** Sets an alpha value on every child view except for the smartspace. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
index 62411db..11eeac2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardMessageArea.java
@@ -153,6 +153,10 @@
             colorState = mNextMessageColorState;
             mNextMessageColorState = ColorStateList.valueOf(DEFAULT_COLOR);
         }
+        if (mAltBouncerShowing) {
+            // alt bouncer has a black scrim, so always show the text in white
+            colorState = ColorStateList.valueOf(Color.WHITE);
+        }
         setTextColor(colorState);
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index a5b18ca..abd89b9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -21,6 +21,8 @@
 
 import static java.lang.Integer.max;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.app.Activity;
 import android.app.AlertDialog;
@@ -35,7 +37,6 @@
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.view.ViewPropertyAnimator;
 import android.view.WindowInsets;
 import android.view.WindowInsetsAnimation;
 import android.view.WindowManager;
@@ -43,6 +44,7 @@
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
 
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.SpringAnimation;
@@ -111,7 +113,7 @@
 
     private boolean mIsSecurityViewLeftAligned = true;
     private boolean mOneHandedMode = false;
-    private ViewPropertyAnimator mRunningOneHandedAnimator;
+    @Nullable private ValueAnimator mRunningOneHandedAnimator;
 
     private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback =
             new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
@@ -347,9 +349,9 @@
             Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN;
             Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
 
-            ValueAnimator anim = ValueAnimator.ofFloat(0.0f, 1.0f);
-            anim.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS);
-            anim.setInterpolator(Interpolators.LINEAR);
+            mRunningOneHandedAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+            mRunningOneHandedAnimator.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS);
+            mRunningOneHandedAnimator.setInterpolator(Interpolators.LINEAR);
 
             int initialTranslation = (int) mSecurityViewFlipper.getTranslationX();
             int totalTranslation = (int) getResources().getDimension(
@@ -361,7 +363,15 @@
                 mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null);
             }
 
-            anim.addUpdateListener(animation -> {
+            float initialAlpha = mSecurityViewFlipper.getAlpha();
+
+            mRunningOneHandedAnimator.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mRunningOneHandedAnimator = null;
+                }
+            });
+            mRunningOneHandedAnimator.addUpdateListener(animation -> {
                 float switchPoint = BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION;
                 boolean isFadingOut = animation.getAnimatedFraction() < switchPoint;
 
@@ -378,13 +388,16 @@
                 if (isFadingOut) {
                     // The bouncer fades out over the first X%.
                     float fadeOutFraction = MathUtils.constrainedMap(
-                            /* rangeMin= */0.0f,
-                            /* rangeMax= */1.0f,
+                            /* rangeMin= */1.0f,
+                            /* rangeMax= */0.0f,
                             /* valueMin= */0.0f,
                             /* valueMax= */switchPoint,
                             animation.getAnimatedFraction());
                     float opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction);
-                    mSecurityViewFlipper.setAlpha(1f - opacity);
+
+                    // When fading out, the alpha needs to start from the initial opacity of the
+                    // view flipper, otherwise we get a weird bit of jank as it ramps back to 100%.
+                    mSecurityViewFlipper.setAlpha(opacity * initialAlpha);
 
                     // Animate away from the source.
                     mSecurityViewFlipper.setTranslationX(initialTranslation + currentTranslation);
@@ -409,7 +422,7 @@
                 }
             });
 
-            anim.start();
+            mRunningOneHandedAnimator.start();
         } else {
             mSecurityViewFlipper.setTranslationX(targetTranslation);
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 9d649e7..d4d3d5b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -209,7 +209,7 @@
     private ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
                 @Override
-                public void onOverlayChanged() {
+                public void onThemeChanged() {
                     mSecurityViewFlipperController.reloadColors();
                 }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
index 428006e..9b76bab 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java
@@ -33,12 +33,10 @@
 import android.text.TextUtils;
 import android.text.TextUtils.TruncateAt;
 import android.util.AttributeSet;
-import android.util.TypedValue;
 import android.view.Gravity;
 import android.view.View;
 import android.view.animation.Animation;
 import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import androidx.slice.SliceItem;
@@ -85,8 +83,6 @@
     private boolean mHasHeader;
     private View.OnClickListener mOnClickListener;
 
-    private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL;
-
     public KeyguardSliceView(Context context, AttributeSet attrs) {
         super(context, attrs);
 
@@ -136,35 +132,6 @@
         }
     }
 
-    /**
-     * Updates the lockscreen mode which may change the layout of the keyguard slice view.
-     */
-    public void updateLockScreenMode(int mode) {
-        mLockScreenMode = mode;
-        if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
-            mTitle.setPaddingRelative(0, 0, 0, 0);
-            mTitle.setGravity(Gravity.START);
-            setGravity(Gravity.START);
-            RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) getLayoutParams();
-            lp.removeRule(RelativeLayout.CENTER_HORIZONTAL);
-            setLayoutParams(lp);
-        } else {
-            final int horizontalPaddingDpValue = (int) TypedValue.applyDimension(
-                    TypedValue.COMPLEX_UNIT_DIP,
-                    44,
-                    getResources().getDisplayMetrics()
-            );
-            mTitle.setPaddingRelative(horizontalPaddingDpValue, 0, horizontalPaddingDpValue, 0);
-            mTitle.setGravity(Gravity.CENTER_HORIZONTAL);
-            setGravity(Gravity.CENTER_HORIZONTAL);
-            RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) getLayoutParams();
-            lp.addRule(RelativeLayout.CENTER_HORIZONTAL);
-            setLayoutParams(lp);
-        }
-        mRow.setLockscreenMode(mode);
-        requestLayout();
-    }
-
     Map<View, PendingIntent> showSlice(RowContent header, List<SliceContent> subItems) {
         Trace.beginSection("KeyguardSliceView#showSlice");
         mHasHeader = header != null;
@@ -189,8 +156,7 @@
         final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it
         mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE);
         LinearLayout.LayoutParams layoutParams = (LayoutParams) mRow.getLayoutParams();
-        layoutParams.gravity = mLockScreenMode !=  KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL
-                ? Gravity.START :  Gravity.CENTER;
+        layoutParams.gravity = Gravity.START;
         mRow.setLayoutParams(layoutParams);
 
         for (int i = startIndex; i < subItemsCount; i++) {
@@ -224,8 +190,7 @@
                 final int iconSize = mHasHeader ? mIconSizeWithHeader : mIconSize;
                 iconDrawable = icon.getIcon().loadDrawable(mContext);
                 if (iconDrawable != null) {
-                    if ((iconDrawable instanceof InsetDrawable)
-                            && mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
+                    if (iconDrawable instanceof InsetDrawable) {
                         // System icons (DnD) use insets which are fine for centered slice content
                         // but will cause a slight indent for left/right-aligned slice views
                         iconDrawable = ((InsetDrawable) iconDrawable).getDrawable();
@@ -321,7 +286,6 @@
         pw.println("  mTextColor: " + Integer.toHexString(mTextColor));
         pw.println("  mDarkAmount: " + mDarkAmount);
         pw.println("  mHasHeader: " + mHasHeader);
-        pw.println("  mLockScreenMode: " + mLockScreenMode);
     }
 
     @Override
@@ -332,7 +296,6 @@
 
     public static class Row extends LinearLayout {
         private Set<KeyguardSliceTextView> mKeyguardSliceTextViewSet = new HashSet();
-        private int mLockScreenModeRow = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL;
 
         /**
          * This view is visible in AOD, which means that the device will sleep if we
@@ -407,11 +370,7 @@
             for (int i = 0; i < childCount; i++) {
                 View child = getChildAt(i);
                 if (child instanceof KeyguardSliceTextView) {
-                    if (mLockScreenModeRow == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
-                        ((KeyguardSliceTextView) child).setMaxWidth(Integer.MAX_VALUE);
-                    } else {
-                        ((KeyguardSliceTextView) child).setMaxWidth(width / 3);
-                    }
+                    ((KeyguardSliceTextView) child).setMaxWidth(Integer.MAX_VALUE);
                 }
             }
 
@@ -443,7 +402,6 @@
             super.addView(view, index);
 
             if (view instanceof KeyguardSliceTextView) {
-                ((KeyguardSliceTextView) view).setLockScreenMode(mLockScreenModeRow);
                 mKeyguardSliceTextViewSet.add((KeyguardSliceTextView) view);
             }
         }
@@ -455,24 +413,6 @@
                 mKeyguardSliceTextViewSet.remove((KeyguardSliceTextView) view);
             }
         }
-
-        /**
-         * Updates the lockscreen mode which may change the layout of this view.
-         */
-        public void setLockscreenMode(int mode) {
-            mLockScreenModeRow = mode;
-            if (mLockScreenModeRow == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
-                setOrientation(LinearLayout.VERTICAL);
-                setGravity(Gravity.START);
-            } else {
-                setOrientation(LinearLayout.HORIZONTAL);
-                setGravity(Gravity.CENTER);
-            }
-
-            for (KeyguardSliceTextView textView : mKeyguardSliceTextViewSet) {
-                textView.setLockScreenMode(mLockScreenModeRow);
-            }
-        }
     }
 
     /**
@@ -480,7 +420,6 @@
      */
     @VisibleForTesting
     static class KeyguardSliceTextView extends TextView {
-        private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL;
 
         @StyleRes
         private static int sStyleId = R.style.TextAppearance_Keyguard_Secondary;
@@ -509,13 +448,8 @@
             boolean hasText = !TextUtils.isEmpty(getText());
             int padding = (int) getContext().getResources()
                     .getDimension(R.dimen.widget_horizontal_padding) / 2;
-            if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
-                // orientation is vertical, so add padding to top & bottom
-                setPadding(0, padding, 0, hasText ? padding : 0);
-            } else {
-                // orientation is horizontal, so add padding to left & right
-                setPadding(padding, 0, padding * (hasText ? 1 : -1), 0);
-            }
+            // orientation is vertical, so add padding to top & bottom
+            setPadding(0, padding, 0, hasText ? padding : 0);
 
             setCompoundDrawablePadding((int) mContext.getResources()
                     .getDimension(R.dimen.widget_icon_padding));
@@ -543,18 +477,5 @@
                 }
             }
         }
-
-        /**
-         * Updates the lockscreen mode which may change the layout of this view.
-         */
-        public void setLockScreenMode(int mode) {
-            mLockScreenMode = mode;
-            if (mLockScreenMode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
-                setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
-            } else {
-                setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
-            }
-            updatePadding();
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
index 8038ce4..d05cc4e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
@@ -73,7 +73,6 @@
     private Uri mKeyguardSliceUri;
     private Slice mSlice;
     private Map<View, PendingIntent> mClickActions;
-    private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL;
 
     TunerService.Tunable mTunable = (key, newValue) -> setupUri(newValue);
 
@@ -84,7 +83,7 @@
             mView.onDensityOrFontScaleChanged();
         }
         @Override
-        public void onOverlayChanged() {
+        public void onThemeChanged() {
             mView.onOverlayChanged();
         }
     };
@@ -137,7 +136,6 @@
                 TAG + "@" + Integer.toHexString(
                         KeyguardSliceViewController.this.hashCode()),
                 KeyguardSliceViewController.this);
-        mView.updateLockScreenMode(mLockScreenMode);
     }
 
     @Override
@@ -160,14 +158,6 @@
     }
 
     /**
-     * Updates the lockscreen mode which may change the layout of the keyguard slice view.
-     */
-    public void updateLockScreenMode(int mode) {
-        mLockScreenMode = mode;
-        mView.updateLockScreenMode(mLockScreenMode);
-    }
-
-    /**
      * Sets the slice provider Uri.
      */
     public void setupUri(String uriString) {
@@ -249,6 +239,5 @@
     public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("  mSlice: " + mSlice);
         pw.println("  mClickActions: " + mClickActions);
-        pw.println("  mLockScreenMode: " + mLockScreenMode);
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index 2362a1a..a72a050e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -88,7 +88,7 @@
             mClockView.setAccessibilityDelegate(new KeyguardClockAccessibilityDelegate(mContext));
         }
 
-        mKeyguardSlice = findViewById(R.id.keyguard_status_area);
+        mKeyguardSlice = findViewById(R.id.keyguard_slice_view);
         mTextColor = mClockView.getCurrentTextColor();
 
         mKeyguardSlice.setContentChangeListener(this::onSliceContentChanged);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 682b217..60af66ba 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -252,11 +252,6 @@
 
     private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
         @Override
-        public void onLockScreenModeChanged(int mode) {
-            mKeyguardSliceViewController.updateLockScreenMode(mode);
-        }
-
-        @Override
         public void onTimeChanged() {
             refreshTime();
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 5969e92..5c0fb5d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -71,7 +71,6 @@
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.os.Vibrator;
 import android.provider.Settings;
 import android.service.dreams.DreamService;
 import android.service.dreams.IDreamManager;
@@ -86,7 +85,6 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 
-import androidx.annotation.Nullable;
 import androidx.lifecycle.Observer;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -98,7 +96,6 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.biometrics.UdfpsController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -191,9 +188,6 @@
     private static final int MSG_TIME_FORMAT_UPDATE = 344;
     private static final int MSG_REQUIRE_NFC_UNLOCK = 345;
 
-    public static final int LOCK_SCREEN_MODE_NORMAL = 0;
-    public static final int LOCK_SCREEN_MODE_LAYOUT_1 = 1;
-
     /** Biometric authentication state: Not listening. */
     private static final int BIOMETRIC_STATE_STOPPED = 0;
 
@@ -286,9 +280,6 @@
     @VisibleForTesting
     protected boolean mTelephonyCapable;
 
-    private final boolean mAcquiredHapticEnabled = false;
-    @Nullable private final Vibrator mVibrator;
-
     // Device provisioning state
     private boolean mDeviceProvisioned;
 
@@ -1407,7 +1398,6 @@
     @VisibleForTesting
     final FingerprintManager.AuthenticationCallback mFingerprintAuthenticationCallback
             = new AuthenticationCallback() {
-                private boolean mPlayedAcquiredHaptic;
 
                 @Override
                 public void onAuthenticationFailed() {
@@ -1419,11 +1409,6 @@
                     Trace.beginSection("KeyguardUpdateMonitor#onAuthenticationSucceeded");
                     handleFingerprintAuthenticated(result.getUserId(), result.isStrongBiometric());
                     Trace.endSection();
-
-                    // on auth success, we sometimes never received an acquired haptic
-                    if (!mPlayedAcquiredHaptic && isUdfpsEnrolled()) {
-                        playAcquiredHaptic();
-                    }
                 }
 
                 @Override
@@ -1439,17 +1424,11 @@
                 @Override
                 public void onAuthenticationAcquired(int acquireInfo) {
                     handleFingerprintAcquired(acquireInfo);
-                    if (acquireInfo == FingerprintManager.FINGERPRINT_ACQUIRED_GOOD
-                            && isUdfpsEnrolled()) {
-                        mPlayedAcquiredHaptic = true;
-                        playAcquiredHaptic();
-                    }
                 }
 
                 @Override
                 public void onUdfpsPointerDown(int sensorId) {
                     Log.d(TAG, "onUdfpsPointerDown, sensorId: " + sensorId);
-                    mPlayedAcquiredHaptic = false;
                 }
 
                 @Override
@@ -1458,17 +1437,6 @@
                 }
             };
 
-    /**
-     * Play haptic to signal udfps fingeprrint acquired.
-     */
-    @VisibleForTesting
-    public void playAcquiredHaptic() {
-        if (mAcquiredHapticEnabled && mVibrator != null) {
-            mVibrator.vibrate(UdfpsController.EFFECT_CLICK,
-                    UdfpsController.VIBRATION_SONIFICATION_ATTRIBUTES);
-        }
-    }
-
     private final FaceManager.FaceDetectionCallback mFaceDetectionCallback
             = (sensorId, userId, isStrongBiometric) -> {
                 // Trigger the face success path so the bouncer can be shown
@@ -1772,8 +1740,8 @@
             AuthController authController,
             TelephonyListenerManager telephonyListenerManager,
             FeatureFlags featureFlags,
-            InteractionJankMonitor interactionJankMonitor,
-            @Nullable Vibrator vibrator) {
+            InteractionJankMonitor interactionJankMonitor
+    ) {
         mContext = context;
         mSubscriptionManager = SubscriptionManager.from(context);
         mTelephonyListenerManager = telephonyListenerManager;
@@ -1789,7 +1757,6 @@
         mLockPatternUtils = lockPatternUtils;
         mAuthController = authController;
         dumpManager.registerDumpable(getClass().getName(), this);
-        mVibrator = vibrator;
 
         mHandler = new Handler(mainLooper) {
             @Override
@@ -1898,9 +1865,6 @@
                     case MSG_KEYGUARD_GOING_AWAY:
                         handleKeyguardGoingAway((boolean) msg.obj);
                         break;
-                    case MSG_LOCK_SCREEN_MODE:
-                        handleLockScreenMode();
-                        break;
                     case MSG_TIME_FORMAT_UPDATE:
                         handleTimeFormatUpdate((String) msg.obj);
                         break;
@@ -2042,8 +2006,6 @@
             }
         }
 
-        updateLockScreenMode(featureFlags.isKeyguardLayoutEnabled());
-
         mTimeFormatChangeObserver = new ContentObserver(mHandler) {
             @Override
             public void onChange(boolean selfChange) {
@@ -2059,14 +2021,6 @@
                 false, mTimeFormatChangeObserver, UserHandle.USER_ALL);
     }
 
-    private void updateLockScreenMode(boolean isEnabled) {
-        final int newMode = isEnabled ? LOCK_SCREEN_MODE_LAYOUT_1 : LOCK_SCREEN_MODE_NORMAL;
-        if (newMode != mLockScreenMode) {
-            mLockScreenMode = newMode;
-            mHandler.sendEmptyMessage(MSG_LOCK_SCREEN_MODE);
-        }
-    }
-
     private void updateUdfpsEnrolled(int userId) {
         mIsUdfpsEnrolled = mAuthController.isUdfpsEnrolled(userId);
     }
@@ -2722,20 +2676,6 @@
     }
 
     /**
-     * Handle {@link #MSG_LOCK_SCREEN_MODE}
-     */
-    private void handleLockScreenMode() {
-        Assert.isMainThread();
-        if (DEBUG) Log.d(TAG, "handleLockScreenMode(" + mLockScreenMode + ")");
-        for (int i = 0; i < mCallbacks.size(); i++) {
-            KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
-            if (cb != null) {
-                cb.onLockScreenModeChanged(mLockScreenMode);
-            }
-        }
-    }
-
-    /**
      * Handle (@line #MSG_TIMEZONE_UPDATE}
      */
     private void handleTimeZoneUpdate(String timeZone) {
@@ -3113,7 +3053,6 @@
         callback.onKeyguardOccludedChanged(mKeyguardOccluded);
         callback.onKeyguardVisibilityChangedRaw(mKeyguardIsVisible);
         callback.onTelephonyCapable(mTelephonyCapable);
-        callback.onLockScreenModeChanged(mLockScreenMode);
 
         for (Entry<Integer, SimData> data : mSimDatas.entrySet()) {
             final SimData state = data.getValue();
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index 6aa7aaa..1e951f9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -330,11 +330,6 @@
     public void onSecondaryLockscreenRequirementChanged(int userId) { }
 
     /**
-     * Called to switch lock screen layout/clock layouts
-     */
-    public void onLockScreenModeChanged(int mode) { }
-
-    /**
      * Called when notifying user to unlock in order to use NFC.
      */
     public void onRequireUnlockForNfc() { }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
index ecc8c00..db729da 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java
@@ -18,7 +18,6 @@
 
 import android.os.Bundle;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.ViewRootImpl;
 
 import com.android.systemui.keyguard.KeyguardViewMediator;
@@ -186,14 +185,12 @@
     /**
      * Registers the StatusBar to which this Keyguard View is mounted.
      * @param statusBar
-     * @param container
      * @param notificationPanelViewController
      * @param biometricUnlockController
      * @param notificationContainer
      * @param bypassController
      */
     void registerStatusBar(StatusBar statusBar,
-            ViewGroup container,
             NotificationPanelViewController notificationPanelViewController,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer,
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 8cfd225..321c1a3 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -22,8 +22,8 @@
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInProgressOffset;
 
-import android.content.Context;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.AnimatedVectorDrawable;
@@ -38,6 +38,7 @@
 import android.util.MathUtils;
 import android.view.GestureDetector;
 import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.accessibility.AccessibilityManager;
@@ -98,9 +99,10 @@
     @NonNull private final AccessibilityManager mAccessibilityManager;
     @NonNull private final ConfigurationController mConfigurationController;
     @NonNull private final DelayableExecutor mExecutor;
+    @NonNull private final LayoutInflater mLayoutInflater;
     private boolean mUdfpsEnrolled;
 
-    @NonNull private LottieAnimationView mAodFp;
+    @Nullable private LottieAnimationView mAodFp;
 
     @NonNull private final AnimatedVectorDrawable mFpToUnlockIcon;
     @NonNull private final AnimatedVectorDrawable mLockToUnlockIcon;
@@ -154,7 +156,9 @@
             @NonNull ConfigurationController configurationController,
             @NonNull @Main DelayableExecutor executor,
             @Nullable Vibrator vibrator,
-            @Nullable AuthRippleController authRippleController
+            @Nullable AuthRippleController authRippleController,
+            @NonNull @Main Resources resources,
+            @NonNull LayoutInflater inflater
     ) {
         super(view);
         mStatusBarStateController = statusBarStateController;
@@ -168,27 +172,19 @@
         mExecutor = executor;
         mVibrator = vibrator;
         mAuthRippleController = authRippleController;
+        mLayoutInflater = inflater;
 
-        final Context context = view.getContext();
-        mAodFp = mView.findViewById(R.id.lock_udfps_aod_fp);
-        mMaxBurnInOffsetX = context.getResources()
-                .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
-        mMaxBurnInOffsetY = context.getResources()
-                .getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
+        mMaxBurnInOffsetX = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
+        mMaxBurnInOffsetY = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
 
-        mUnlockIcon = mView.getContext().getResources().getDrawable(
-            R.drawable.ic_unlock,
-            mView.getContext().getTheme());
-        mLockIcon = mView.getContext().getResources().getDrawable(
-                R.anim.lock_to_unlock,
-                mView.getContext().getTheme());
-        mFpToUnlockIcon = (AnimatedVectorDrawable) mView.getContext().getResources().getDrawable(
+        mUnlockIcon = resources.getDrawable(R.drawable.ic_unlock, mView.getContext().getTheme());
+        mLockIcon = resources.getDrawable(R.anim.lock_to_unlock, mView.getContext().getTheme());
+        mFpToUnlockIcon = (AnimatedVectorDrawable) resources.getDrawable(
                 R.anim.fp_to_unlock, mView.getContext().getTheme());
-        mLockToUnlockIcon = (AnimatedVectorDrawable) mView.getContext().getResources().getDrawable(
-                R.anim.lock_to_unlock,
+        mLockToUnlockIcon = (AnimatedVectorDrawable) resources.getDrawable(R.anim.lock_to_unlock,
                 mView.getContext().getTheme());
-        mUnlockedLabel = context.getResources().getString(R.string.accessibility_unlock_button);
-        mLockedLabel = context.getResources().getString(R.string.accessibility_lock_icon);
+        mUnlockedLabel = resources.getString(R.string.accessibility_unlock_button);
+        mLockedLabel = resources.getString(R.string.accessibility_lock_icon);
         dumpManager.registerDumpable("LockIconViewController", this);
     }
 
@@ -264,8 +260,8 @@
         boolean wasShowingLockIcon = mShowLockIcon;
         boolean wasShowingUnlockIcon = mShowUnlockIcon;
         mShowLockIcon = !mCanDismissLockScreen && !mUserUnlockedWithBiometric && isLockScreen()
-            && (!mUdfpsEnrolled || !mRunningFPS);
-        mShowUnlockIcon = mCanDismissLockScreen && isLockScreen();
+                && (!mUdfpsEnrolled || !mRunningFPS);
+        mShowUnlockIcon = (mCanDismissLockScreen || mUserUnlockedWithBiometric) && isLockScreen();
         mShowAODFpIcon = mIsDozing && mUdfpsEnrolled && !mRunningFPS;
 
         final CharSequence prevContentDescription = mView.getContentDescription();
@@ -300,7 +296,7 @@
             mView.setContentDescription(null);
         }
 
-        if (!mShowAODFpIcon) {
+        if (!mShowAODFpIcon && mAodFp != null) {
             mAodFp.setVisibility(View.INVISIBLE);
             mAodFp.setContentDescription(null);
         }
@@ -416,10 +412,12 @@
                         - mMaxBurnInOffsetY, mInterpolatedDarkAmount);
         float progress = MathUtils.lerp(0f, getBurnInProgressOffset(), mInterpolatedDarkAmount);
 
-        mAodFp.setTranslationX(offsetX);
-        mAodFp.setTranslationY(offsetY);
-        mAodFp.setProgress(progress);
-        mAodFp.setAlpha(255 * mInterpolatedDarkAmount);
+        if (mAodFp != null) {
+            mAodFp.setTranslationX(offsetX);
+            mAodFp.setTranslationY(offsetY);
+            mAodFp.setProgress(progress);
+            mAodFp.setAlpha(255 * mInterpolatedDarkAmount);
+        }
     }
 
     private void updateIsUdfpsEnrolled() {
@@ -430,6 +428,10 @@
         mView.setUseBackground(mUdfpsSupported);
 
         mUdfpsEnrolled = mKeyguardUpdateMonitor.isUdfpsEnrolled();
+        if (!wasUdfpsEnrolled && mUdfpsEnrolled && mAodFp == null) {
+            mLayoutInflater.inflate(R.layout.udfps_aod_lock_icon, mView);
+            mAodFp = mView.findViewById(R.id.lock_udfps_aod_fp);
+        }
         if (wasUdfpsSupported != mUdfpsSupported || wasUdfpsEnrolled != mUdfpsEnrolled) {
             updateVisibility();
         }
@@ -477,13 +479,15 @@
                 @Override
                 public void onBiometricRunningStateChanged(boolean running,
                         BiometricSourceType biometricSourceType) {
+                    final boolean wasRunningFps = mRunningFPS;
+                    final boolean wasUserUnlockedWithBiometric = mUserUnlockedWithBiometric;
                     mUserUnlockedWithBiometric =
                             mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(
                                     KeyguardUpdateMonitor.getCurrentUser());
 
                     if (biometricSourceType == FINGERPRINT) {
                         mRunningFPS = running;
-                        if (!mRunningFPS) {
+                        if (wasRunningFps && !mRunningFPS) {
                             if (mCancelDelayedUpdateVisibilityRunnable != null) {
                                 mCancelDelayedUpdateVisibilityRunnable.run();
                             }
@@ -493,10 +497,14 @@
                             // button in this case, so we delay updating the visibility by 50ms.
                             mCancelDelayedUpdateVisibilityRunnable =
                                     mExecutor.executeDelayed(() -> updateVisibility(), 50);
-                        } else {
-                            updateVisibility();
+                            return;
                         }
                     }
+
+                    if (wasUserUnlockedWithBiometric != mUserUnlockedWithBiometric
+                            || wasRunningFps != mRunningFPS) {
+                        updateVisibility();
+                    }
                 }
             };
 
@@ -545,11 +553,6 @@
         }
 
         @Override
-        public void onOverlayChanged() {
-            updateColors();
-        }
-
-        @Override
         public void onConfigChanged(Configuration newConfig) {
             updateConfiguration();
             updateColors();
@@ -650,7 +653,7 @@
     public boolean onTouchEvent(MotionEvent event, Runnable onGestureDetectedRunnable) {
         if (mSensorTouchLocation.contains((int) event.getX(), (int) event.getY())
                 && (mView.getVisibility() == View.VISIBLE
-                || mAodFp.getVisibility() == View.VISIBLE)) {
+                || (mAodFp != null && mAodFp.getVisibility() == View.VISIBLE))) {
             mOnGestureDetectedRunnable = onGestureDetectedRunnable;
             mGestureDetector.onTouchEvent(event);
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewModule.java
index 1d51e59..b8841ed 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusViewModule.java
@@ -34,6 +34,6 @@
 
     @Provides
     static KeyguardSliceView getKeyguardSliceView(KeyguardClockSwitch keyguardClockSwitch) {
-        return keyguardClockSwitch.findViewById(R.id.keyguard_status_area);
+        return keyguardClockSwitch.findViewById(R.id.keyguard_slice_view);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/AutoReinflateContainer.java b/packages/SystemUI/src/com/android/systemui/AutoReinflateContainer.java
index 5ed9eaa..12dd8f0 100644
--- a/packages/SystemUI/src/com/android/systemui/AutoReinflateContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/AutoReinflateContainer.java
@@ -86,7 +86,7 @@
     }
 
     @Override
-    public void onOverlayChanged() {
+    public void onThemeChanged() {
         inflateLayout();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
index 60b0637..f11dc93 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java
@@ -824,11 +824,25 @@
         return new AuthDialog.LayoutParams(width, totalHeight);
     }
 
+    private boolean isLargeDisplay() {
+        return com.android.systemui.util.Utils.shouldUseSplitNotificationShade(getResources());
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         final int width = MeasureSpec.getSize(widthMeasureSpec);
         final int height = MeasureSpec.getSize(heightMeasureSpec);
-        final int newWidth = Math.min(width, height);
+
+        final boolean isLargeDisplay = isLargeDisplay();
+
+        final int newWidth;
+        if (isLargeDisplay) {
+            // TODO(b/201811580): Unless we can come up with a one-size-fits-all equation, we may
+            //  want to consider moving this to an overlay.
+            newWidth = 2 * Math.min(width, height) / 3;
+        } else {
+            newWidth = Math.min(width, height);
+        }
 
         // Use "newWidth" instead, so the landscape dialog width is the same as the portrait
         // width.
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
index fa50f89..f1e42e0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthPanelController.java
@@ -117,24 +117,6 @@
         mUseFullScreen = fullScreen;
     }
 
-    public ValueAnimator getTranslationAnimator(float relativeTranslationY) {
-        final ValueAnimator animator = ValueAnimator.ofFloat(
-                mPanelView.getY(), mPanelView.getY() - relativeTranslationY);
-        animator.addUpdateListener(animation -> {
-            final float translation = (float) animation.getAnimatedValue();
-            mPanelView.setTranslationY(translation);
-        });
-        return animator;
-    }
-
-    public ValueAnimator getAlphaAnimator(float alpha) {
-        final ValueAnimator animator = ValueAnimator.ofFloat(mPanelView.getAlpha(), alpha);
-        animator.addUpdateListener(animation -> {
-            mPanelView.setAlpha((float) animation.getAnimatedValue());
-        });
-        return animator;
-    }
-
     public void updateForContentDimensions(int contentWidth, int contentHeight,
             int animateDurationMs) {
         if (DEBUG) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index eb6b193..8b04bf5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -18,7 +18,6 @@
 
 import android.animation.ValueAnimator
 import android.content.Context
-import android.content.res.Configuration
 import android.graphics.PointF
 import android.hardware.biometrics.BiometricSourceType
 import android.util.DisplayMetrics
@@ -125,6 +124,7 @@
             return
         }
 
+        updateSensorLocation()
         if (biometricSourceType == BiometricSourceType.FINGERPRINT &&
             fingerprintSensorLocation != null) {
             mView.setSensorLocation(fingerprintSensorLocation!!)
@@ -266,18 +266,12 @@
 
     private val configurationChangedListener =
         object : ConfigurationController.ConfigurationListener {
-            override fun onConfigChanged(newConfig: Configuration?) {
-                updateSensorLocation()
-            }
             override fun onUiModeChanged() {
                 updateRippleColor()
             }
             override fun onThemeChanged() {
                 updateRippleColor()
             }
-            override fun onOverlayChanged() {
-                updateRippleColor()
-            }
     }
 
     private val udfpsControllerCallback =
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 5018e57c..069a1b2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -256,6 +256,13 @@
         @Override
         public void hideUdfpsOverlay(int sensorId) {
             mFgExecutor.execute(() -> {
+                if (mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) {
+                    // if we get here, we expect keyguardUpdateMonitor's fingerprintRunningState
+                    // to be updated shortly afterwards
+                    Log.d(TAG, "hiding udfps overlay when "
+                            + "mKeyguardUpdateMonitor.isFingerprintDetectionRunning()=true");
+                }
+
                 mServerRequest = null;
                 updateOverlay();
             });
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt
index ea2bbfa..e231310 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsHapticsSimulator.kt
@@ -58,9 +58,6 @@
                 "start" -> {
                     udfpsController?.playStartHaptic()
                 }
-                "acquired" -> {
-                    keyguardUpdateMonitor.playAcquiredHaptic()
-                }
                 "success" -> {
                     // needs to be kept up to date with AcquisitionClient#SUCCESS_VIBRATION_EFFECT
                     vibrator?.vibrate(
@@ -82,7 +79,6 @@
         pw.println("Usage: adb shell cmd statusbar udfps-haptic <haptic>")
         pw.println("Available commands:")
         pw.println("  start")
-        pw.println("  acquired")
         pw.println("  success, always plays CLICK haptic")
         pw.println("  error, always plays DOUBLE_CLICK haptic")
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
index db93b26..7a28c9d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
@@ -398,11 +398,6 @@
                 }
 
                 @Override
-                public void onOverlayChanged() {
-                    mView.updateColor();
-                }
-
-                @Override
                 public void onConfigChanged(Configuration newConfig) {
                     mView.updateColor();
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
index 14e5991..be326da 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingCollectorImpl.java
@@ -37,6 +37,7 @@
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.sensors.ProximitySensor;
 import com.android.systemui.util.sensors.ThresholdSensor;
+import com.android.systemui.util.sensors.ThresholdSensorEvent;
 import com.android.systemui.util.time.SystemClock;
 
 import java.util.Collections;
@@ -405,7 +406,7 @@
         mProximitySensor.unregister(mSensorEventListener);
     }
 
-    private void onProximityEvent(ThresholdSensor.ThresholdSensorEvent proximityEvent) {
+    private void onProximityEvent(ThresholdSensorEvent proximityEvent) {
         // TODO: some of these classifiers might allow us to abort early, meaning we don't have to
         // make these calls.
         mFalsingManager.onProximityEvent(new ProximityEventImpl(proximityEvent));
@@ -423,9 +424,9 @@
     }
 
     private static class ProximityEventImpl implements FalsingManager.ProximityEvent {
-        private ThresholdSensor.ThresholdSensorEvent mThresholdSensorEvent;
+        private ThresholdSensorEvent mThresholdSensorEvent;
 
-        ProximityEventImpl(ThresholdSensor.ThresholdSensorEvent thresholdSensorEvent) {
+        ProximityEventImpl(ThresholdSensorEvent thresholdSensorEvent) {
             mThresholdSensorEvent = thresholdSensorEvent;
         }
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalStateController.java b/packages/SystemUI/src/com/android/systemui/communal/CommunalStateController.java
index c72f542..7be8ecc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalStateController.java
@@ -17,6 +17,9 @@
 package com.android.systemui.communal;
 
 import android.annotation.NonNull;
+import android.app.communal.CommunalManager;
+import android.content.Context;
+import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.dagger.SysUISingleton;
@@ -33,7 +36,9 @@
 @SysUISingleton
 public class CommunalStateController implements
         CallbackController<CommunalStateController.Callback> {
+    private static final String TAG = CommunalStateController.class.getSimpleName();
     private final ArrayList<Callback> mCallbacks = new ArrayList<>();
+    private final CommunalManager mCommunalManager;
     private boolean mCommunalViewOccluded;
     private boolean mCommunalViewShowing;
 
@@ -56,7 +61,8 @@
 
     @VisibleForTesting
     @Inject
-    public CommunalStateController() {
+    public CommunalStateController(Context context) {
+        mCommunalManager = context.getSystemService(CommunalManager.class);
     }
 
     /**
@@ -70,6 +76,12 @@
 
         mCommunalViewShowing = communalViewShowing;
 
+        try {
+            mCommunalManager.setCommunalViewShowing(communalViewShowing);
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Error updating communal manager service state", e);
+        }
+
         final ArrayList<Callback> callbacks = new ArrayList<>(mCallbacks);
         for (Callback callback : callbacks) {
             callback.onCommunalViewShowingChanged();
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
index d3d6e03..6f30ac3 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
@@ -18,6 +18,7 @@
 
 import android.annotation.MainThread
 import android.app.Dialog
+import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
@@ -88,7 +89,7 @@
         bouncerOrRun(createAction(cvh.cws.ci.controlId, {
             cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
             if (cvh.usePanel()) {
-                showDetail(cvh, control.getAppIntent().getIntent())
+                showDetail(cvh, control.getAppIntent())
             } else {
                 cvh.action(CommandAction(templateId))
             }
@@ -116,7 +117,7 @@
             // Long press snould only be called when there is valid control state, otherwise ignore
             cvh.cws.control?.let {
                 cvh.layout.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
-                showDetail(cvh, it.getAppIntent().getIntent())
+                showDetail(cvh, it.getAppIntent())
             }
         }, false /* blockable */))
     }
@@ -167,10 +168,10 @@
         bgExecutor.execute { vibrator.vibrate(effect) }
     }
 
-    private fun showDetail(cvh: ControlViewHolder, intent: Intent) {
+    private fun showDetail(cvh: ControlViewHolder, pendingIntent: PendingIntent) {
         bgExecutor.execute {
             val activities: List<ResolveInfo> = context.packageManager.queryIntentActivities(
-                intent,
+                pendingIntent.getIntent(),
                 PackageManager.MATCH_DEFAULT_ONLY
             )
 
@@ -178,7 +179,7 @@
                 // make sure the intent is valid before attempting to open the dialog
                 if (activities.isNotEmpty() && taskViewFactory.isPresent) {
                     taskViewFactory.get().create(context, uiExecutor, {
-                        dialog = DetailDialog(activityContext, it, intent, cvh).also {
+                        dialog = DetailDialog(activityContext, it, pendingIntent, cvh).also {
                             it.setOnDismissListener { _ -> dialog = null }
                             it.show()
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
index 8a47a36..4758ab0 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
@@ -43,7 +43,7 @@
 class DetailDialog(
     val activityContext: Context?,
     val taskView: TaskView,
-    val intent: Intent,
+    val pendingIntent: PendingIntent,
     val cvh: ControlViewHolder
 ) : Dialog(
     activityContext ?: cvh.context,
@@ -59,6 +59,14 @@
 
     var detailTaskId = INVALID_TASK_ID
 
+    private val fillInIntent = Intent().apply {
+        putExtra(EXTRA_USE_PANEL, true)
+
+        // Apply flags to make behaviour match documentLaunchMode=always.
+        addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
+        addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
+    }
+
     fun removeDetailTask() {
         if (detailTaskId == INVALID_TASK_ID) return
         ActivityTaskManager.getInstance().removeTask(detailTaskId)
@@ -67,13 +75,6 @@
 
     val stateCallback = object : TaskView.Listener {
         override fun onInitialized() {
-            val launchIntent = Intent(intent)
-            launchIntent.putExtra(EXTRA_USE_PANEL, true)
-
-            // Apply flags to make behaviour match documentLaunchMode=always.
-            launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
-            launchIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
-
             val options = activityContext?.let {
                 ActivityOptions.makeCustomAnimation(
                     it,
@@ -82,9 +83,8 @@
                 )
             } ?: ActivityOptions.makeBasic()
             taskView.startActivity(
-                PendingIntent.getActivity(context, 0, launchIntent,
-                        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE),
-                null /* fillInIntent */,
+                pendingIntent,
+                fillInIntent,
                 options,
                 getTaskViewBounds()
             )
@@ -97,6 +97,9 @@
 
         override fun onTaskCreated(taskId: Int, name: ComponentName?) {
             detailTaskId = taskId
+            requireViewById<ViewGroup>(R.id.controls_activity_view).apply {
+                setAlpha(1f)
+            }
         }
 
         override fun onReleased() {
@@ -121,6 +124,7 @@
 
         requireViewById<ViewGroup>(R.id.controls_activity_view).apply {
             addView(taskView)
+            setAlpha(0f)
         }
 
         requireViewById<ImageView>(R.id.control_detail_close).apply {
@@ -134,7 +138,7 @@
                 removeDetailTask()
                 dismiss()
                 context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
-                v.context.startActivity(intent)
+                pendingIntent.send()
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index d74df37..7972318 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -374,6 +374,12 @@
 
     @Provides
     @Singleton
+    static Optional<TelecomManager> provideOptionalTelecomManager(Context context) {
+        return Optional.ofNullable(context.getSystemService(TelecomManager.class));
+    }
+
+    @Provides
+    @Singleton
     static TelephonyManager provideTelephonyManager(Context context) {
         return context.getSystemService(TelephonyManager.class);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 1e7f0d9..b17f078 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -46,6 +46,7 @@
 import com.android.systemui.util.Assert;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.sensors.AsyncSensorManager;
+import com.android.systemui.util.sensors.ProximityCheck;
 import com.android.systemui.util.sensors.ProximitySensor;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.wakelock.WakeLock;
@@ -90,7 +91,7 @@
     private final TriggerReceiver mBroadcastReceiver = new TriggerReceiver();
     private final DockEventListener mDockEventListener = new DockEventListener();
     private final DockManager mDockManager;
-    private final ProximitySensor.ProximityCheck mProxCheck;
+    private final ProximityCheck mProxCheck;
     private final BroadcastDispatcher mBroadcastDispatcher;
     private final AuthController mAuthController;
     private final DelayableExecutor mMainExecutor;
@@ -181,7 +182,8 @@
             AmbientDisplayConfiguration config,
             DozeParameters dozeParameters, AsyncSensorManager sensorManager,
             WakeLock wakeLock, DockManager dockManager,
-            ProximitySensor proximitySensor, ProximitySensor.ProximityCheck proxCheck,
+            ProximitySensor proximitySensor,
+            ProximityCheck proxCheck,
             DozeLog dozeLog, BroadcastDispatcher broadcastDispatcher,
             SecureSettings secureSettings, AuthController authController,
             @Main DelayableExecutor mainExecutor,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.java
index 432d6c2..2e30e7f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.java
@@ -94,10 +94,6 @@
         return mFlagReader.isEnabled(R.bool.flag_notification_pipeline2_rendering);
     }
 
-    public boolean isKeyguardLayoutEnabled() {
-        return mFlagReader.isEnabled(R.bool.flag_keyguard_layout);
-    }
-
     /** */
     public boolean useNewLockscreenAnimations() {
         return mFlagReader.isEnabled(R.bool.flag_lockscreen_animations);
@@ -123,6 +119,16 @@
         return mFlagReader.isEnabled(R.bool.flag_ongoing_call_status_bar_chip);
     }
 
+    public boolean isOngoingCallInImmersiveEnabled() {
+        return isOngoingCallStatusBarChipEnabled()
+                && mFlagReader.isEnabled(R.bool.flag_ongoing_call_in_immersive);
+    }
+
+    public boolean isOngoingCallInImmersiveChipTapEnabled() {
+        return isOngoingCallInImmersiveEnabled()
+                && mFlagReader.isEnabled(R.bool.flag_ongoing_call_in_immersive_chip_tap);
+    }
+
     public boolean isSmartspaceEnabled() {
         return mFlagReader.isEnabled(R.bool.flag_smartspace);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt b/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt
index 6561bd5..1dc5a9f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/SystemPropertiesHelper.kt
@@ -26,11 +26,19 @@
  */
 @SysUISingleton
 open class SystemPropertiesHelper @Inject constructor() {
+    fun get(name: String): String {
+        return SystemProperties.get(name)
+    }
+
     fun getBoolean(name: String, default: Boolean): Boolean {
         return SystemProperties.getBoolean(name, default)
     }
 
+    fun set(name: String, value: String) {
+        SystemProperties.set(name, value)
+    }
+
     fun set(name: String, value: Int) {
-        SystemProperties.set(name, value.toString())
+        set(name, value.toString())
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/idle/AmbientLightModeMonitor.kt b/packages/SystemUI/src/com/android/systemui/idle/AmbientLightModeMonitor.kt
new file mode 100644
index 0000000..8564497
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/idle/AmbientLightModeMonitor.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.idle
+
+import android.annotation.IntDef
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
+import android.util.Log
+import com.android.systemui.util.sensors.AsyncSensorManager
+import javax.inject.Inject
+import kotlin.properties.Delegates
+
+/**
+ * Monitors ambient light signals, applies a debouncing algorithm, and produces the current
+ * [AmbientLightMode].
+ *
+ * For debouncer behavior, refer to go/titan-light-sensor-debouncer.
+ *
+ * @property sensorManager the sensor manager used to register sensor event updates.
+ */
+class AmbientLightModeMonitor @Inject constructor(
+    private val sensorManager: AsyncSensorManager
+) {
+    companion object {
+        private const val TAG = "AmbientLightModeMonitor"
+        private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
+
+        const val AMBIENT_LIGHT_MODE_LIGHT = 0
+        const val AMBIENT_LIGHT_MODE_DARK = 1
+        const val AMBIENT_LIGHT_MODE_UNDECIDED = 2
+    }
+
+    // Light sensor used to detect ambient lighting conditions.
+    private val lightSensor: Sensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)
+
+    // Registered callback, which gets triggered when the ambient light mode changes.
+    private var callback: Callback? = null
+
+    // Represents all ambient light modes.
+    @Retention(AnnotationRetention.SOURCE)
+    @IntDef(AMBIENT_LIGHT_MODE_LIGHT, AMBIENT_LIGHT_MODE_DARK, AMBIENT_LIGHT_MODE_UNDECIDED)
+    annotation class AmbientLightMode
+
+    // The current ambient light mode.
+    @AmbientLightMode private var mode: Int by Delegates.observable(AMBIENT_LIGHT_MODE_UNDECIDED
+    ) { _, old, new ->
+        if (old != new) {
+            callback?.onChange(new)
+        }
+    }
+
+    /**
+     * Start monitoring the current ambient light mode.
+     *
+     * @param callback callback that gets triggered when the ambient light mode changes. It also
+     * gets triggered immediately to update the current value when this function is called.
+     */
+    fun start(callback: Callback) {
+        if (DEBUG) Log.d(TAG, "start monitoring ambient light mode")
+
+        if (this.callback != null) {
+            if (DEBUG) Log.w(TAG, "already started")
+            return
+        }
+
+        this.callback = callback
+        callback.onChange(mode)
+
+        sensorManager.registerListener(mSensorEventListener, lightSensor,
+                SensorManager.SENSOR_DELAY_NORMAL)
+    }
+
+    /**
+     * Stop monitoring the current ambient light mode.
+     */
+    fun stop() {
+        if (DEBUG) Log.d(TAG, "stop monitoring ambient light mode")
+
+        if (callback == null) {
+            if (DEBUG) Log.w(TAG, "haven't started")
+            return
+        }
+
+        callback = null
+        sensorManager.unregisterListener(mSensorEventListener)
+    }
+
+    private val mSensorEventListener: SensorEventListener = object : SensorEventListener {
+        override fun onSensorChanged(event: SensorEvent) {
+            if (event.values.isEmpty()) {
+                if (DEBUG) Log.w(TAG, "SensorEvent doesn't have any value")
+                return
+            }
+
+            // TODO(b/201657509): add debouncing logic.
+            val shouldBeLowLight = event.values[0] < 10
+            mode = if (shouldBeLowLight) AMBIENT_LIGHT_MODE_DARK else AMBIENT_LIGHT_MODE_LIGHT
+        }
+
+        override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
+            // Do nothing.
+        }
+    }
+
+    /**
+     * Interface of the ambient light mode callback, which gets triggered when the mode changes.
+     */
+    interface Callback {
+        fun onChange(@AmbientLightMode mode: Int)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/idle/IdleHostViewController.java b/packages/SystemUI/src/com/android/systemui/idle/IdleHostViewController.java
index 192c4184a..6b212b7 100644
--- a/packages/SystemUI/src/com/android/systemui/idle/IdleHostViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/idle/IdleHostViewController.java
@@ -24,10 +24,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Resources;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
 import android.os.Looper;
 import android.os.PowerManager;
 import android.os.SystemClock;
@@ -44,7 +40,6 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.sensors.AsyncSensorManager;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -56,8 +51,7 @@
 /**
  * {@link IdleHostViewController} processes signals to control the lifecycle of the idle screen.
  */
-public class IdleHostViewController extends ViewController<IdleHostView> implements
-        SensorEventListener {
+public class IdleHostViewController extends ViewController<IdleHostView> {
     private static final String INPUT_MONITOR_IDENTIFIER = "IdleHostViewController";
     private static final String TAG = "IdleHostViewController";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -114,11 +108,6 @@
 
     private final PowerManager mPowerManager;
 
-    private final AsyncSensorManager mSensorManager;
-
-    // Light sensor used to detect low light condition.
-    private final Sensor mSensor;
-
     // Runnable for canceling enabling idle.
     private Runnable mCancelEnableIdling;
 
@@ -146,6 +135,9 @@
     // Intent filter for receiving dream broadcasts.
     private IntentFilter mDreamIntentFilter;
 
+    // Monitor for the current ambient light mode. Used to trigger / exit low-light mode.
+    private final AmbientLightModeMonitor mAmbientLightModeMonitor;
+
     // Delayed callback for starting idling.
     private final Runnable mEnableIdlingCallback = () -> {
         if (DEBUG) {
@@ -181,6 +173,31 @@
         }
     };
 
+    private final AmbientLightModeMonitor.Callback mAmbientLightModeCallback =
+            mode -> {
+                boolean shouldBeLowLight;
+                switch (mode) {
+                    case AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED:
+                        return;
+                    case AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT:
+                        shouldBeLowLight = false;
+                        break;
+                    case AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK:
+                        shouldBeLowLight = true;
+                        break;
+                    default:
+                        Log.w(TAG, "invalid ambient light mode");
+                        return;
+                }
+
+                if (DEBUG) Log.d(TAG, "ambient light mode changed to " + mode);
+
+                final boolean isLowLight = getState(STATE_LOW_LIGHT);
+                if (shouldBeLowLight != isLowLight) {
+                    setState(STATE_LOW_LIGHT, shouldBeLowLight);
+                }
+            };
+
     final Provider<View> mIdleViewProvider;
 
     @Inject
@@ -188,7 +205,6 @@
             Context context,
             BroadcastDispatcher broadcastDispatcher,
             PowerManager powerManager,
-            AsyncSensorManager sensorManager,
             IdleHostView view, InputMonitorFactory factory,
             @Main DelayableExecutor delayableExecutor,
             @Main Resources resources,
@@ -197,18 +213,19 @@
             Choreographer choreographer,
             KeyguardStateController keyguardStateController,
             StatusBarStateController statusBarStateController,
-            DreamHelper dreamHelper) {
+            DreamHelper dreamHelper,
+            AmbientLightModeMonitor ambientLightModeMonitor) {
         super(view);
         mContext = context;
         mBroadcastDispatcher = broadcastDispatcher;
         mPowerManager = powerManager;
-        mSensorManager = sensorManager;
         mIdleViewProvider = idleViewProvider;
         mKeyguardStateController = keyguardStateController;
         mStatusBarStateController = statusBarStateController;
         mLooper = looper;
         mChoreographer = choreographer;
         mDreamHelper = dreamHelper;
+        mAmbientLightModeMonitor = ambientLightModeMonitor;
 
         mState = STATE_KEYGUARD_SHOWING;
 
@@ -222,7 +239,6 @@
         mIdleTimeout = resources.getInteger(R.integer.config_idleModeTimeout);
         mInputMonitorFactory = factory;
         mDelayableExecutor = delayableExecutor;
-        mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
 
         if (DEBUG) {
             Log.d(TAG, "initial state:" + mState + " enabled:" + enabled
@@ -405,11 +421,10 @@
 
         if (mIsMonitoringLowLight) {
             if (DEBUG) Log.d(TAG, "enable low light monitoring");
-            mSensorManager.registerListener(this /*listener*/, mSensor,
-                    SensorManager.SENSOR_DELAY_NORMAL);
+            mAmbientLightModeMonitor.start(mAmbientLightModeCallback);
         } else {
             if (DEBUG) Log.d(TAG, "disable low light monitoring");
-            mSensorManager.unregisterListener(this);
+            mAmbientLightModeMonitor.stop();
         }
     }
 
@@ -449,28 +464,6 @@
         mStatusBarStateController.removeCallback(mStatusBarCallback);
     }
 
-    @Override
-    public void onSensorChanged(SensorEvent event) {
-        if (event.values.length == 0) {
-            if (DEBUG) Log.w(TAG, "SensorEvent doesn't have value");
-            return;
-        }
-
-        final boolean shouldBeLowLight = event.values[0] < 10;
-        final boolean isLowLight = getState(STATE_LOW_LIGHT);
-
-        if (shouldBeLowLight != isLowLight) {
-            setState(STATE_LOW_LIGHT, shouldBeLowLight);
-        }
-    }
-
-    @Override
-    public void onAccuracyChanged(Sensor sensor, int accuracy) {
-        if (DEBUG) {
-            Log.d(TAG, "onAccuracyChanged accuracy=" + accuracy);
-        }
-    }
-
     // Returns whether the device just stopped idling by comparing the previous state with the
     // current one.
     private boolean stoppedIdling(int oldState) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 1561eb6..5265718 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -77,7 +77,6 @@
 import android.view.RemoteAnimationTarget;
 import android.view.SyncRtSurfaceTransactionApplier;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.view.WindowManagerPolicyConstants;
 import android.view.animation.Animation;
@@ -1475,7 +1474,9 @@
     public void doKeyguardTimeout(Bundle options) {
         mHandler.removeMessages(KEYGUARD_TIMEOUT);
         Message msg = mHandler.obtainMessage(KEYGUARD_TIMEOUT, options);
-        mHandler.sendMessage(msg);
+        // Treat these messages with priority - A call to timeout means the device should lock
+        // as soon as possible and not wait for other messages on the thread to process first.
+        mHandler.sendMessageAtFrontOfQueue(msg);
     }
 
     /**
@@ -1664,12 +1665,15 @@
      * @see #handleShow
      */
     private void showLocked(Bundle options) {
-        Trace.beginSection("KeyguardViewMediator#showLocked aqcuiring mShowKeyguardWakeLock");
+        Trace.beginSection("KeyguardViewMediator#showLocked acquiring mShowKeyguardWakeLock");
         if (DEBUG) Log.d(TAG, "showLocked");
         // ensure we stay awake until we are finished displaying the keyguard
         mShowKeyguardWakeLock.acquire();
         Message msg = mHandler.obtainMessage(SHOW, options);
-        mHandler.sendMessage(msg);
+        // Treat these messages with priority - This call can originate from #doKeyguardTimeout,
+        // meaning the device should lock as soon as possible and not wait for other messages on
+        // the thread to process first.
+        mHandler.sendMessageAtFrontOfQueue(msg);
         Trace.endSection();
     }
 
@@ -1855,6 +1859,7 @@
                 case KEYGUARD_TIMEOUT:
                     synchronized (KeyguardViewMediator.this) {
                         doKeyguardLocked((Bundle) msg.obj);
+                        notifyDefaultDisplayCallbacks(mShowing);
                     }
                     break;
                 case DISMISS:
@@ -2623,7 +2628,6 @@
      * Registers the StatusBar to which the Keyguard View is mounted.
      *
      * @param statusBar
-     * @param container
      * @param panelView
      * @param biometricUnlockController
      * @param notificationContainer
@@ -2631,10 +2635,10 @@
      * @return the View Controller for the Keyguard View this class is mediating.
      */
     public KeyguardViewController registerStatusBar(StatusBar statusBar,
-            ViewGroup container, NotificationPanelViewController panelView,
+            NotificationPanelViewController panelView,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer, KeyguardBypassController bypassController) {
-        mKeyguardViewControllerLazy.get().registerStatusBar(statusBar, container, panelView,
+        mKeyguardViewControllerLazy.get().registerStatusBar(statusBar, panelView,
                 biometricUnlockController, notificationContainer, bypassController);
         return mKeyguardViewControllerLazy.get();
     }
@@ -2840,7 +2844,7 @@
             for (int i = size - 1; i >= 0; i--) {
                 IKeyguardStateCallback callback = mKeyguardStateCallbacks.get(i);
                 try {
-                    callback.onShowingStateChanged(showing);
+                    callback.onShowingStateChanged(showing, KeyguardUpdateMonitor.getCurrentUser());
                 } catch (RemoteException e) {
                     Slog.w(TAG, "Failed to call onShowingStateChanged", e);
                     if (e instanceof DeadObjectException) {
@@ -2889,7 +2893,7 @@
             mKeyguardStateCallbacks.add(callback);
             try {
                 callback.onSimSecureStateChanged(mUpdateMonitor.isSimPinSecure());
-                callback.onShowingStateChanged(mShowing);
+                callback.onShowingStateChanged(mShowing, KeyguardUpdateMonitor.getCurrentUser());
                 callback.onInputRestrictedStateChanged(mInputRestricted);
                 callback.onTrustedChanged(mUpdateMonitor.getUserHasTrust(
                         KeyguardUpdateMonitor.getCurrentUser()));
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 84c5a57..72601e9 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -111,6 +111,17 @@
         return factory.create("CollapsedSbFragmentLog", 20);
     }
 
+    /**
+     * Provides a logging buffer for logs related to swiping away the status bar while in immersive
+     * mode. See {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}.
+     */
+    @Provides
+    @SysUISingleton
+    @SwipeStatusBarAwayLog
+    public static LogBuffer provideSwipeAwayGestureLogBuffer(LogBufferFactory factory) {
+        return factory.create("SwipeStatusBarAwayLog", 30);
+    }
+
     /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */
     @Provides
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
new file mode 100644
index 0000000..dd68375
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SwipeStatusBarAwayLog.java
@@ -0,0 +1,36 @@
+/*
+ * 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.systemui.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/**
+ * A {@link LogBuffer} for
+ * {@link com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureLogger}.
+ */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface SwipeStatusBarAwayLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index c743fe1..e87558e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -148,7 +148,7 @@
             inflateSettingsButton()
         }
 
-        override fun onOverlayChanged() {
+        override fun onThemeChanged() {
             recreatePlayers()
             inflateSettingsButton()
         }
@@ -833,11 +833,12 @@
     )
 
     private val comparator =
-            compareByDescending<MediaSortKey> { it.data.isPlaying }
+            compareByDescending<MediaSortKey> { it.data.isPlaying == true && it.data.isLocalSession }
+                    .thenByDescending { it.data.isPlaying }
                     .thenByDescending { if (shouldPrioritizeSs) it.isSsMediaRec else !it.isSsMediaRec }
-                    .thenByDescending { it.data.isLocalSession }
                     .thenByDescending { !it.data.resumption }
                     .thenByDescending { it.updateTime }
+                    .thenByDescending { !it.data.isLocalSession }
 
     private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
     private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 424f801..e73eb66 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -55,6 +55,7 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
 import com.android.systemui.util.animation.TransitionLayout;
@@ -119,6 +120,7 @@
     private int mSmartspaceMediaItemsCount;
     private MediaCarouselController mMediaCarouselController;
     private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final FalsingManager mFalsingManager;
 
     /**
      * Initialize a new control panel
@@ -131,7 +133,8 @@
             ActivityStarter activityStarter, MediaViewController mediaViewController,
             SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager,
             KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory
-            mediaOutputDialogFactory, MediaCarouselController mediaCarouselController) {
+            mediaOutputDialogFactory, MediaCarouselController mediaCarouselController,
+            FalsingManager falsingManager) {
         mContext = context;
         mBackgroundExecutor = backgroundExecutor;
         mActivityStarter = activityStarter;
@@ -141,6 +144,7 @@
         mKeyguardDismissUtil = keyguardDismissUtil;
         mMediaOutputDialogFactory = mediaOutputDialogFactory;
         mMediaCarouselController = mediaCarouselController;
+        mFalsingManager = falsingManager;
         loadDimens();
 
         mSeekBarViewModel.setLogSmartspaceClick(() -> {
@@ -235,10 +239,14 @@
             }
         });
         mPlayerViewHolder.getCancel().setOnClickListener(v -> {
-            closeGuts();
+            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                closeGuts();
+            }
         });
         mPlayerViewHolder.getSettings().setOnClickListener(v -> {
-            mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
+            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
+            }
         });
     }
 
@@ -259,10 +267,14 @@
             }
         });
         mRecommendationViewHolder.getCancel().setOnClickListener(v -> {
-            closeGuts();
+            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                closeGuts();
+            }
         });
         mRecommendationViewHolder.getSettings().setOnClickListener(v -> {
-            mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
+            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
+            }
         });
     }
 
@@ -299,6 +311,7 @@
         PendingIntent clickIntent = data.getClickIntent();
         if (clickIntent != null) {
             mPlayerViewHolder.getPlayer().setOnClickListener(v -> {
+                if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
                 if (mMediaViewController.isGutsVisible()) return;
 
                 logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
@@ -365,8 +378,12 @@
         setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
         setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
         seamlessView.setOnClickListener(
-                v -> mMediaOutputDialogFactory.create(data.getPackageName(), true,
-                        mPlayerViewHolder.getSeamlessButton()));
+                v -> {
+                    if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                        mMediaOutputDialogFactory.create(data.getPackageName(), true,
+                                mPlayerViewHolder.getSeamlessButton());
+                    }
+                });
 
         ImageView iconView = mPlayerViewHolder.getSeamlessIcon();
         TextView deviceName = mPlayerViewHolder.getSeamlessText();
@@ -417,9 +434,11 @@
             } else {
                 button.setEnabled(true);
                 button.setOnClickListener(v -> {
-                    logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
-                            /* isRecommendationCard */ false);
-                    action.run();
+                    if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                        logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
+                                /* isRecommendationCard */ false);
+                        action.run();
+                    }
                 });
             }
             boolean visibleInCompat = actionsWhenCollapsed.contains(i);
@@ -451,6 +470,8 @@
         mPlayerViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA);
         mPlayerViewHolder.getDismiss().setEnabled(isDismissible);
         mPlayerViewHolder.getDismiss().setOnClickListener(v -> {
+            if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
+
             logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
                     /* isRecommendationCard */ false);
 
@@ -633,6 +654,8 @@
         mSmartspaceMediaItemsCount = uiComponentIndex;
         // Set up long press to show guts setting panel.
         mRecommendationViewHolder.getDismiss().setOnClickListener(v -> {
+            if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
+
             logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
                     /* isRecommendationCard */ true);
             closeGuts();
@@ -788,6 +811,8 @@
         }
 
         view.setOnClickListener(v -> {
+            if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
+
             logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
                     /* isRecommendationCard */ true,
                     interactedSubcardRank,
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 06a1eea..3631d2f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -768,7 +768,7 @@
             val resumeAction = getResumeMediaAction(removed.resumeAction!!)
             val updated = removed.copy(token = null, actions = listOf(resumeAction),
                     actionsToShowInCompact = listOf(0), active = false, resumption = true,
-                    isClearable = true)
+                    isPlaying = false, isClearable = true)
             val pkg = removed.packageName
             val migrate = mediaEntries.put(pkg, updated) == null
             // Notify listeners of "new" controls when migrating or removed and update when not
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
index f17ad6f..33ef19a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt
@@ -50,6 +50,7 @@
             holder.seekBar.setProgress(0)
             holder.elapsedTimeView.setText("")
             holder.totalTimeView.setText("")
+            holder.seekBar.contentDescription = ""
             return
         }
 
@@ -61,16 +62,22 @@
             setVerticalPadding(seekBarEnabledVerticalPadding)
         }
 
-        data.duration?.let {
-            holder.seekBar.setMax(it)
-            holder.totalTimeView.setText(DateUtils.formatElapsedTime(
-                    it / DateUtils.SECOND_IN_MILLIS))
-        }
+        holder.seekBar.setMax(data.duration)
+        val totalTimeString = DateUtils.formatElapsedTime(
+            data.duration / DateUtils.SECOND_IN_MILLIS)
+        holder.totalTimeView.setText(totalTimeString)
 
         data.elapsedTime?.let {
             holder.seekBar.setProgress(it)
-            holder.elapsedTimeView.setText(DateUtils.formatElapsedTime(
-                    it / DateUtils.SECOND_IN_MILLIS))
+            val elapsedTimeString = DateUtils.formatElapsedTime(
+                it / DateUtils.SECOND_IN_MILLIS)
+            holder.elapsedTimeView.setText(elapsedTimeString)
+
+            holder.seekBar.contentDescription = holder.seekBar.context.getString(
+                R.string.controls_media_seekbar_description,
+                elapsedTimeString,
+                totalTimeString
+            )
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index d1b6548..125b87b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -135,14 +135,11 @@
             if (currentlyConnected && mController.isActiveRemoteDevice(device)
                     && mController.getSelectableMediaDevice().size() > 0) {
                 // Init active device layout
-                mDivider.setVisibility(View.VISIBLE);
-                mDivider.setTransitionAlpha(1);
                 mAddIcon.setVisibility(View.VISIBLE);
                 mAddIcon.setTransitionAlpha(1);
                 mAddIcon.setOnClickListener(this::onEndItemClick);
             } else {
                 // Init non-active device layout
-                mDivider.setVisibility(View.GONE);
                 mAddIcon.setVisibility(View.GONE);
             }
             if (mCurrentActivePosition == position) {
@@ -181,7 +178,6 @@
             super.onBind(customizedItem, topMargin, bottomMargin);
             if (customizedItem == CUSTOMIZED_ITEM_PAIR_NEW) {
                 mCheckBox.setVisibility(View.GONE);
-                mDivider.setVisibility(View.GONE);
                 mAddIcon.setVisibility(View.GONE);
                 mBottomDivider.setVisibility(View.GONE);
                 setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new),
@@ -196,13 +192,10 @@
                 mBottomDivider.setVisibility(View.GONE);
                 mCheckBox.setVisibility(View.GONE);
                 if (mController.getSelectableMediaDevice().size() > 0) {
-                    mDivider.setVisibility(View.VISIBLE);
-                    mDivider.setTransitionAlpha(1);
                     mAddIcon.setVisibility(View.VISIBLE);
                     mAddIcon.setTransitionAlpha(1);
                     mAddIcon.setOnClickListener(this::onEndItemClick);
                 } else {
-                    mDivider.setVisibility(View.GONE);
                     mAddIcon.setVisibility(View.GONE);
                 }
                 mTitleIcon.setImageDrawable(getSpeakerDrawable());
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index 0890841..1ffc2c4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -121,7 +121,6 @@
         final ProgressBar mProgressBar;
         final SeekBar mSeekBar;
         final RelativeLayout mTwoLineLayout;
-        final View mDivider;
         final View mBottomDivider;
         final CheckBox mCheckBox;
         private String mDeviceId;
@@ -136,7 +135,6 @@
             mTitleIcon = view.requireViewById(R.id.title_icon);
             mProgressBar = view.requireViewById(R.id.volume_indeterminate_progress);
             mSeekBar = view.requireViewById(R.id.volume_seekbar);
-            mDivider = view.requireViewById(R.id.end_divider);
             mBottomDivider = view.requireViewById(R.id.bottom_divider);
             mAddIcon = view.requireViewById(R.id.add_icon);
             mCheckBox = view.requireViewById(R.id.check_box);
@@ -151,21 +149,12 @@
                         return;
                     }
                     mTitleIcon.setImageIcon(icon);
-                    setMargin(topMargin, bottomMargin);
                 });
             });
         }
 
         void onBind(int customizedItem, boolean topMargin, boolean bottomMargin) {
-            setMargin(topMargin, bottomMargin);
-        }
-
-        private void setMargin(boolean topMargin, boolean bottomMargin) {
-            ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mContainerLayout
-                    .getLayoutParams();
-            params.topMargin = topMargin ? mMargin : 0;
-            params.bottomMargin = bottomMargin ? mMargin : 0;
-            mContainerLayout.setLayoutParams(params);
+            // TODO (b/201718621): clean up method after adjustment
         }
 
         void setSingleLineLayout(CharSequence title, boolean bFocused) {
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 85d0802..6895ef1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -175,7 +175,7 @@
         }
         if (!mAdapter.isDragging() && !mAdapter.isAnimating()) {
             int currentActivePosition = mAdapter.getCurrentActivePosition();
-            if (currentActivePosition >= 0) {
+            if (currentActivePosition >= 0 && currentActivePosition < mAdapter.getItemCount()) {
                 mAdapter.notifyItemChanged(currentActivePosition);
             } else {
                 mAdapter.notifyDataSetChanged();
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 437a0c8..42dd886 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -73,6 +73,7 @@
     private final String mPackageName;
     private final Context mContext;
     private final MediaSessionManager mMediaSessionManager;
+    private final LocalBluetoothManager mLocalBluetoothManager;
     private final ShadeController mShadeController;
     private final ActivityStarter mActivityStarter;
     private final DialogLaunchAnimator mDialogLaunchAnimator;
@@ -85,7 +86,6 @@
     private MediaController mMediaController;
     @VisibleForTesting
     Callback mCallback;
-    Callback mPreviousCallback;
     @VisibleForTesting
     LocalMediaManager mLocalMediaManager;
 
@@ -101,6 +101,7 @@
         mContext = context;
         mPackageName = packageName;
         mMediaSessionManager = mediaSessionManager;
+        mLocalBluetoothManager = lbm;
         mShadeController = shadeController;
         mActivityStarter = starter;
         mAboveStatusbar = aboveStatusbar;
@@ -135,19 +136,7 @@
             }
             return;
         }
-
-        if (mPreviousCallback != null) {
-            Log.w(TAG,
-                    "Callback started when mPreviousCallback is not null, which is unexpected");
-            mPreviousCallback.dismissDialog();
-        }
-
-        // If we start the output group dialog when the output dialog is shown, we need to keep a
-        // reference to the output dialog to set it back as the callback once we dismiss the output
-        // group dialog.
-        mPreviousCallback = mCallback;
         mCallback = cb;
-
         mLocalMediaManager.unregisterCallback(this);
         mLocalMediaManager.stopScan();
         mLocalMediaManager.registerCallback(this);
@@ -163,15 +152,6 @@
             mLocalMediaManager.stopScan();
         }
         mMediaDevices.clear();
-
-        // If there was a previous callback, i.e. we just dismissed the output group dialog and are
-        // now back on the output dialog, then we reset the callback to its previous value.
-        mCallback = null;
-        Callback previous = mPreviousCallback;
-        mPreviousCallback = null;
-        if (previous != null) {
-            start(previous);
-        }
     }
 
     @Override
@@ -480,7 +460,11 @@
 
     void launchMediaOutputGroupDialog(View mediaOutputDialog) {
         // We show the output group dialog from the output dialog.
-        MediaOutputGroupDialog dialog = new MediaOutputGroupDialog(mContext, mAboveStatusbar, this);
+        MediaOutputController controller = new MediaOutputController(mContext, mPackageName,
+                mAboveStatusbar, mMediaSessionManager, mLocalBluetoothManager, mShadeController,
+                mActivityStarter, mNotificationEntryManager, mUiEventLogger, mDialogLaunchAnimator);
+        MediaOutputGroupDialog dialog = new MediaOutputGroupDialog(mContext, mAboveStatusbar,
+                controller);
         mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
index 968c350..11d76db 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputGroupAdapter.java
@@ -96,7 +96,6 @@
         @Override
         void onBind(MediaDevice device, boolean topMargin, boolean bottomMargin, int position) {
             super.onBind(device, topMargin, bottomMargin, position);
-            mDivider.setVisibility(View.GONE);
             mAddIcon.setVisibility(View.GONE);
             mBottomDivider.setVisibility(View.GONE);
             mCheckBox.setVisibility(View.VISIBLE);
@@ -135,7 +134,6 @@
                 mTitleIcon.setImageDrawable(getSpeakerDrawable());
                 mBottomDivider.setVisibility(View.VISIBLE);
                 mCheckBox.setVisibility(View.GONE);
-                mDivider.setVisibility(View.GONE);
                 mAddIcon.setVisibility(View.GONE);
                 initSessionSeekbar();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 8576a28..7809b5f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -93,7 +93,6 @@
 import android.view.Display;
 import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
-import android.view.IWindowManager;
 import android.view.InsetsState.InternalInsetsType;
 import android.view.InsetsVisibilities;
 import android.view.KeyEvent;
@@ -118,20 +117,17 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.util.LatencyTracker;
 import com.android.internal.view.AppearanceRegion;
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
 import com.android.systemui.accessibility.SystemActions;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.buttons.ButtonDispatcher;
 import com.android.systemui.navigationbar.buttons.KeyButtonView;
 import com.android.systemui.navigationbar.buttons.RotationContextButton;
 import com.android.systemui.navigationbar.gestural.QuickswitchOrientedNavHandle;
-import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.recents.Recents;
@@ -151,8 +147,6 @@
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
-import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
 import com.android.wm.shell.pip.Pip;
@@ -162,6 +156,8 @@
 import java.util.Optional;
 import java.util.function.Consumer;
 
+import javax.inject.Inject;
+
 import dagger.Lazy;
 
 /**
@@ -243,7 +239,13 @@
     private boolean mTransientShown;
     private int mNavBarMode = NAV_BAR_MODE_3BUTTON;
     private LightBarController mLightBarController;
+    private final LightBarController mMainLightBarController;
+    private final LightBarController.Factory mLightBarControllerFactory;
     private AutoHideController mAutoHideController;
+    private final AutoHideController mMainAutoHideController;
+    private final AutoHideController.Factory mAutoHideControllerFactory;
+    private final Optional<TelecomManager> mTelecomManagerOptional;
+    private final InputMethodManager mInputMethodManager;
 
     @VisibleForTesting
     public int mDisplayId;
@@ -267,6 +269,7 @@
     private ViewTreeObserver.OnGlobalLayoutListener mOrientationHandleGlobalLayoutListener;
     private boolean mShowOrientedHandleForImmersiveMode;
 
+
     @com.android.internal.annotations.VisibleForTesting
     public enum NavBarActionEvent implements UiEventLogger.UiEventEnum {
 
@@ -478,11 +481,10 @@
                 }
             };
 
-    public NavigationBar(Context context,
+    private NavigationBar(Context context,
             WindowManager windowManager,
             Lazy<AssistManager> assistManagerLazy,
             AccessibilityManager accessibilityManager,
-            AccessibilityManagerWrapper accessibilityManagerWrapper,
             DeviceProvisionedController deviceProvisionedController,
             MetricsLogger metricsLogger,
             OverviewProxyService overviewProxyService,
@@ -504,7 +506,13 @@
             NavigationBarOverlayController navbarOverlayController,
             UiEventLogger uiEventLogger,
             NavigationBarA11yHelper navigationBarA11yHelper,
-            UserTracker userTracker) {
+            UserTracker userTracker,
+            LightBarController mainLightBarController,
+            LightBarController.Factory lightBarControllerFactory,
+            AutoHideController mainAutoHideController,
+            AutoHideController.Factory autoHideControllerFactory,
+            Optional<TelecomManager> telecomManagerOptional,
+            InputMethodManager inputMethodManager) {
         mContext = context;
         mWindowManager = windowManager;
         mAccessibilityManager = accessibilityManager;
@@ -531,6 +539,12 @@
         mNavigationBarA11yHelper = navigationBarA11yHelper;
         mUserTracker = userTracker;
         mNotificationShadeDepthController = notificationShadeDepthController;
+        mMainLightBarController = mainLightBarController;
+        mLightBarControllerFactory = lightBarControllerFactory;
+        mMainAutoHideController = mainAutoHideController;
+        mAutoHideControllerFactory = autoHideControllerFactory;
+        mTelecomManagerOptional = telecomManagerOptional;
+        mInputMethodManager = inputMethodManager;
 
         mNavBarMode = mNavigationModeController.addListener(this);
     }
@@ -548,7 +562,7 @@
         mNavigationBarView = barView.findViewById(R.id.navigation_bar_view);
 
         if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + barView);
-        mContext.getSystemService(WindowManager.class).addView(mFrame,
+        mWindowManager.addView(mFrame,
                 getBarLayoutParams(mContext.getResources().getConfiguration().windowConfiguration
                         .getRotation()));
         mDisplayId = mContext.getDisplayId();
@@ -606,8 +620,7 @@
     public void destroyView() {
         setAutoHideController(/* autoHideController */ null);
         mCommandQueue.removeCallback(this);
-        mContext.getSystemService(WindowManager.class).removeViewImmediate(
-                mNavigationBarView.getRootView());
+        mWindowManager.removeViewImmediate(mNavigationBarView.getRootView());
         mNavigationModeController.removeListener(this);
 
         mNavigationBarA11yHelper.removeA11yEventListener(mAccessibilityListener);
@@ -673,22 +686,16 @@
         // before notifications creation. We cannot directly use getLightBarController()
         // from NavigationBarFragment directly.
         LightBarController lightBarController = mIsOnDefaultDisplay
-                ? Dependency.get(LightBarController.class)
-                : new LightBarController(mContext,
-                        Dependency.get(DarkIconDispatcher.class),
-                        Dependency.get(BatteryController.class),
-                        Dependency.get(NavigationModeController.class),
-                        Dependency.get(DumpManager.class));
+                ? mMainLightBarController : mLightBarControllerFactory.create(mContext);
         setLightBarController(lightBarController);
 
         // TODO(b/118592525): to support multi-display, we start to add something which is
         //                    per-display, while others may be global. I think it's time to
         //                    add a new class maybe named DisplayDependency to solve
         //                    per-display Dependency problem.
+        // Alternative: this is a good case for a Dagger subcomponent. Same with LightBarController.
         AutoHideController autoHideController = mIsOnDefaultDisplay
-                ? Dependency.get(AutoHideController.class)
-                : new AutoHideController(mContext, mHandler,
-                        Dependency.get(IWindowManager.class));
+                ? mMainAutoHideController : mAutoHideControllerFactory.create(mContext);
         setAutoHideController(autoHideController);
         restoreAppearanceAndTransientState();
     }
@@ -1183,9 +1190,8 @@
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 mHomeBlockedThisTouch = false;
-                TelecomManager telecomManager =
-                        mContext.getSystemService(TelecomManager.class);
-                if (telecomManager != null && telecomManager.isRinging()) {
+                if (mTelecomManagerOptional.isPresent()
+                        && mTelecomManagerOptional.get().isRinging()) {
                     if (statusBarOptional.map(StatusBar::isKeyguardShowing).orElse(false)) {
                         Log.i(TAG, "Ignoring HOME; there's a ringing incoming call. " +
                                 "No heads up");
@@ -1267,7 +1273,7 @@
     }
 
     private void onImeSwitcherClick(View v) {
-        mContext.getSystemService(InputMethodManager.class).showInputMethodPickerFromSystem(
+        mInputMethodManager.showInputMethodPickerFromSystem(
                 true /* showAuxiliarySubtypes */, mDisplayId);
     };
 
@@ -1702,4 +1708,121 @@
     int getNavigationIconHints() {
         return mNavigationIconHints;
     }
-}
+
+    /**
+     * Injectable factory for construction a {@link NavigationBar}.
+     */
+    public static class Factory {
+        private final Lazy<AssistManager> mAssistManagerLazy;
+        private final AccessibilityManager mAccessibilityManager;
+        private final DeviceProvisionedController mDeviceProvisionedController;
+        private final MetricsLogger mMetricsLogger;
+        private final OverviewProxyService mOverviewProxyService;
+        private final NavigationModeController mNavigationModeController;
+        private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
+        private final StatusBarStateController mStatusBarStateController;
+        private final SysUiState mSysUiFlagsContainer;
+        private final BroadcastDispatcher mBroadcastDispatcher;
+        private final CommandQueue mCommandQueue;
+        private final Optional<Pip> mPipOptional;
+        private final Optional<LegacySplitScreen> mSplitScreenOptional;
+        private final Optional<Recents> mRecentsOptional;
+        private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
+        private final ShadeController mShadeController;
+        private final NotificationRemoteInputManager mNotificationRemoteInputManager;
+        private final NotificationShadeDepthController mNotificationShadeDepthController;
+        private final SystemActions mSystemActions;
+        private final Handler mMainHandler;
+        private final NavigationBarOverlayController mNavbarOverlayController;
+        private final UiEventLogger mUiEventLogger;
+        private final NavigationBarA11yHelper mNavigationBarA11yHelper;
+        private final UserTracker mUserTracker;
+        private final LightBarController mMainLightBarController;
+        private final LightBarController.Factory mLightBarControllerFactory;
+        private final AutoHideController mMainAutoHideController;
+        private final AutoHideController.Factory mAutoHideControllerFactory;
+        private final Optional<TelecomManager> mTelecomManagerOptional;
+        private final InputMethodManager mInputMethodManager;
+
+        @Inject
+        public Factory(
+                Lazy<AssistManager> assistManagerLazy,
+                AccessibilityManager accessibilityManager,
+                DeviceProvisionedController deviceProvisionedController,
+                MetricsLogger metricsLogger,
+                OverviewProxyService overviewProxyService,
+                NavigationModeController navigationModeController,
+                AccessibilityButtonModeObserver accessibilityButtonModeObserver,
+                StatusBarStateController statusBarStateController,
+                SysUiState sysUiFlagsContainer,
+                BroadcastDispatcher broadcastDispatcher,
+                CommandQueue commandQueue,
+                Optional<Pip> pipOptional,
+                Optional<LegacySplitScreen> splitScreenOptional,
+                Optional<Recents> recentsOptional,
+                Lazy<Optional<StatusBar>> statusBarOptionalLazy,
+                ShadeController shadeController,
+                NotificationRemoteInputManager notificationRemoteInputManager,
+                NotificationShadeDepthController notificationShadeDepthController,
+                SystemActions systemActions,
+                @Main Handler mainHandler,
+                NavigationBarOverlayController navbarOverlayController,
+                UiEventLogger uiEventLogger,
+                NavigationBarA11yHelper navigationBarA11yHelper,
+                UserTracker userTracker,
+                LightBarController mainLightBarController,
+                LightBarController.Factory lightBarControllerFactory,
+                AutoHideController mainAutoHideController,
+                AutoHideController.Factory autoHideControllerFactory,
+                Optional<TelecomManager> telecomManagerOptional,
+                InputMethodManager inputMethodManager) {
+            mAssistManagerLazy = assistManagerLazy;
+            mAccessibilityManager = accessibilityManager;
+            mDeviceProvisionedController = deviceProvisionedController;
+            mMetricsLogger = metricsLogger;
+            mOverviewProxyService = overviewProxyService;
+            mNavigationModeController = navigationModeController;
+            mAccessibilityButtonModeObserver = accessibilityButtonModeObserver;
+            mStatusBarStateController = statusBarStateController;
+            mSysUiFlagsContainer = sysUiFlagsContainer;
+            mBroadcastDispatcher = broadcastDispatcher;
+            mCommandQueue = commandQueue;
+            mPipOptional = pipOptional;
+            mSplitScreenOptional = splitScreenOptional;
+            mRecentsOptional = recentsOptional;
+            mStatusBarOptionalLazy = statusBarOptionalLazy;
+            mShadeController = shadeController;
+            mNotificationRemoteInputManager = notificationRemoteInputManager;
+            mNotificationShadeDepthController = notificationShadeDepthController;
+            mSystemActions = systemActions;
+            mMainHandler = mainHandler;
+            mNavbarOverlayController = navbarOverlayController;
+            mUiEventLogger = uiEventLogger;
+            mNavigationBarA11yHelper = navigationBarA11yHelper;
+            mUserTracker = userTracker;
+            mMainLightBarController = mainLightBarController;
+            mLightBarControllerFactory = lightBarControllerFactory;
+            mMainAutoHideController = mainAutoHideController;
+            mAutoHideControllerFactory = autoHideControllerFactory;
+            mTelecomManagerOptional = telecomManagerOptional;
+            mInputMethodManager = inputMethodManager;
+        }
+
+        /** Construct a {@link NavigationBar} */
+        public NavigationBar create(Context context) {
+            final WindowManager wm = context.getSystemService(WindowManager.class);
+            return new NavigationBar(context, wm, mAssistManagerLazy,
+                    mAccessibilityManager, mDeviceProvisionedController, mMetricsLogger,
+                    mOverviewProxyService, mNavigationModeController,
+                    mAccessibilityButtonModeObserver, mStatusBarStateController,
+                    mSysUiFlagsContainer, mBroadcastDispatcher, mCommandQueue, mPipOptional,
+                    mSplitScreenOptional, mRecentsOptional, mStatusBarOptionalLazy,
+                    mShadeController, mNotificationRemoteInputManager,
+                    mNotificationShadeDepthController, mSystemActions, mMainHandler,
+                    mNavbarOverlayController, mUiEventLogger, mNavigationBarA11yHelper,
+                    mUserTracker, mMainLightBarController, mLightBarControllerFactory,
+                    mMainAutoHideController, mAutoHideControllerFactory, mTelecomManagerOptional,
+                    mInputMethodManager);
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
index a1a630a..97bcb00 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
@@ -32,52 +32,31 @@
 import android.view.Display;
 import android.view.IWindowManager;
 import android.view.View;
-import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
-import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
 import com.android.internal.statusbar.RegisterStatusBarResult;
 import com.android.settingslib.applications.InterestingConfigChanges;
 import com.android.systemui.Dumpable;
-import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
-import com.android.systemui.accessibility.SystemActions;
-import com.android.systemui.assist.AssistManager;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.recents.Recents;
-import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.CommandQueue.Callbacks;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode;
-import com.android.systemui.statusbar.phone.ShadeController;
-import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
-import com.android.wm.shell.pip.Pip;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.Optional;
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
-
 
 /** A controller to handle navigation bars. */
 @SysUISingleton
@@ -90,36 +69,12 @@
     private static final String TAG = NavigationBarController.class.getSimpleName();
 
     private final Context mContext;
-    private final WindowManager mWindowManager;
-    private final Lazy<AssistManager> mAssistManagerLazy;
-    private final AccessibilityManager mAccessibilityManager;
-    private final AccessibilityManagerWrapper mAccessibilityManagerWrapper;
-    private final DeviceProvisionedController mDeviceProvisionedController;
-    private final MetricsLogger mMetricsLogger;
-    private final OverviewProxyService mOverviewProxyService;
-    private final NavigationModeController mNavigationModeController;
-    private final AccessibilityButtonModeObserver mAccessibilityButtonModeObserver;
-    private final StatusBarStateController mStatusBarStateController;
-    private final SysUiState mSysUiFlagsContainer;
-    private final BroadcastDispatcher mBroadcastDispatcher;
-    private final CommandQueue mCommandQueue;
-    private final Optional<Pip> mPipOptional;
-    private final Optional<LegacySplitScreen> mSplitScreenOptional;
-    private final Optional<Recents> mRecentsOptional;
-    private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
-    private final ShadeController mShadeController;
-    private final NotificationRemoteInputManager mNotificationRemoteInputManager;
-    private final SystemActions mSystemActions;
-    private final UiEventLogger mUiEventLogger;
     private final Handler mHandler;
-    private final NavigationBarA11yHelper mNavigationBarA11yHelper;
+    private final NavigationBar.Factory mNavigationBarFactory;
     private final DisplayManager mDisplayManager;
-    private final NavigationBarOverlayController mNavBarOverlayController;
     private final TaskbarDelegate mTaskbarDelegate;
-    private final NotificationShadeDepthController mNotificationShadeDepthController;
     private int mNavMode;
     @VisibleForTesting boolean mIsTablet;
-    private final UserTracker mUserTracker;
 
     /** A displayId - nav bar maps. */
     @VisibleForTesting
@@ -132,73 +87,30 @@
 
     @Inject
     public NavigationBarController(Context context,
-            WindowManager windowManager,
-            Lazy<AssistManager> assistManagerLazy,
-            AccessibilityManager accessibilityManager,
-            AccessibilityManagerWrapper accessibilityManagerWrapper,
-            DeviceProvisionedController deviceProvisionedController,
-            MetricsLogger metricsLogger,
             OverviewProxyService overviewProxyService,
             NavigationModeController navigationModeController,
-            AccessibilityButtonModeObserver accessibilityButtonModeObserver,
-            StatusBarStateController statusBarStateController,
             SysUiState sysUiFlagsContainer,
-            BroadcastDispatcher broadcastDispatcher,
             CommandQueue commandQueue,
-            Optional<Pip> pipOptional,
-            Optional<LegacySplitScreen> splitScreenOptional,
-            Optional<Recents> recentsOptional,
-            Lazy<Optional<StatusBar>> statusBarOptionalLazy,
-            ShadeController shadeController,
-            NotificationRemoteInputManager notificationRemoteInputManager,
-            NotificationShadeDepthController notificationShadeDepthController,
-            SystemActions systemActions,
             @Main Handler mainHandler,
-            UiEventLogger uiEventLogger,
-            NavigationBarOverlayController navBarOverlayController,
             ConfigurationController configurationController,
             NavigationBarA11yHelper navigationBarA11yHelper,
             TaskbarDelegate taskbarDelegate,
-            UserTracker userTracker,
-            DumpManager dumpManager) {
+            NavigationBar.Factory navigationBarFactory,
+            DumpManager dumpManager,
+            AutoHideController autoHideController) {
         mContext = context;
-        mWindowManager = windowManager;
-        mAssistManagerLazy = assistManagerLazy;
-        mAccessibilityManager = accessibilityManager;
-        mAccessibilityManagerWrapper = accessibilityManagerWrapper;
-        mDeviceProvisionedController = deviceProvisionedController;
-        mMetricsLogger = metricsLogger;
-        mOverviewProxyService = overviewProxyService;
-        mNavigationModeController = navigationModeController;
-        mAccessibilityButtonModeObserver = accessibilityButtonModeObserver;
-        mStatusBarStateController = statusBarStateController;
-        mSysUiFlagsContainer = sysUiFlagsContainer;
-        mBroadcastDispatcher = broadcastDispatcher;
-        mCommandQueue = commandQueue;
-        mPipOptional = pipOptional;
-        mSplitScreenOptional = splitScreenOptional;
-        mRecentsOptional = recentsOptional;
-        mStatusBarOptionalLazy = statusBarOptionalLazy;
-        mShadeController = shadeController;
-        mNotificationRemoteInputManager = notificationRemoteInputManager;
-        mNotificationShadeDepthController = notificationShadeDepthController;
-        mSystemActions = systemActions;
-        mUiEventLogger = uiEventLogger;
         mHandler = mainHandler;
-        mNavigationBarA11yHelper = navigationBarA11yHelper;
+        mNavigationBarFactory = navigationBarFactory;
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         commandQueue.addCallback(this);
         configurationController.addCallback(this);
         mConfigChanges.applyNewConfig(mContext.getResources());
-        mNavBarOverlayController = navBarOverlayController;
-        mNavMode = mNavigationModeController.addListener(this);
-        mNavigationModeController.addListener(this);
+        mNavMode = navigationModeController.addListener(this);
         mTaskbarDelegate = taskbarDelegate;
-        mTaskbarDelegate.setOverviewProxyService(commandQueue, overviewProxyService,
-                navigationBarA11yHelper, navigationModeController, sysUiFlagsContainer);
+        mTaskbarDelegate.setDependencies(commandQueue, overviewProxyService,
+                navigationBarA11yHelper, navigationModeController, sysUiFlagsContainer,
+                dumpManager, autoHideController);
         mIsTablet = isTablet(mContext);
-        mUserTracker = userTracker;
-
         dumpManager.registerDumpable(this);
     }
 
@@ -354,33 +266,8 @@
         final Context context = isOnDefaultDisplay
                 ? mContext
                 : mContext.createDisplayContext(display);
-        NavigationBar navBar = new NavigationBar(context,
-                mWindowManager,
-                mAssistManagerLazy,
-                mAccessibilityManager,
-                mAccessibilityManagerWrapper,
-                mDeviceProvisionedController,
-                mMetricsLogger,
-                mOverviewProxyService,
-                mNavigationModeController,
-                mAccessibilityButtonModeObserver,
-                mStatusBarStateController,
-                mSysUiFlagsContainer,
-                mBroadcastDispatcher,
-                mCommandQueue,
-                mPipOptional,
-                mSplitScreenOptional,
-                mRecentsOptional,
-                mStatusBarOptionalLazy,
-                mShadeController,
-                mNotificationRemoteInputManager,
-                mNotificationShadeDepthController,
-                mSystemActions,
-                mHandler,
-                mNavBarOverlayController,
-                mUiEventLogger,
-                mNavigationBarA11yHelper,
-                mUserTracker);
+        NavigationBar navBar = mNavigationBarFactory.create(context);
+
         mNavigationBars.put(displayId, navBar);
 
         View navigationBarView = navBar.createView(savedState);
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java
index 0603bb7..73a0c54 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationModeController.java
@@ -118,7 +118,7 @@
 
         configurationController.addCallback(new ConfigurationController.ConfigurationListener() {
             @Override
-            public void onOverlayChanged() {
+            public void onThemeChanged() {
                 if (DEBUG) {
                     Log.d(TAG, "onOverlayChanged");
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
index fa616921..d707dbd 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/TaskbarDelegate.java
@@ -19,6 +19,8 @@
 import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
 import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
+import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
+import static android.view.InsetsState.containsType;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
@@ -46,14 +48,23 @@
 import android.view.View;
 import android.view.WindowInsetsController.Behavior;
 
+import androidx.annotation.NonNull;
+
 import com.android.internal.view.AppearanceRegion;
 import com.android.systemui.Dependency;
+import com.android.systemui.Dumpable;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.shared.recents.utilities.Utilities;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.statusbar.AutoHideUiElement;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.phone.AutoHideController;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -61,7 +72,7 @@
 @Singleton
 public class TaskbarDelegate implements CommandQueue.Callbacks,
         OverviewProxyService.OverviewProxyListener, NavigationModeController.ModeChangedListener,
-        ComponentCallbacks {
+        ComponentCallbacks, Dumpable {
 
     private final EdgeBackGestureHandler mEdgeBackGestureHandler;
 
@@ -70,6 +81,7 @@
     private NavigationBarA11yHelper mNavigationBarA11yHelper;
     private NavigationModeController mNavigationModeController;
     private SysUiState mSysUiState;
+    private AutoHideController mAutoHideController;
     private int mDisplayId;
     private int mNavigationIconHints;
     private final NavigationBarA11yHelper.NavA11yEventListener mNavA11yEventListener =
@@ -80,6 +92,28 @@
     private final Context mContext;
     private final DisplayManager mDisplayManager;
     private Context mWindowContext;
+    /**
+     * Tracks the system calls for when taskbar should transiently show or hide so we can return
+     * this value in {@link AutoHideUiElement#isVisible()} below.
+     *
+     * This also gets set by {@link #onTaskbarAutohideSuspend(boolean)} to force show the transient
+     * taskbar if launcher has requested to suspend auto-hide behavior.
+     */
+    private boolean mTaskbarTransientShowing;
+    private final AutoHideUiElement mAutoHideUiElement = new AutoHideUiElement() {
+        @Override
+        public void synchronizeState() {
+        }
+
+        @Override
+        public boolean isVisible() {
+            return mTaskbarTransientShowing;
+        }
+
+        @Override
+        public void hide() {
+        }
+    };
 
     @Inject
     public TaskbarDelegate(Context context) {
@@ -89,29 +123,20 @@
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
     }
 
-    public void setOverviewProxyService(CommandQueue commandQueue,
+    public void setDependencies(CommandQueue commandQueue,
             OverviewProxyService overviewProxyService,
             NavigationBarA11yHelper navigationBarA11yHelper,
             NavigationModeController navigationModeController,
-            SysUiState sysUiState) {
+            SysUiState sysUiState, DumpManager dumpManager,
+            AutoHideController autoHideController) {
         // TODO: adding this in the ctor results in a dagger dependency cycle :(
         mCommandQueue = commandQueue;
         mOverviewProxyService = overviewProxyService;
         mNavigationBarA11yHelper = navigationBarA11yHelper;
         mNavigationModeController = navigationModeController;
         mSysUiState = sysUiState;
-    }
-
-    public void destroy() {
-        mCommandQueue.removeCallback(this);
-        mOverviewProxyService.removeCallback(this);
-        mNavigationModeController.removeListener(this);
-        mNavigationBarA11yHelper.removeA11yEventListener(mNavA11yEventListener);
-        mEdgeBackGestureHandler.onNavBarDetached();
-        if (mWindowContext != null) {
-            mWindowContext.unregisterComponentCallbacks(this);
-            mWindowContext = null;
-        }
+        dumpManager.registerDumpable(this);
+        mAutoHideController = autoHideController;
     }
 
     public void init(int displayId) {
@@ -128,6 +153,20 @@
         mWindowContext.registerComponentCallbacks(this);
         // Set initial state for any listeners
         updateSysuiFlags();
+        mAutoHideController.setNavigationBar(mAutoHideUiElement);
+    }
+
+    public void destroy() {
+        mCommandQueue.removeCallback(this);
+        mOverviewProxyService.removeCallback(this);
+        mNavigationModeController.removeListener(this);
+        mNavigationBarA11yHelper.removeA11yEventListener(mNavA11yEventListener);
+        mEdgeBackGestureHandler.onNavBarDetached();
+        if (mWindowContext != null) {
+            mWindowContext.unregisterComponentCallbacks(this);
+            mWindowContext = null;
+        }
+        mAutoHideController.setNavigationBar(null);
     }
 
     private void updateSysuiFlags() {
@@ -201,6 +240,38 @@
     }
 
     @Override
+    public void showTransient(int displayId, int[] types) {
+        if (displayId != mDisplayId) {
+            return;
+        }
+        if (!containsType(types, ITYPE_NAVIGATION_BAR)) {
+            return;
+        }
+        mTaskbarTransientShowing = true;
+    }
+
+    @Override
+    public void abortTransient(int displayId, int[] types) {
+        if (displayId != mDisplayId) {
+            return;
+        }
+        if (!containsType(types, ITYPE_NAVIGATION_BAR)) {
+            return;
+        }
+        mTaskbarTransientShowing = false;
+    }
+
+    @Override
+    public void onTaskbarAutohideSuspend(boolean suspend) {
+        mTaskbarTransientShowing = suspend;
+        if (suspend) {
+            mAutoHideController.suspendAutoHide();
+        } else {
+            mAutoHideController.resumeSuspendedAutoHide();
+        }
+    }
+
+    @Override
     public void onNavigationModeChanged(int mode) {
         mEdgeBackGestureHandler.onNavigationModeChanged(mode);
     }
@@ -220,4 +291,15 @@
 
     @Override
     public void onLowMemory() {}
+
+    @Override
+    public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
+        pw.println("TaskbarDelegate (displayId=" + mDisplayId + "):");
+        pw.println("  mNavigationIconHints=" + mNavigationIconHints);
+        pw.println("  mDisabledFlags=" + mDisabledFlags);
+        pw.println("  mTaskBarWindowState=" + mTaskBarWindowState);
+        pw.println("  mBehavior=" + mBehavior);
+        pw.println("  mTaskbarTransientShowing=" + mTaskbarTransientShowing);
+        mEdgeBackGestureHandler.dump(pw);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index c6da342..fa37c86 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -39,7 +39,6 @@
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.Choreographer;
-import android.view.Display;
 import android.view.ISystemGestureExclusionListener;
 import android.view.IWindowManager;
 import android.view.InputDevice;
@@ -105,9 +104,6 @@
     static final boolean DEBUG_MISSING_GESTURE = true;
     static final String DEBUG_MISSING_GESTURE_TAG = "NoBackGesture";
 
-    private static final boolean ENABLE_PER_WINDOW_INPUT_ROTATION =
-            SystemProperties.getBoolean("persist.debug.per_window_input_rotation", false);
-
     private ISystemGestureExclusionListener mGestureExclusionListener =
             new ISystemGestureExclusionListener.Stub() {
                 @Override
@@ -559,16 +555,6 @@
     private void onInputEvent(InputEvent ev) {
         if (!(ev instanceof MotionEvent)) return;
         MotionEvent event = (MotionEvent) ev;
-        if (ENABLE_PER_WINDOW_INPUT_ROTATION) {
-            final Display display = mContext.getDisplay();
-            int rotation = display.getRotation();
-            if (rotation != Surface.ROTATION_0) {
-                Point sz = new Point();
-                display.getRealSize(sz);
-                event = MotionEvent.obtain(event);
-                event.transform(MotionEvent.createRotateMatrix(rotation, sz.x, sz.y));
-            }
-        }
         onMotionEvent(event);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index bfb63ea..90d3448 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -35,7 +35,6 @@
 import com.android.systemui.qs.TouchAnimator.Listener;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.tileimpl.HeightOverrideable;
-import com.android.systemui.statusbar.CrossFadeHelper;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
 import com.android.wm.shell.animation.Interpolators;
@@ -170,19 +169,6 @@
         }
     }
 
-    void startAlphaAnimation(boolean show) {
-        if (show == mToShowing) {
-            return;
-        }
-        mToShowing = show;
-        if (show) {
-            CrossFadeHelper.fadeIn(mQs.getView(), QQS_FADE_IN_DURATION, 0 /* delay */);
-        } else {
-            CrossFadeHelper.fadeOut(mQs.getView(), QQS_FADE_OUT_DURATION, 0 /* delay */,
-                    null /* endRunnable */);
-        }
-    }
-
     /**
      * Sets whether or not the keyguard is currently being shown with a collapsed header.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 8e43661..3fc4f50 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -26,7 +26,6 @@
 import android.graphics.PointF;
 import android.util.AttributeSet;
 import android.view.View;
-import android.view.WindowInsets;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
@@ -58,7 +57,6 @@
     private int mSideMargins;
     private boolean mQsDisabled;
     private int mContentPadding = -1;
-    private int mNavBarInset = 0;
     private boolean mClippingEnabled;
 
     public QSContainerImpl(Context context, AttributeSet attrs) {
@@ -95,24 +93,13 @@
     }
 
     @Override
-    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        mNavBarInset = insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
-        mQSPanelContainer.setPaddingRelative(
-                mQSPanelContainer.getPaddingStart(),
-                mQSPanelContainer.getPaddingTop(),
-                mQSPanelContainer.getPaddingEnd(),
-                mNavBarInset
-        );
-        return super.onApplyWindowInsets(insets);
-    }
-
-    @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         // QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the
         // bottom and footer are inside the screen.
         MarginLayoutParams layoutParams = (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
 
-        int maxQs = getDisplayHeight() - layoutParams.topMargin - layoutParams.bottomMargin
+        int availableHeight = View.MeasureSpec.getSize(heightMeasureSpec);
+        int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin
                 - getPaddingBottom();
         int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
                 + layoutParams.rightMargin;
@@ -122,11 +109,11 @@
                 MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
         int width = mQSPanelContainer.getMeasuredWidth() + padding;
         super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
-                MeasureSpec.makeMeasureSpec(getDisplayHeight(), MeasureSpec.EXACTLY));
+                MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
         // QSCustomizer will always be the height of the screen, but do this after
         // other measuring to avoid changing the height of the QS.
         mQSCustomizer.measure(widthMeasureSpec,
-                MeasureSpec.makeMeasureSpec(getDisplayHeight(), MeasureSpec.EXACTLY));
+                MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
index 929927e..58a942a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetail.java
@@ -46,8 +46,8 @@
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.DetailAdapter;
+import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
 
 public class QSDetail extends LinearLayout {
 
@@ -86,7 +86,7 @@
     private boolean mSwitchState;
     private QSFooter mFooter;
 
-    private NotificationsQuickSettingsContainer mContainer;
+    private QSContainerController mQsContainerController;
 
     public QSDetail(Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
@@ -120,8 +120,8 @@
         mClipper = new QSDetailClipper(this);
     }
 
-    public void setContainer(NotificationsQuickSettingsContainer container) {
-        mContainer = container;
+    public void setContainerController(QSContainerController controller) {
+        mQsContainerController = controller;
     }
 
     /** */
@@ -262,8 +262,8 @@
         }
         sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
         animateDetailVisibleDiff(x, y, visibleDiff, listener);
-        if (mContainer != null) {
-            mContainer.setDetailShowing(showingDetail);
+        if (mQsContainerController != null) {
+            mQsContainerController.setDetailShowing(showingDetail);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index a1aff57..17c2fd0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -44,6 +44,7 @@
 import com.android.systemui.media.MediaHost;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSFragmentComponent;
@@ -51,7 +52,6 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
 import com.android.systemui.util.InjectionInflationController;
@@ -90,6 +90,7 @@
     private int mLayoutDirection;
     private QSFooter mFooter;
     private float mLastQSExpansion = -1;
+    private float mLastPanelFraction;
     private boolean mQsDisabled;
     private ImageView mQsDragHandler;
 
@@ -121,7 +122,7 @@
      * When true, QS will translate from outside the screen. It will be clipped with parallax
      * otherwise.
      */
-    private boolean mTranslateWhileExpanding;
+    private boolean mInSplitShade;
     private boolean mPulseExpanding;
 
     /**
@@ -136,6 +137,12 @@
 
     private DumpManager mDumpManager;
 
+    /**
+     * Progress of pull down from the center of the lock screen.
+     * @see com.android.systemui.statusbar.LockscreenShadeTransitionController
+     */
+    private float mFullShadeProgress;
+
     @Inject
     public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
             InjectionInflationController injectionInflater, QSTileHost qsTileHost,
@@ -227,7 +234,8 @@
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                     boolean sizeChanged = (oldTop - oldBottom) != (top - bottom);
                     if (sizeChanged) {
-                        setQsExpansion(mLastQSExpansion, mLastHeaderTranslation);
+                        setQsExpansion(mLastQSExpansion, mLastPanelFraction,
+                                mLastHeaderTranslation);
                     }
                 });
         mQSPanelController.setUsingHorizontalLayoutChangeListener(
@@ -337,11 +345,9 @@
     }
 
     @Override
-    public void setContainer(ViewGroup container) {
-        if (container instanceof NotificationsQuickSettingsContainer) {
-            mQSCustomizerController.setContainer((NotificationsQuickSettingsContainer) container);
-            mQSDetail.setContainer((NotificationsQuickSettingsContainer) container);
-        }
+    public void setContainerController(QSContainerController controller) {
+        mQSCustomizerController.setContainerController(controller);
+        mQSDetail.setContainerController(controller);
     }
 
     @Override
@@ -411,7 +417,7 @@
                 mQSAnimator.setShowCollapsedOnKeyguard(showCollapsed);
             }
             if (!showCollapsed && isKeyguardState()) {
-                setQsExpansion(mLastQSExpansion, 0);
+                setQsExpansion(mLastQSExpansion, mLastPanelFraction, 0);
             }
         }
     }
@@ -479,33 +485,30 @@
     }
 
     @Override
-    public void setTranslateWhileExpanding(boolean shouldTranslate) {
-        mTranslateWhileExpanding = shouldTranslate;
-        mQSAnimator.setTranslateWhileExpanding(shouldTranslate);
+    public void setInSplitShade(boolean inSplitShade) {
+        mInSplitShade = inSplitShade;
+        mQSAnimator.setTranslateWhileExpanding(inSplitShade);
     }
 
     @Override
-    public void setTransitionToFullShadeAmount(float pxAmount, boolean animated) {
+    public void setTransitionToFullShadeAmount(float pxAmount, float progress) {
         boolean isTransitioningToFullShade = pxAmount > 0;
         if (isTransitioningToFullShade != mTransitioningToFullShade) {
             mTransitioningToFullShade = isTransitioningToFullShade;
             updateShowCollapsedOnKeyguard();
-            setQsExpansion(mLastQSExpansion, mLastHeaderTranslation);
         }
+        mFullShadeProgress = progress;
+        setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation);
     }
 
     @Override
-    public void setQsExpansion(float expansion, float proposedTranslation) {
-        if (DEBUG) Log.d(TAG, "setQSExpansion " + expansion + " " + proposedTranslation);
+    public void setQsExpansion(float expansion, float panelExpansionFraction,
+            float proposedTranslation) {
         float headerTranslation = mTransitioningToFullShade ? 0 : proposedTranslation;
-        if (mQSAnimator != null) {
-            final boolean showQSOnLockscreen = expansion > 0;
-            final boolean showQSUnlocked = headerTranslation == 0 || !mTranslateWhileExpanding;
-            mQSAnimator.startAlphaAnimation(showQSOnLockscreen || showQSUnlocked
-                    || mTransitioningToFullShade);
-        }
+        float progress = mTransitioningToFullShade ? mFullShadeProgress : panelExpansionFraction;
+        setAlphaAnimationProgress(mInSplitShade ? progress : 1);
         mContainer.setExpansion(expansion);
-        final float translationScaleY = (mTranslateWhileExpanding
+        final float translationScaleY = (mInSplitShade
                 ? 1 : QSAnimator.SHORT_PARALLAX_AMOUNT) * (expansion - 1);
         boolean onKeyguardAndExpanded = isKeyguardState() && !mShowCollapsedOnKeyguard;
         if (!mHeaderAnimating && !headerWillBeAnimating()) {
@@ -522,6 +525,7 @@
             return;
         }
         mLastHeaderTranslation = headerTranslation;
+        mLastPanelFraction = panelExpansionFraction;
         mLastQSExpansion = expansion;
         mLastKeyguardAndExpanded = onKeyguardAndExpanded;
         mLastViewHeight = currentHeight;
@@ -563,6 +567,17 @@
         updateMediaPositions();
     }
 
+    private void setAlphaAnimationProgress(float progress) {
+        final View view = getView();
+        if (progress == 0 && view.getVisibility() != View.INVISIBLE) {
+            view.setVisibility(View.INVISIBLE);
+        } else if (progress > 0 && view.getVisibility() != View.VISIBLE) {
+            view.setVisibility((View.VISIBLE));
+        }
+        float alpha = Interpolators.getNotificationScrimAlpha(progress, true /* uiContent */);
+        view.setAlpha(alpha);
+    }
+
     private void updateQsBounds() {
         if (mLastQSExpansion == 1.0f) {
             // Fully expanded, let's set the layout bounds as clip bounds. This is necessary because
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index e6ab436..c692252 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -33,9 +33,9 @@
 
 import com.android.systemui.R;
 import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.qs.QSDetailClipper;
 import com.android.systemui.statusbar.phone.LightBarController;
-import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
 
 /**
  * Allows full-screen customization of QS, through show() and hide().
@@ -54,7 +54,7 @@
     private boolean isShown;
     private final RecyclerView mRecyclerView;
     private boolean mCustomizing;
-    private NotificationsQuickSettingsContainer mNotifQsContainer;
+    private QSContainerController mQsContainerController;
     private QS mQs;
     private int mX;
     private int mY;
@@ -103,8 +103,8 @@
         lightBarController.setQsCustomizing(mIsShowingNavBackdrop && isShown);
     }
 
-    public void setContainer(NotificationsQuickSettingsContainer notificationsQsContainer) {
-        mNotifQsContainer = notificationsQsContainer;
+    public void setContainerController(QSContainerController controller) {
+        mQsContainerController = controller;
     }
 
     public void setQs(QS qs) {
@@ -124,8 +124,8 @@
             mOpening = true;
             setVisibility(View.VISIBLE);
             mClipper.animateCircularClip(mX, mY, true, new ExpandAnimatorListener(tileAdapter));
-            mNotifQsContainer.setCustomizerAnimating(true);
-            mNotifQsContainer.setCustomizerShowing(true);
+            mQsContainerController.setCustomizerAnimating(true);
+            mQsContainerController.setCustomizerShowing(true);
         }
     }
 
@@ -138,8 +138,8 @@
             mClipper.showBackground();
             isShown = true;
             setCustomizing(true);
-            mNotifQsContainer.setCustomizerAnimating(false);
-            mNotifQsContainer.setCustomizerShowing(true);
+            mQsContainerController.setCustomizerAnimating(false);
+            mQsContainerController.setCustomizerShowing(true);
         }
     }
 
@@ -156,8 +156,8 @@
             } else {
                 setVisibility(View.GONE);
             }
-            mNotifQsContainer.setCustomizerAnimating(animate);
-            mNotifQsContainer.setCustomizerShowing(false);
+            mQsContainerController.setCustomizerAnimating(animate);
+            mQsContainerController.setCustomizerShowing(false);
         }
     }
 
@@ -195,7 +195,7 @@
                 setCustomizing(true);
             }
             mOpening = false;
-            mNotifQsContainer.setCustomizerAnimating(false);
+            mQsContainerController.setCustomizerAnimating(false);
             mRecyclerView.setAdapter(mTileAdapter);
         }
 
@@ -203,7 +203,7 @@
         public void onAnimationCancel(Animator animation) {
             mOpening = false;
             mQs.notifyCustomizeChanged();
-            mNotifQsContainer.setCustomizerAnimating(false);
+            mQsContainerController.setCustomizerAnimating(false);
         }
     }
 
@@ -213,7 +213,7 @@
             if (!isShown) {
                 setVisibility(View.GONE);
             }
-            mNotifQsContainer.setCustomizerAnimating(false);
+            mQsContainerController.setCustomizerAnimating(false);
         }
 
         @Override
@@ -221,7 +221,7 @@
             if (!isShown) {
                 setVisibility(View.GONE);
             }
-            mNotifQsContainer.setCustomizerAnimating(false);
+            mQsContainerController.setCustomizerAnimating(false);
         }
     };
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
index 49d18e6..618a429 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java
@@ -35,13 +35,13 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.R;
 import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.QSEditEvent;
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.statusbar.phone.LightBarController;
-import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -233,8 +233,8 @@
     }
 
     /** */
-    public void setContainer(NotificationsQuickSettingsContainer container) {
-        mView.setContainer(container);
+    public void setContainerController(QSContainerController controller) {
+        mView.setContainerController(controller);
     }
 
     public boolean isShown() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt
index fe1a619..3b85e5d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt
@@ -52,7 +52,7 @@
         internal const val DONT_ADD_TILE = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED
         internal const val TILE_ALREADY_ADDED =
                 StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED
-        internal const val DISMISSED = 3
+        internal const val DISMISSED = StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED
     }
 
     private val commandQueueCallback = object : CommandQueue.Callbacks {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserDialog.kt
index 2ad06c1..01afa56 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/user/UserDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserDialog.kt
@@ -32,7 +32,7 @@
  */
 class UserDialog(
     context: Context
-) : SystemUIDialog(context, R.style.Theme_SystemUI_Dialog_QSDialog) {
+) : SystemUIDialog(context) {
 
     // create() is no-op after creation
     private lateinit var _doneButton: View
@@ -72,7 +72,7 @@
             attributes.fitInsetsTypes = attributes.fitInsetsTypes or WindowInsets.Type.statusBars()
             attributes.receiveInsetsIgnoringZOrder = true
             setLayout(
-                    context.resources.getDimensionPixelSize(R.dimen.qs_panel_width),
+                    context.resources.getDimensionPixelSize(R.dimen.notification_panel_width),
                     ViewGroup.LayoutParams.WRAP_CONTENT
             )
             setGravity(Gravity.CENTER)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
index a5e4ba1..bae7996 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt
@@ -21,6 +21,7 @@
 import android.provider.Settings
 import android.view.View
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
@@ -36,6 +37,7 @@
     private val userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
     private val activityStarter: ActivityStarter,
     private val falsingManager: FalsingManager,
+    private val dialogLaunchAnimator: DialogLaunchAnimator,
     private val dialogFactory: (Context) -> UserDialog
 ) {
 
@@ -43,11 +45,13 @@
     constructor(
         userDetailViewAdapterProvider: Provider<UserDetailView.Adapter>,
         activityStarter: ActivityStarter,
-        falsingManager: FalsingManager
+        falsingManager: FalsingManager,
+        dialogLaunchAnimator: DialogLaunchAnimator
     ) : this(
         userDetailViewAdapterProvider,
         activityStarter,
         falsingManager,
+        dialogLaunchAnimator,
         { UserDialog(it) }
     )
 
@@ -69,7 +73,11 @@
 
             settingsButton.setOnClickListener {
                 if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                    activityStarter.postStartActivityDismissingKeyguard(USER_SETTINGS_INTENT, 0)
+                    dialogLaunchAnimator.disableAllCurrentDialogsExitAnimations()
+                    activityStarter.postStartActivityDismissingKeyguard(
+                        USER_SETTINGS_INTENT,
+                        0
+                    )
                 }
                 dismiss()
             }
@@ -81,7 +89,7 @@
             }
             adapter.linkToViewGroup(grid)
 
-            show()
+            dialogLaunchAnimator.showFromView(this, view)
         }
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index c76f01b..721a6af 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -248,6 +248,12 @@
                     onTaskbarStatusUpdated(visible, stashed));
         }
 
+        @Override
+        public void notifyTaskbarAutohideSuspend(boolean suspend) {
+            verifyCallerAndClearCallingIdentityPostMain("notifyTaskbarAutohideSuspend", () ->
+                    onTaskbarAutohideSuspend(suspend));
+        }
+
         private boolean sendEvent(int action, int code) {
             long when = SystemClock.uptimeMillis();
             final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */,
@@ -818,6 +824,12 @@
         }
     }
 
+    private void onTaskbarAutohideSuspend(boolean suspend) {
+        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
+            mConnectionCallbacks.get(i).onTaskbarAutohideSuspend(suspend);
+        }
+    }
+
     private void notifyConnectionChanged() {
         for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
             mConnectionCallbacks.get(i).onConnectionChanged(mOverviewProxy != null);
@@ -1000,6 +1012,7 @@
         default void onNavBarButtonAlphaChanged(float alpha, boolean animate) {}
         default void onHomeRotationEnabled(boolean enabled) {}
         default void onTaskbarStatusUpdated(boolean visible, boolean stashed) {}
+        default void onTaskbarAutohideSuspend(boolean suspend) {}
         default void onSystemUiStateChanged(int sysuiStateFlags) {}
         default void onAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {}
         default void onAssistantGestureCompletion(float velocity) {}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 7d26857..a0118ab 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -422,11 +422,6 @@
         } else {
             mScreenshotView.animateDismissal();
         }
-
-        if (mLastScrollCaptureResponse != null) {
-            mLastScrollCaptureResponse.close();
-            mLastScrollCaptureResponse = null;
-        }
     }
 
     boolean isPendingSharedTransition() {
@@ -456,6 +451,9 @@
         mScreenshotView.init(mUiEventLogger, new ScreenshotView.ScreenshotViewCallback() {
             @Override
             public void onUserInteraction() {
+                if (DEBUG_INPUT) {
+                    Log.d(TAG, "onUserInteraction");
+                }
                 resetTimeout();
             }
 
@@ -670,52 +668,7 @@
                 mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
                         mScreenshotTakenInPortrait);
                 // delay starting scroll capture to make sure the scrim is up before the app moves
-                mScreenshotView.post(() -> {
-                    // Clear the reference to prevent close() in dismissScreenshot
-                    mLastScrollCaptureResponse = null;
-                    final ListenableFuture<ScrollCaptureController.LongScreenshot> future =
-                            mScrollCaptureController.run(response);
-                    future.addListener(() -> {
-                        ScrollCaptureController.LongScreenshot longScreenshot;
-
-                        try {
-                            longScreenshot = future.get();
-                        } catch (CancellationException
-                                | InterruptedException
-                                | ExecutionException e) {
-                            Log.e(TAG, "Exception", e);
-                            mScreenshotView.restoreNonScrollingUi();
-                            return;
-                        }
-
-                        if (longScreenshot.getHeight() == 0) {
-                            mScreenshotView.restoreNonScrollingUi();
-                            return;
-                        }
-
-                        mLongScreenshotHolder.setLongScreenshot(longScreenshot);
-                        mLongScreenshotHolder.setTransitionDestinationCallback(
-                                (transitionDestination, onTransitionEnd) ->
-                                        mScreenshotView.startLongScreenshotTransition(
-                                                transitionDestination, onTransitionEnd,
-                                                longScreenshot));
-
-                        final Intent intent = new Intent(mContext, LongScreenshotActivity.class);
-                        intent.setFlags(
-                                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
-                        mContext.startActivity(intent,
-                                ActivityOptions.makeCustomAnimation(mContext, 0, 0).toBundle());
-                        RemoteAnimationAdapter runner = new RemoteAnimationAdapter(
-                                SCREENSHOT_REMOTE_RUNNER, 0, 0);
-                        try {
-                            WindowManagerGlobal.getWindowManagerService()
-                                    .overridePendingAppTransitionRemote(runner, DEFAULT_DISPLAY);
-                        } catch (Exception e) {
-                            Log.e(TAG, "Error overriding screenshot app transition", e);
-                        }
-                    }, mMainExecutor);
-                });
+                mScreenshotView.post(() -> runBatchScrollCapture(response));
             });
         } catch (CancellationException e) {
             // Ignore
@@ -723,6 +676,57 @@
             Log.e(TAG, "requestScrollCapture failed", e);
         }
     }
+    ListenableFuture<ScrollCaptureController.LongScreenshot> mLongScreenshotFuture;
+
+    private void runBatchScrollCapture(ScrollCaptureResponse response) {
+        // Clear the reference to prevent close() in dismissScreenshot
+        mLastScrollCaptureResponse = null;
+
+        if (mLongScreenshotFuture != null) {
+            mLongScreenshotFuture.cancel(true);
+        }
+        mLongScreenshotFuture = mScrollCaptureController.run(response);
+        mLongScreenshotFuture.addListener(() -> {
+            ScrollCaptureController.LongScreenshot longScreenshot;
+            try {
+                longScreenshot = mLongScreenshotFuture.get();
+            } catch (CancellationException e) {
+                Log.e(TAG, "Long screenshot cancelled");
+                return;
+            } catch (InterruptedException | ExecutionException e) {
+                Log.e(TAG, "Exception", e);
+                mScreenshotView.restoreNonScrollingUi();
+                return;
+            }
+
+            if (longScreenshot.getHeight() == 0) {
+                mScreenshotView.restoreNonScrollingUi();
+                return;
+            }
+
+            mLongScreenshotHolder.setLongScreenshot(longScreenshot);
+            mLongScreenshotHolder.setTransitionDestinationCallback(
+                    (transitionDestination, onTransitionEnd) ->
+                            mScreenshotView.startLongScreenshotTransition(
+                                    transitionDestination, onTransitionEnd,
+                                    longScreenshot));
+
+            final Intent intent = new Intent(mContext, LongScreenshotActivity.class);
+            intent.setFlags(
+                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+            mContext.startActivity(intent,
+                    ActivityOptions.makeCustomAnimation(mContext, 0, 0).toBundle());
+            RemoteAnimationAdapter runner = new RemoteAnimationAdapter(
+                    SCREENSHOT_REMOTE_RUNNER, 0, 0);
+            try {
+                WindowManagerGlobal.getWindowManagerService()
+                        .overridePendingAppTransitionRemote(runner, DEFAULT_DISPLAY);
+            } catch (Exception e) {
+                Log.e(TAG, "Error overriding screenshot app transition", e);
+            }
+        }, mMainExecutor);
+    }
 
     private void withWindowAttached(Runnable action) {
         View decorView = mWindow.getDecorView();
@@ -824,16 +828,27 @@
 
     /** Reset screenshot view and then call onCompleteRunnable */
     private void finishDismiss() {
-        if (DEBUG_UI) {
+        if (DEBUG_DISMISS) {
             Log.d(TAG, "finishDismiss");
         }
-        cancelTimeout();
-        removeWindow();
-        mScreenshotView.reset();
+        if (mLastScrollCaptureRequest != null) {
+            mLastScrollCaptureRequest.cancel(true);
+            mLastScrollCaptureRequest = null;
+        }
+        if (mLastScrollCaptureResponse != null) {
+            mLastScrollCaptureResponse.close();
+            mLastScrollCaptureResponse = null;
+        }
+        if (mLongScreenshotFuture != null) {
+            mLongScreenshotFuture.cancel(true);
+        }
         if (mCurrentRequestCallback != null) {
             mCurrentRequestCallback.onFinish();
             mCurrentRequestCallback = null;
         }
+        mScreenshotView.reset();
+        removeWindow();
+        cancelTimeout();
     }
 
     /**
@@ -861,6 +876,9 @@
     }
 
     private void cancelTimeout() {
+        if (DEBUG_DISMISS) {
+            Log.d(TAG, "cancel timeout");
+        }
         mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT);
     }
 
@@ -876,7 +894,7 @@
         mScreenshotHandler.sendMessageDelayed(
                 mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT),
                 timeoutMs);
-        if (DEBUG_UI) {
+        if (DEBUG_DISMISS) {
             Log.d(TAG, "dismiss timeout: " + timeoutMs + " ms");
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index e812397..3314c75 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -1117,9 +1117,11 @@
                 mPreviousX = mStartX;
                 return true;
             } else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
-                if (isPastDismissThreshold()
-                        && (mDismissAnimation == null || !mDismissAnimation.isRunning())) {
-                    if (DEBUG_INPUT) {
+                if (mDismissAnimation != null && mDismissAnimation.isRunning()) {
+                    return true;
+                }
+                if (isPastDismissThreshold()) {
+                    if (DEBUG_DISMISS) {
                         Log.d(TAG, "dismiss triggered via swipe gesture");
                     }
                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED);
@@ -1129,9 +1131,7 @@
                     if (DEBUG_DISMISS) {
                         Log.d(TAG, "swipe gesture abandoned");
                     }
-                    if ((mDismissAnimation == null || !mDismissAnimation.isRunning())) {
-                        createSwipeReturnAnimation().start();
-                    }
+                    createSwipeReturnAnimation().start();
                 }
                 return true;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
index ef7355a..83b60fb 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -35,6 +35,7 @@
 
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 
@@ -72,6 +73,7 @@
     private ListenableFuture<CaptureResult> mTileFuture;
     private ListenableFuture<Void> mEndFuture;
     private String mWindowOwner;
+    private volatile boolean mCancelled;
 
     static class LongScreenshot {
         private final ImageTileSet mImageTileSet;
@@ -159,9 +161,11 @@
      * @return a future ImageTile set containing the result
      */
     ListenableFuture<LongScreenshot> run(ScrollCaptureResponse response) {
+        mCancelled = false;
         return CallbackToFutureAdapter.getFuture(completer -> {
             mCaptureCompleter = completer;
             mWindowOwner = response.getPackageName();
+            mCaptureCompleter.addCancellationListener(this::onCancelled, mBgExecutor);
             mBgExecutor.execute(() -> {
                 float maxPages = Settings.Secure.getFloat(mContext.getContentResolver(),
                         SETTING_KEY_MAX_PAGES, MAX_PAGES_DEFAULT);
@@ -172,6 +176,24 @@
         });
     }
 
+    /**
+     * The ListenableFuture for the long screenshot was cancelled. Be sure to cancel all downstream
+     * futures that might be pending.
+     */
+    private void onCancelled() {
+        mCancelled = true;
+        if (mSessionFuture != null) {
+            mSessionFuture.cancel(true);
+        }
+        if (mTileFuture != null) {
+            mTileFuture.cancel(true);
+        }
+        if (mSession != null) {
+            mSession.end();
+        }
+        mEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_FAILURE, 0, mWindowOwner);
+    }
+
     private void onStartComplete() {
         try {
             mSession = mSessionFuture.get();
@@ -192,18 +214,21 @@
         if (LogConfig.DEBUG_SCROLL) {
             Log.d(TAG, "requestNextTile: " + topPx);
         }
+        if (mCancelled) {
+            Log.d(TAG, "requestNextTile: CANCELLED");
+            return;
+        }
         mTileFuture = mSession.requestTile(topPx);
         mTileFuture.addListener(() -> {
             try {
-                if (LogConfig.DEBUG_SCROLL) {
-                    Log.d(TAG, "onCaptureResult");
-                }
                 onCaptureResult(mTileFuture.get());
+            } catch (CancellationException e) {
+                Log.e(TAG, "requestTile cancelled");
             } catch (InterruptedException | ExecutionException e) {
                 Log.e(TAG, "requestTile failed!", e);
                 mCaptureCompleter.setException(e);
             }
-        }, mContext.getMainExecutor());
+        }, mBgExecutor);
     }
 
     private void onCaptureResult(CaptureResult result) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 8a39719..74ebfe5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -147,7 +147,6 @@
     private boolean mBatteryPresent = true;
     private long mChargingTimeRemaining;
     private String mMessageToShowOnScreenOn;
-    protected int mLockScreenMode;
     private boolean mInited;
 
     private KeyguardUpdateMonitorCallback mUpdateMonitorCallback;
@@ -600,10 +599,6 @@
         mHideTransientMessageOnScreenOff = hideOnScreenOff && transientIndication != null;
         mHandler.removeMessages(MSG_HIDE_TRANSIENT);
         mHandler.removeMessages(MSG_SHOW_ACTION_TO_UNLOCK);
-        if (mDozing && !TextUtils.isEmpty(mTransientIndication)) {
-            // Make sure this doesn't get stuck and burns in. Acquire wakelock until its cleared.
-            mWakeLock.setAcquired(true);
-        }
         hideTransientIndicationDelayed(BaseKeyguardCallback.HIDE_DELAY_MS);
 
         updateIndication(false);
@@ -623,10 +618,6 @@
     }
 
     protected final void updateIndication(boolean animate) {
-        if (TextUtils.isEmpty(mTransientIndication)) {
-            mWakeLock.setAcquired(false);
-        }
-
         if (!mVisible) {
             return;
         }
@@ -644,24 +635,31 @@
             // colors can be hard to read in low brightness.
             mTopIndicationView.setTextColor(Color.WHITE);
             if (!TextUtils.isEmpty(mTransientIndication)) {
-                mTopIndicationView.switchIndication(mTransientIndication, null);
+                mWakeLock.setAcquired(true);
+                mTopIndicationView.switchIndication(mTransientIndication, null,
+                        true, () -> mWakeLock.setAcquired(false));
             } else if (!mBatteryPresent) {
                 // If there is no battery detected, hide the indication and bail
                 mIndicationArea.setVisibility(GONE);
             } else if (!TextUtils.isEmpty(mAlignmentIndication)) {
-                mTopIndicationView.switchIndication(mAlignmentIndication, null);
+                mTopIndicationView.switchIndication(mAlignmentIndication, null,
+                        false /* animate */, null /* onAnimationEndCallback */);
                 mTopIndicationView.setTextColor(mContext.getColor(R.color.misalignment_text_color));
             } else if (mPowerPluggedIn || mEnableBatteryDefender) {
                 String indication = computePowerIndication();
                 if (animate) {
-                    animateText(mTopIndicationView, indication);
+                    mWakeLock.setAcquired(true);
+                    mTopIndicationView.switchIndication(indication, null, true /* animate */,
+                            () -> mWakeLock.setAcquired(false));
                 } else {
-                    mTopIndicationView.switchIndication(indication, null);
+                    mTopIndicationView.switchIndication(indication, null, false /* animate */,
+                            null /* onAnimationEndCallback */);
                 }
             } else {
                 String percentage = NumberFormat.getPercentInstance()
                         .format(mBatteryLevel / 100f);
-                mTopIndicationView.switchIndication(percentage, null);
+                mTopIndicationView.switchIndication(percentage, null /* indication */,
+                        false /* animate */, null /* onAnimationEnd*/);
             }
             return;
         }
@@ -819,12 +817,15 @@
         }
     }
 
-    private void showTryFingerprintMsg(String a11yString) {
+    private void showTryFingerprintMsg(int msgId, String a11yString) {
         if (mKeyguardUpdateMonitor.isUdfpsAvailable()) {
             // if udfps available, there will always be a tappable affordance to unlock
             // For example, the lock icon
             if (mKeyguardBypassController.getUserHasDeviceEntryIntent()) {
                 showTransientIndication(R.string.keyguard_unlock_press);
+            } else if (msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT) {
+                // since face is locked out, simply show "try fingerprint"
+                showTransientIndication(R.string.keyguard_try_fingerprint);
             } else {
                 showTransientIndication(R.string.keyguard_face_failed_use_fp);
             }
@@ -860,11 +861,6 @@
         public static final int HIDE_DELAY_MS = 5000;
 
         @Override
-        public void onLockScreenModeChanged(int mode) {
-            mLockScreenMode = mode;
-        }
-
-        @Override
         public void onRefreshBatteryInfo(BatteryStatus status) {
             boolean isChargingOrFull = status.status == BatteryManager.BATTERY_STATUS_CHARGING
                     || status.status == BatteryManager.BATTERY_STATUS_FULL;
@@ -916,7 +912,7 @@
             } else if (mKeyguardUpdateMonitor.isScreenOn()) {
                 if (biometricSourceType == BiometricSourceType.FACE
                         && shouldSuppressFaceMsgAndShowTryFingerprintMsg()) {
-                    showTryFingerprintMsg(helpString);
+                    showTryFingerprintMsg(msgId, helpString);
                     return;
                 }
                 showTransientIndication(helpString, false /* isError */, showActionToUnlock);
@@ -936,7 +932,7 @@
                     && shouldSuppressFaceMsgAndShowTryFingerprintMsg()
                     && !mStatusBarKeyguardViewManager.isBouncerShowing()
                     && mKeyguardUpdateMonitor.isScreenOn()) {
-                showTryFingerprintMsg(errString);
+                showTryFingerprintMsg(msgId, errString);
                 return;
             }
             if (msgId == FaceManager.FACE_ERROR_TIMEOUT) {
@@ -945,7 +941,7 @@
                 if (!mStatusBarKeyguardViewManager.isBouncerShowing()
                         && mKeyguardUpdateMonitor.isUdfpsEnrolled()
                         && mKeyguardUpdateMonitor.isFingerprintDetectionRunning()) {
-                    showTryFingerprintMsg(errString);
+                    showTryFingerprintMsg(msgId, errString);
                 } else if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) {
                     mStatusBarKeyguardViewManager.showBouncerMessage(
                             mContext.getResources().getString(R.string.keyguard_unlock_press),
@@ -989,8 +985,8 @@
         private boolean shouldSuppressFaceMsgAndShowTryFingerprintMsg() {
             // For dual biometric, don't show face auth messages
             return mKeyguardUpdateMonitor.isFingerprintDetectionRunning()
-                && mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
-                    true /* isStrongBiometric */);
+                    && mKeyguardUpdateMonitor.isUnlockingWithBiometricAllowed(
+                            true /* isStrongBiometric */);
         }
 
         private boolean shouldSuppressFaceError(int msgId, KeyguardUpdateMonitor updateMonitor) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 03d8e7e..77e329f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -208,6 +208,11 @@
     lateinit var isScrimOpaqueChangedListener: Consumer<Boolean>
 
     /**
+     * A runnable to call when the scrim has been fully revealed. This is only invoked once
+     */
+    var fullyRevealedRunnable: Runnable? = null
+
+    /**
      * How much of the underlying views are revealed, in percent. 0 means they will be completely
      * obscured and 1 means they'll be fully visible.
      */
@@ -218,10 +223,20 @@
 
                 revealEffect.setRevealAmountOnScrim(value, this)
                 updateScrimOpaque()
+                maybeTriggerFullyRevealedRunnable()
                 invalidate()
             }
         }
 
+    private fun maybeTriggerFullyRevealedRunnable() {
+        if (revealAmount == 1.0f) {
+            fullyRevealedRunnable?.let {
+                it.run()
+                fullyRevealedRunnable = null
+            }
+        }
+    }
+
     /**
      * The [LightRevealEffect] used to manipulate the radial gradient whenever [revealAmount]
      * changes.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index ca18b07..dca7f70 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -298,9 +298,8 @@
                     nsslController.setTransitionToFullShadeAmount(field)
                     notificationPanelController.setTransitionToFullShadeAmount(field,
                             false /* animate */, 0 /* delay */)
-                    // TODO: appear qs also in split shade
-                    val qsAmount = if (useSplitShade) 0f else field
-                    qS.setTransitionToFullShadeAmount(qsAmount, false /* animate */)
+                    val progress = MathUtils.saturate(dragDownAmount / scrimTransitionDistance)
+                    qS.setTransitionToFullShadeAmount(field, progress)
                     // TODO: appear media also in split shade
                     val mediaAmount = if (useSplitShade) 0f else field
                     mediaHierarchyManager.setTransitionToFullShadeAmount(mediaAmount)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index 7aa2dc7..d4f54e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -36,7 +36,6 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
-import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.PanelExpansionListener
 import com.android.systemui.statusbar.phone.ScrimController
@@ -73,6 +72,10 @@
         private const val TAG = "DepthController"
     }
 
+    /**
+     * Did we already unblur while dozing?
+     */
+    private var alreadyUnblurredWhileDozing = false
     lateinit var root: View
     private var blurRoot: View? = null
     private var keyguardAnimator: Animator? = null
@@ -229,9 +232,11 @@
     private val keyguardStateCallback = object : KeyguardStateController.Callback {
         override fun onKeyguardFadingAwayChanged() {
             if (!keyguardStateController.isKeyguardFadingAway ||
-                    biometricUnlockController.mode != MODE_WAKE_AND_UNLOCK) {
+                    !biometricUnlockController.isWakeAndUnlock) {
                 return
             }
+            // When wakeAndUnlocking the screen remains dozing, so we have to manually trigger
+            // the unblur earlier
 
             keyguardAnimator?.cancel()
             keyguardAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
@@ -253,6 +258,7 @@
                 })
                 start()
             }
+            alreadyUnblurredWhileDozing = statusBarStateController.dozeAmount != 0.0f
         }
 
         override fun onKeyguardShowingChanged() {
@@ -274,10 +280,24 @@
             if (isDozing) {
                 shadeAnimation.finishIfRunning()
                 brightnessMirrorSpring.finishIfRunning()
+
+                // unset this for safety, to be ready for the next wakeup
+                alreadyUnblurredWhileDozing = false
             }
         }
 
         override fun onDozeAmountChanged(linear: Float, eased: Float) {
+            if (alreadyUnblurredWhileDozing) {
+                if (linear == 0.0f) {
+                    // We finished waking up, let's reset
+                    alreadyUnblurredWhileDozing = false
+                } else {
+                    // We've already handled the unbluring from the keyguardAnimator above.
+                    // if we would continue, we'd play another unzoom / blur animation from the
+                    // dozing changing.
+                    return
+                }
+            }
             wakeAndUnlockBlurRadius = blurUtils.blurRadiusOfRatio(eased)
             scheduleUpdate()
         }
@@ -435,6 +455,7 @@
             it.println("blursDisabledForAppLaunch: $blursDisabledForAppLaunch")
             it.println("qsPanelExpansion: $qsPanelExpansion")
             it.println("transitionToFullShadeProgress: $transitionToFullShadeProgress")
+            it.println("alreadyUnblurredWhileDozing: $alreadyUnblurredWhileDozing")
             it.println("lastAppliedBlur: $lastAppliedBlur")
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 19876ba..6da981b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -28,6 +28,7 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.os.SystemProperties;
+import android.os.Trace;
 import android.text.format.DateFormat;
 import android.util.FloatProperty;
 import android.util.Log;
@@ -181,6 +182,7 @@
         }
 
         synchronized (mListeners) {
+            Trace.beginSection(TAG + "#setState(" + StatusBarState.toShortString(state) + ")");
             String tag = getClass().getSimpleName() + "#setState(" + state + ")";
             DejankUtils.startDetectingBlockingIpcs(tag);
             for (RankedListener rl : new ArrayList<>(mListeners)) {
@@ -198,6 +200,7 @@
                 rl.mListener.onStatePostChange();
             }
             DejankUtils.stopDetectingBlockingIpcs(tag);
+            Trace.endSection();
         }
 
         return true;
@@ -262,12 +265,14 @@
         mIsDozing = isDozing;
 
         synchronized (mListeners) {
+            Trace.beginSection(TAG + "#setDozing(" + isDozing + ")");
             String tag = getClass().getSimpleName() + "#setIsDozing";
             DejankUtils.startDetectingBlockingIpcs(tag);
             for (RankedListener rl : new ArrayList<>(mListeners)) {
                 rl.mListener.onDozingChanged(isDozing);
             }
             DejankUtils.stopDetectingBlockingIpcs(tag);
+            Trace.endSection();
         }
 
         return true;
@@ -333,12 +338,14 @@
         mDozeAmount = dozeAmount;
         float interpolatedAmount = mDozeInterpolator.getInterpolation(dozeAmount);
         synchronized (mListeners) {
+            Trace.beginSection(TAG + "#setDozeAmount");
             String tag = getClass().getSimpleName() + "#setDozeAmount";
             DejankUtils.startDetectingBlockingIpcs(tag);
             for (RankedListener rl : new ArrayList<>(mListeners)) {
                 rl.mListener.onDozeAmountChanged(mDozeAmount, interpolatedAmount);
             }
             DejankUtils.stopDetectingBlockingIpcs(tag);
+            Trace.endSection();
         }
     }
 
@@ -446,7 +453,7 @@
             mIsFullscreen = isFullscreen;
             synchronized (mListeners) {
                 for (RankedListener rl : new ArrayList<>(mListeners)) {
-                    rl.mListener.onFullscreenStateChanged(isFullscreen, true /* isImmersive */);
+                    rl.mListener.onFullscreenStateChanged(isFullscreen);
                 }
             }
         }
@@ -469,11 +476,13 @@
     public void setPulsing(boolean pulsing) {
         if (mPulsing != pulsing) {
             mPulsing = pulsing;
+            Trace.beginSection(TAG + "#setPulsing(" + pulsing + ")");
             synchronized (mListeners) {
                 for (RankedListener rl : new ArrayList<>(mListeners)) {
                     rl.mListener.onPulsingChanged(pulsing);
                 }
             }
+            Trace.endSection();
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
index d74297e..04c60fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
@@ -114,9 +114,6 @@
             override fun onThemeChanged() {
                 updateRippleColor()
             }
-            override fun onOverlayChanged() {
-                updateRippleColor()
-            }
 
             override fun onConfigChanged(newConfig: Configuration?) {
                 normalizedPortPosX = context.resources.getFloat(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index 6730afa..d297d95 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -47,6 +47,7 @@
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.commandline.CommandRegistry;
+import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler;
 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
 import com.android.systemui.statusbar.notification.DynamicChildBindController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
@@ -65,6 +66,7 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl;
 import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
+import com.android.systemui.statusbar.phone.StatusBarWindowController;
 import com.android.systemui.statusbar.phone.SystemUIHostDialogProvider;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger;
@@ -253,11 +255,31 @@
             @Main Executor mainExecutor,
             IActivityManager iActivityManager,
             OngoingCallLogger logger,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            StatusBarWindowController statusBarWindowController,
+            SwipeStatusBarAwayGestureHandler swipeStatusBarAwayGestureHandler,
+            StatusBarStateController statusBarStateController) {
+        Optional<StatusBarWindowController> windowController =
+                featureFlags.isOngoingCallInImmersiveEnabled()
+                        ? Optional.of(statusBarWindowController)
+                        : Optional.empty();
+        Optional<SwipeStatusBarAwayGestureHandler> gestureHandler =
+                featureFlags.isOngoingCallInImmersiveEnabled()
+                        ? Optional.of(swipeStatusBarAwayGestureHandler)
+                        : Optional.empty();
         OngoingCallController ongoingCallController =
                 new OngoingCallController(
-                        notifCollection, featureFlags, systemClock, activityStarter, mainExecutor,
-                        iActivityManager, logger, dumpManager);
+                        notifCollection,
+                        featureFlags,
+                        systemClock,
+                        activityStarter,
+                        mainExecutor,
+                        iActivityManager,
+                        logger,
+                        dumpManager,
+                        windowController,
+                        gestureHandler,
+                        statusBarStateController);
         ongoingCallController.init();
         return ongoingCallController;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
new file mode 100644
index 0000000..80577ee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandler.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.gesture
+
+import android.content.Context
+import android.os.Looper
+import android.view.Choreographer
+import android.view.Display
+import android.view.InputEvent
+import android.view.MotionEvent
+import android.view.MotionEvent.*
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shared.system.InputChannelCompat
+import com.android.systemui.shared.system.InputMonitorCompat
+import com.android.systemui.statusbar.phone.StatusBarWindowController
+import javax.inject.Inject
+
+/**
+ * A class to detect when a user swipes away the status bar. To be notified when the swipe away
+ * gesture is detected, add a callback via [addOnGestureDetectedCallback].
+ */
+@SysUISingleton
+open class SwipeStatusBarAwayGestureHandler @Inject constructor(
+    context: Context,
+    private val statusBarWindowController: StatusBarWindowController,
+    private val logger: SwipeStatusBarAwayGestureLogger
+) {
+
+    /**
+     * Active callbacks, each associated with a tag. Gestures will only be monitored if
+     * [callbacks.size] > 0.
+     */
+    private val callbacks: MutableMap<String, () -> Unit> = mutableMapOf()
+
+    private var startY: Float = 0f
+    private var startTime: Long = 0L
+    private var monitoringCurrentTouch: Boolean = false
+
+    private var inputMonitor: InputMonitorCompat? = null
+    private var inputReceiver: InputChannelCompat.InputEventReceiver? = null
+
+    private var swipeDistanceThreshold: Int = context.resources.getDimensionPixelSize(
+        com.android.internal.R.dimen.system_gestures_start_threshold
+    )
+
+    /** Adds a callback that will be triggered when the swipe away gesture is detected. */
+    fun addOnGestureDetectedCallback(tag: String, callback: () -> Unit) {
+        val callbacksWasEmpty = callbacks.isEmpty()
+        callbacks[tag] = callback
+        if (callbacksWasEmpty) {
+            startGestureListening()
+        }
+    }
+
+    /** Removes the callback. */
+    fun removeOnGestureDetectedCallback(tag: String) {
+        callbacks.remove(tag)
+        if (callbacks.isEmpty()) {
+             stopGestureListening()
+        }
+    }
+
+    private fun onInputEvent(ev: InputEvent) {
+        if (ev !is MotionEvent) {
+            return
+        }
+
+        when (ev.actionMasked) {
+            ACTION_DOWN -> {
+                if (
+                    // Gesture starts just below the status bar
+                    ev.y >= statusBarWindowController.statusBarHeight
+                    && ev.y <= 3 * statusBarWindowController.statusBarHeight
+                ) {
+                    logger.logGestureDetectionStarted(ev.y.toInt())
+                    startY = ev.y
+                    startTime = ev.eventTime
+                    monitoringCurrentTouch = true
+                } else {
+                    monitoringCurrentTouch = false
+                }
+            }
+            ACTION_MOVE -> {
+                if (!monitoringCurrentTouch) {
+                    return
+                }
+                if (
+                    // Gesture is up
+                    ev.y < startY
+                    // Gesture went far enough
+                    && (startY - ev.y) >= swipeDistanceThreshold
+                    // Gesture completed quickly enough
+                    && (ev.eventTime - startTime) < SWIPE_TIMEOUT_MS
+                ) {
+                    monitoringCurrentTouch = false
+                    logger.logGestureDetected(ev.y.toInt())
+                    callbacks.values.forEach { it.invoke() }
+                }
+            }
+            ACTION_CANCEL, ACTION_UP -> {
+                if (monitoringCurrentTouch) {
+                    logger.logGestureDetectionEndedWithoutTriggering(ev.y.toInt())
+                }
+                monitoringCurrentTouch = false
+            }
+        }
+    }
+
+    /** Start listening for the swipe gesture. */
+    private fun startGestureListening() {
+        stopGestureListening()
+
+        logger.logInputListeningStarted()
+        inputMonitor = InputMonitorCompat(TAG, Display.DEFAULT_DISPLAY).also {
+            inputReceiver = it.getInputReceiver(
+                Looper.getMainLooper(),
+                Choreographer.getInstance(),
+                this::onInputEvent
+            )
+        }
+    }
+
+    /** Stop listening for the swipe gesture. */
+    private fun stopGestureListening() {
+        inputMonitor?.let {
+            logger.logInputListeningStopped()
+            inputMonitor = null
+            it.dispose()
+        }
+        inputReceiver?.let {
+            inputReceiver = null
+            it.dispose()
+        }
+    }
+}
+
+private const val SWIPE_TIMEOUT_MS: Long = 500
+private val TAG = SwipeStatusBarAwayGestureHandler::class.simpleName
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
new file mode 100644
index 0000000..17feaa8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureLogger.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.gesture
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.dagger.SwipeStatusBarAwayLog
+import javax.inject.Inject
+
+/** Log messages for [SwipeStatusBarAwayGestureHandler]. */
+class SwipeStatusBarAwayGestureLogger @Inject constructor(
+    @SwipeStatusBarAwayLog private val buffer: LogBuffer
+) {
+    fun logGestureDetectionStarted(y: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { int1 = y },
+            { "Beginning gesture detection. y=$int1" }
+        )
+    }
+
+    fun logGestureDetectionEndedWithoutTriggering(y: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { int1 = y },
+            { "Gesture finished; no swipe up gesture detected. Final y=$int1" }
+        )
+    }
+
+    fun logGestureDetected(y: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            { int1 = y },
+            { "Gesture detected; notifying callbacks. y=$int1" }
+        )
+    }
+
+    fun logInputListeningStarted() {
+        buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening started "})
+    }
+
+    fun logInputListeningStopped() {
+        buffer.log(TAG, LogLevel.VERBOSE, {}, { "Input listening stopped "})
+    }
+}
+
+private const val TAG = "SwipeStatusBarAwayGestureHandler"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 83c98d5..26f6460 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -30,6 +30,7 @@
 import android.os.Handler
 import android.os.UserHandle
 import android.provider.Settings
+import android.util.Log
 import android.view.View
 import android.view.ViewGroup
 import com.android.settingslib.Utils
@@ -74,6 +75,10 @@
     @Main private val handler: Handler,
     optionalPlugin: Optional<BcSmartspaceDataPlugin>
 ) {
+    companion object {
+        private const val TAG = "LockscreenSmartspaceController"
+    }
+
     private var session: SmartspaceSession? = null
     private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null)
 
@@ -211,6 +216,7 @@
 
         val newSession = smartspaceManager.createSmartspaceSession(
                 SmartspaceConfig.Builder(context, "lockscreen").build())
+        Log.d(TAG, "Starting smartspace session for lockscreen")
         newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener)
         this.session = newSession
 
@@ -232,6 +238,8 @@
      * Disconnects the smartspace view from the smartspace service and cleans up any resources.
      */
     fun disconnect() {
+        if (!smartspaceViews.isEmpty()) return
+
         execution.assertIsMainThread()
 
         if (session == null) {
@@ -249,6 +257,7 @@
         session = null
 
         plugin?.onTargetsAvailable(emptyList())
+        Log.d(TAG, "Ending smartspace session for lockscreen")
     }
 
     fun addListener(listener: SmartspaceTargetListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 216115e..09ab90e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -272,15 +272,6 @@
         }
 
         @Override
-        public void onOverlayChanged() {
-            updateShowEmptyShadeView();
-            mView.updateCornerRadius();
-            mView.updateBgColor();
-            mView.updateDecorViews();
-            mView.reinflateViews();
-        }
-
-        @Override
         public void onUiModeChanged() {
             mView.updateBgColor();
             mView.updateDecorViews();
@@ -288,6 +279,11 @@
 
         @Override
         public void onThemeChanged() {
+            updateShowEmptyShadeView();
+            mView.updateCornerRadius();
+            mView.updateBgColor();
+            mView.updateDecorViews();
+            mView.reinflateViews();
             updateFooter();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
index aeb2efd..111cbbe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoHideController.java
@@ -37,6 +37,7 @@
     private final Handler mHandler;
 
     private AutoHideUiElement mStatusBar;
+    /** For tablets, this will represent the Taskbar */
     private AutoHideUiElement mNavigationBar;
     private int mDisplayId;
 
@@ -89,7 +90,7 @@
         }
     }
 
-    void resumeSuspendedAutoHide() {
+    public void resumeSuspendedAutoHide() {
         if (mAutoHideSuspended) {
             scheduleAutoHide();
             Runnable checkBarModesRunnable = getCheckBarModesRunnable();
@@ -99,7 +100,7 @@
         }
     }
 
-    void suspendAutoHide() {
+    public void suspendAutoHide() {
         mHandler.removeCallbacks(mAutoHide);
         Runnable checkBarModesRunnable = getCheckBarModesRunnable();
         if (checkBarModesRunnable != null) {
@@ -171,4 +172,23 @@
 
         return false;
     }
+
+    /**
+     * Injectable factory for creating a {@link AutoHideController}.
+     */
+    public static class Factory {
+        private final Handler mHandler;
+        private final IWindowManager mIWindowManager;
+
+        @Inject
+        public Factory(@Main Handler handler, IWindowManager iWindowManager) {
+            mHandler = handler;
+            mIWindowManager = iWindowManager;
+        }
+
+        /** Create an {@link AutoHideController} */
+        public AutoHideController create(Context context) {
+            return new AutoHideController(context, mHandler, mIWindowManager);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
index 12ae3f1..96fa8a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt
@@ -123,7 +123,7 @@
 
         if (lastConfig.updateFrom(newConfig) and ActivityInfo.CONFIG_ASSETS_PATHS != 0) {
             listeners.filterForEach({ this.listeners.contains(it) }) {
-                it.onOverlayChanged()
+                it.onThemeChanged()
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
index 878fbbf..927b4c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
@@ -163,7 +163,6 @@
         mHeadsUpStatusBarView.setOnDrawingRectChangedListener(null);
         mWakeUpCoordinator.removeListener(this);
         mNotificationPanelViewController.removeTrackingHeadsUpListener(mSetTrackingHeadsUp);
-        mNotificationPanelViewController.setVerticalTranslationListener(null);
         mNotificationPanelViewController.setHeadsUpAppearanceController(null);
         mStackScrollerController.removeOnExpandedHeightChangedListener(mSetExpandedHeight);
         mDarkIconDispatcher.removeDarkReceiver(this);
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 4b545eb..5f402d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -121,7 +121,7 @@
             }
 
             @Override
-            public void onOverlayChanged() {
+            public void onThemeChanged() {
                 updateResources();
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index f1d5e02..7abe9bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -44,6 +44,7 @@
 import com.android.systemui.keyguard.DismissCallbackRegistry;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.ListenerSet;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -83,11 +84,11 @@
     private final Runnable mRemoveViewRunnable = this::removeView;
     private final KeyguardBypassController mKeyguardBypassController;
     private KeyguardHostViewController mKeyguardViewController;
-    private final List<KeyguardResetCallback> mResetCallbacks = new ArrayList<>();
+    private final ListenerSet<KeyguardResetCallback> mResetCallbacks = new ListenerSet<>();
     private final Runnable mResetRunnable = ()-> {
         if (mKeyguardViewController != null) {
             mKeyguardViewController.resetSecurityContainer();
-            for (KeyguardResetCallback callback : new ArrayList<>(mResetCallbacks)) {
+            for (KeyguardResetCallback callback : mResetCallbacks) {
                 callback.onKeyguardReset();
             }
         }
@@ -126,6 +127,19 @@
         mExpansionCallbacks.add(expansionCallback);
     }
 
+    /**
+     * Enable/disable only the back button
+     */
+    public void setBackButtonEnabled(boolean enabled) {
+        int vis = mContainer.getSystemUiVisibility();
+        if (enabled) {
+            vis &= ~View.STATUS_BAR_DISABLE_BACK;
+        } else {
+            vis |= View.STATUS_BAR_DISABLE_BACK;
+        }
+        mContainer.setSystemUiVisibility(vis);
+    }
+
     public void show(boolean resetSecuritySelection) {
         show(resetSecuritySelection, true /* scrimmed */);
     }
@@ -589,7 +603,7 @@
     }
 
     public void addKeyguardResetCallback(KeyguardResetCallback callback) {
-        mResetCallbacks.add(callback);
+        mResetCallbacks.addIfAbsent(callback);
     }
 
     public void removeKeyguardResetCallback(KeyguardResetCallback callback) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
index a5b5f1c..3a68b9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
@@ -95,44 +95,85 @@
     }
 
     /**
-     * Changes the text with an animation and makes sure a single indication is shown long enough.
+     * Changes the text with an animation. Makes sure a single indication is shown long enough.
+     */
+    public void switchIndication(CharSequence text, KeyguardIndication indication) {
+        switchIndication(text, indication, true, null);
+    }
+
+    /**
+     * Changes the text with an optional animation. For animating text, makes sure a single
+     * indication is shown long enough.
      *
      * @param text The text to show.
      * @param indication optional display information for the text
+     * @param animate whether to animate this indication in - we may not want this on AOD
+     * @param onAnimationEndCallback runnable called after this indication is animated in
      */
-    public void switchIndication(CharSequence text, KeyguardIndication indication) {
+    public void switchIndication(CharSequence text, KeyguardIndication indication,
+            boolean animate, Runnable onAnimationEndCallback) {
         if (text == null) text = "";
 
         CharSequence lastPendingMessage = mMessages.peekLast();
         if (TextUtils.equals(lastPendingMessage, text)
                 || (lastPendingMessage == null && TextUtils.equals(text, getText()))) {
+            if (onAnimationEndCallback != null) {
+                onAnimationEndCallback.run();
+            }
             return;
         }
         mMessages.add(text);
         mKeyguardIndicationInfo.add(indication);
 
-        final boolean hasIcon = indication != null && indication.getIcon() != null;
-        final AnimatorSet animSet = new AnimatorSet();
-        final AnimatorSet.Builder animSetBuilder = animSet.play(getOutAnimator());
+        if (animate) {
+            final boolean hasIcon = indication != null && indication.getIcon() != null;
+            final AnimatorSet animator = new AnimatorSet();
+            // Make sure each animation is visible for a minimum amount of time, while not worrying
+            // about fading in blank text
+            long timeInMillis = System.currentTimeMillis();
+            long delay = Math.max(0, mNextAnimationTime - timeInMillis);
+            setNextAnimationTime(timeInMillis + delay + getFadeOutDuration());
+            final long minDurationMillis =
+                    (indication != null && indication.getMinVisibilityMillis() != null)
+                            ? indication.getMinVisibilityMillis()
+                            : MSG_MIN_DURATION_MILLIS_DEFAULT;
+            if (!text.equals("") || hasIcon) {
+                setNextAnimationTime(mNextAnimationTime + minDurationMillis);
+                Animator inAnimator = getInAnimator();
+                inAnimator.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        super.onAnimationEnd(animation);
+                        if (onAnimationEndCallback != null) {
+                            onAnimationEndCallback.run();
+                        }
+                    }
+                });
+                animator.playSequentially(getOutAnimator(), inAnimator);
+            } else {
+                Animator outAnimator = getOutAnimator();
+                outAnimator.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        super.onAnimationEnd(animation);
+                        if (onAnimationEndCallback != null) {
+                            onAnimationEndCallback.run();
+                        }
+                    }
+                });
+                animator.play(outAnimator);
+            }
 
-        // Make sure each animation is visible for a minimum amount of time, while not worrying
-        // about fading in blank text
-        long timeInMillis = System.currentTimeMillis();
-        long delay = Math.max(0, mNextAnimationTime - timeInMillis);
-        setNextAnimationTime(timeInMillis + delay + getFadeOutDuration());
-
-        final long minDurationMillis =
-                (indication != null && indication.getMinVisibilityMillis() != null)
-                    ? indication.getMinVisibilityMillis()
-                    : MSG_MIN_DURATION_MILLIS_DEFAULT;
-
-        if (!text.equals("") || hasIcon) {
-            setNextAnimationTime(mNextAnimationTime + minDurationMillis);
-            animSetBuilder.before(getInAnimator());
+            animator.setStartDelay(delay);
+            animator.start();
+        } else {
+            setAlpha(1f);
+            setTranslationY(0f);
+            setNextIndication();
+            if (onAnimationEndCallback != null) {
+                onAnimationEndCallback.run();
+            }
         }
-
-        animSet.setStartDelay(delay);
-        animSet.start();
     }
 
     private AnimatorSet getOutAnimator() {
@@ -143,29 +184,8 @@
         fadeOut.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animator) {
-                KeyguardIndication info = mKeyguardIndicationInfo.poll();
-                if (info != null) {
-                    // First, update the style.
-                    // If a background is set on the text, we don't want shadow on the text
-                    if (info.getBackground() != null) {
-                        setTextAppearance(sButtonStyleId);
-                    } else {
-                        setTextAppearance(sStyleId);
-                    }
-                    setBackground(info.getBackground());
-                    setTextColor(info.getTextColor());
-                    setOnClickListener(info.getClickListener());
-                    setClickable(info.getClickListener() != null);
-                    final Drawable icon = info.getIcon();
-                    if (icon != null) {
-                        icon.setTint(getCurrentTextColor());
-                        if (icon instanceof AnimatedVectorDrawable) {
-                            ((AnimatedVectorDrawable) icon).start();
-                        }
-                    }
-                    setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
-                }
-                setText(mMessages.poll());
+                super.onAnimationEnd(animator);
+                setNextIndication();
             }
         });
 
@@ -177,6 +197,32 @@
         return animatorSet;
     }
 
+    private void setNextIndication() {
+        KeyguardIndication info = mKeyguardIndicationInfo.poll();
+        if (info != null) {
+            // First, update the style.
+            // If a background is set on the text, we don't want shadow on the text
+            if (info.getBackground() != null) {
+                setTextAppearance(sButtonStyleId);
+            } else {
+                setTextAppearance(sStyleId);
+            }
+            setBackground(info.getBackground());
+            setTextColor(info.getTextColor());
+            setOnClickListener(info.getClickListener());
+            setClickable(info.getClickListener() != null);
+            final Drawable icon = info.getIcon();
+            if (icon != null) {
+                icon.setTint(getCurrentTextColor());
+                if (icon instanceof AnimatedVectorDrawable) {
+                    ((AnimatedVectorDrawable) icon).start();
+                }
+            }
+            setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
+        }
+        setText(mMessages.poll());
+    }
+
     private AnimatorSet getInAnimator() {
         AnimatorSet animatorSet = new AnimatorSet();
         ObjectAnimator fadeIn = ObjectAnimator.ofFloat(this, View.ALPHA, 1f);
@@ -190,6 +236,7 @@
         yTranslate.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationCancel(Animator animation) {
+                super.onAnimationCancel(animation);
                 setTranslationY(0);
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 5feb405..9055081 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -100,13 +100,8 @@
                 }
 
                 @Override
-                public void onOverlayChanged() {
-                    mView.onOverlayChanged();
-                    KeyguardStatusBarViewController.this.onThemeChanged();
-                }
-
-                @Override
                 public void onThemeChanged() {
+                    mView.onOverlayChanged();
                     KeyguardStatusBarViewController.this.onThemeChanged();
                 }
             };
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
index abee7a5..570b0ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 
@@ -101,7 +102,9 @@
             mNavigationMode = mode;
         });
 
-        dumpManager.registerDumpable(getClass().getSimpleName(), this);
+        if (ctx.getDisplayId() == DEFAULT_DISPLAY) {
+            dumpManager.registerDumpable(getClass().getSimpleName(), this);
+        }
     }
 
     public void setNavigationBar(LightBarTransitionsController navigationBar) {
@@ -298,4 +301,33 @@
             pw.println();
         }
     }
+
+    /**
+     * Injectable factory for creating a {@link LightBarController}.
+     */
+    public static class Factory {
+        private final DarkIconDispatcher mDarkIconDispatcher;
+        private final BatteryController mBatteryController;
+        private final NavigationModeController mNavModeController;
+        private final DumpManager mDumpManager;
+
+        @Inject
+        public Factory(
+                DarkIconDispatcher darkIconDispatcher,
+                BatteryController batteryController,
+                NavigationModeController navModeController,
+                DumpManager dumpManager) {
+
+            mDarkIconDispatcher = darkIconDispatcher;
+            mBatteryController = batteryController;
+            mNavModeController = navModeController;
+            mDumpManager = dumpManager;
+        }
+
+        /** Create an {@link LightBarController} */
+        public LightBarController create(Context context) {
+            return new LightBarController(context, mDarkIconDispatcher, mBatteryController,
+                    mNavModeController, mDumpManager);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 2e7f4b8b..52e5754 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -34,6 +34,9 @@
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
 import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
+import static com.android.systemui.statusbar.phone.PanelBar.STATE_CLOSED;
+import static com.android.systemui.statusbar.phone.PanelBar.STATE_OPEN;
+import static com.android.systemui.statusbar.phone.PanelBar.STATE_OPENING;
 
 import static java.lang.Float.isNaN;
 
@@ -57,7 +60,6 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
-import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Bundle;
@@ -80,6 +82,7 @@
 import android.view.ViewStub;
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
+import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
@@ -332,6 +335,7 @@
     private IdleHostViewController mIdleHostViewController;
     private LockIconViewController mLockIconViewController;
     private NotificationsQuickSettingsContainer mNotificationContainerParent;
+    private NotificationsQSContainerController mNotificationsQSContainerController;
     private boolean mAnimateNextPositionUpdate;
     private float mQuickQsOffsetHeight;
     private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
@@ -469,7 +473,6 @@
     private ArrayList<Consumer<ExpandableNotificationRow>>
             mTrackingHeadsUpListeners =
             new ArrayList<>();
-    private Runnable mVerticalTranslationListener;
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
 
     private int mPanelAlpha;
@@ -730,6 +733,7 @@
             ConversationNotificationManager conversationNotificationManager,
             MediaHierarchyManager mediaHierarchyManager,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+            NotificationsQSContainerController notificationsQSContainerController,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
             KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory,
             KeyguardQsUserSwitchComponent.Factory keyguardQsUserSwitchComponentFactory,
@@ -787,6 +791,8 @@
         mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder;
         mMediaHierarchyManager = mediaHierarchyManager;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+        mNotificationsQSContainerController = notificationsQSContainerController;
+        mNotificationsQSContainerController.init();
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
         mGroupManager = groupManager;
         mNotificationIconAreaController = notificationIconAreaController;
@@ -1084,7 +1090,7 @@
                 Utils.shouldUseSplitNotificationShade(mResources);
         mScrimController.setClipsQsScrim(!mShouldUseSplitNotificationShade);
         if (mQs != null) {
-            mQs.setTranslateWhileExpanding(mShouldUseSplitNotificationShade);
+            mQs.setInSplitShade(mShouldUseSplitNotificationShade);
         }
 
         int topMargin = mShouldUseSplitNotificationShade ? mSplitShadeStatusBarHeight :
@@ -1113,7 +1119,7 @@
         constraintSet.setMargin(R.id.notification_stack_scroller, TOP, topMargin);
         constraintSet.setMargin(R.id.qs_frame, TOP, topMargin);
         constraintSet.applyTo(mNotificationContainerParent);
-        mNotificationContainerParent.setSplitShadeEnabled(mShouldUseSplitNotificationShade);
+        mNotificationsQSContainerController.setSplitShadeEnabled(mShouldUseSplitNotificationShade);
 
         updateKeyguardStatusViewAlignment(/* animate= */false);
 
@@ -1940,15 +1946,6 @@
         return !mQsTouchAboveFalsingThreshold;
     }
 
-    /**
-     * Percentage of panel expansion offset, caused by pulling down on a heads-up.
-     */
-    @Override
-    public void setMinFraction(float minFraction) {
-        mMinFraction = minFraction;
-        mDepthController.setPanelPullDownMinFraction(mMinFraction);
-    }
-
     private float computeQsExpansionFraction() {
         if (mQSAnimatingHiddenFromCollapsed) {
             // When hiding QS from collapsed state, the expansion can sometimes temporarily
@@ -2248,7 +2245,7 @@
             requestPanelHeightUpdate();
             mFalsingCollector.setQsExpanded(expanded);
             mStatusBar.setQsExpanded(expanded);
-            mNotificationContainerParent.setQsExpanded(expanded);
+            mNotificationsQSContainerController.setQsExpanded(expanded);
             mPulseExpansionHandler.setQsExpanded(expanded);
             mKeyguardBypassController.setQSExpanded(expanded);
             mStatusBarKeyguardViewManager.setQsExpanded(expanded);
@@ -2346,7 +2343,7 @@
     private void updateQsExpansion() {
         if (mQs == null) return;
         float qsExpansionFraction = computeQsExpansionFraction();
-        mQs.setQsExpansion(qsExpansionFraction, getHeaderTranslation());
+        mQs.setQsExpansion(qsExpansionFraction, getExpandedFraction(), getHeaderTranslation());
         mMediaHierarchyManager.setQsExpansion(qsExpansionFraction);
         int qsPanelBottomY = calculateQsBottomPosition(qsExpansionFraction);
         mScrimController.setQsPosition(qsExpansionFraction, qsPanelBottomY);
@@ -2374,12 +2371,6 @@
         updateQSExpansionEnabledAmbient();
     }
 
-    @Override
-    public void setIsShadeOpening(boolean opening) {
-        mAmbientState.setIsShadeOpening(opening);
-        updateQSExpansionEnabledAmbient();
-    }
-
     private void updateQSExpansionEnabledAmbient() {
         final float scrollRangeToTop = mAmbientState.getTopPadding() - mQuickQsOffsetHeight;
         mQsExpansionEnabledAmbient = mShouldUseSplitNotificationShade
@@ -2855,8 +2846,9 @@
      * @return Whether we should intercept a gesture to open Quick Settings.
      */
     private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
-        if (!isQsExpansionEnabled() || mCollapsedOnDown || (mKeyguardShowing
-                && mKeyguardBypassController.getBypassEnabled())) {
+        if (!isQsExpansionEnabled() || mCollapsedOnDown
+                || (mKeyguardShowing && mKeyguardBypassController.getBypassEnabled())
+                || (mKeyguardShowing && mShouldUseSplitNotificationShade)) {
             return false;
         }
         View header = mKeyguardShowing || mQs == null ? mKeyguardStatusBar : mQs.getHeader();
@@ -3399,7 +3391,6 @@
     @Override
     protected void onClosingFinished() {
         super.onClosingFinished();
-        resetHorizontalPanelPosition();
         setClosingWithAlphaFadeout(false);
         mMediaHierarchyManager.closeGuts();
     }
@@ -3409,47 +3400,6 @@
         mNotificationStackScrollLayoutController.forceNoOverlappingRendering(closing);
     }
 
-    /**
-     * Updates the horizontal position of the panel so it is positioned closer to the touch
-     * responsible for opening the panel.
-     *
-     * @param x the x-coordinate the touch event
-     */
-    protected void updateHorizontalPanelPosition(float x) {
-        if (mNotificationStackScrollLayoutController.getWidth() * 1.75f > mView.getWidth()
-                || mShouldUseSplitNotificationShade) {
-            resetHorizontalPanelPosition();
-            return;
-        }
-        float leftMost = mPositionMinSideMargin
-                + mNotificationStackScrollLayoutController.getWidth() / 2;
-        float
-                rightMost =
-                mView.getWidth() - mPositionMinSideMargin
-                        - mNotificationStackScrollLayoutController.getWidth() / 2;
-        if (Math.abs(x - mView.getWidth() / 2)
-                < mNotificationStackScrollLayoutController.getWidth() / 4) {
-            x = mView.getWidth() / 2;
-        }
-        x = Math.min(rightMost, Math.max(leftMost, x));
-        float
-                center = mNotificationStackScrollLayoutController.getLeft()
-                + mNotificationStackScrollLayoutController.getWidth() / 2;
-        setHorizontalPanelTranslation(x - center);
-    }
-
-    private void resetHorizontalPanelPosition() {
-        setHorizontalPanelTranslation(0f);
-    }
-
-    protected void setHorizontalPanelTranslation(float translation) {
-        mNotificationStackScrollLayoutController.setTranslationX(translation);
-        mQsFrame.setTranslationX(translation);
-        if (mVerticalTranslationListener != null) {
-            mVerticalTranslationListener.run();
-        }
-    }
-
     protected void updateExpandedHeight(float expandedHeight) {
         if (mTracking) {
             mNotificationStackScrollLayoutController
@@ -3491,8 +3441,14 @@
         return mBarState == KEYGUARD;
     }
 
+    /**
+     * Sets the minimum fraction for the panel expansion offset. This may be non-zero in certain
+     * cases, such as if there's a heads-up notification.
+     */
     public void setPanelScrimMinFraction(float minFraction) {
-        mBar.onPanelMinFractionChanged(minFraction);
+        mMinFraction = minFraction;
+        mDepthController.setPanelPullDownMinFraction(mMinFraction);
+        mScrimController.setPanelScrimMinFraction(mMinFraction);
     }
 
     public void clearNotificationEffects() {
@@ -3608,7 +3564,7 @@
             mQs.setExpandClickListener(mOnClickListener);
             mQs.setHeaderClickable(isQsExpansionEnabled());
             mQs.setOverscrolling(mStackScrollerOverscrolling);
-            mQs.setTranslateWhileExpanding(mShouldUseSplitNotificationShade);
+            mQs.setInSplitShade(mShouldUseSplitNotificationShade);
 
             // recompute internal state when qspanel height changes
             mQs.getView().addOnLayoutChangeListener(
@@ -3756,10 +3712,6 @@
         mTrackingHeadsUpListeners.remove(listener);
     }
 
-    public void setVerticalTranslationListener(Runnable verticalTranslationListener) {
-        mVerticalTranslationListener = verticalTranslationListener;
-    }
-
     public void setHeadsUpAppearanceController(
             HeadsUpAppearanceController headsUpAppearanceController) {
         mHeadsUpAppearanceController = headsUpAppearanceController;
@@ -4028,7 +3980,6 @@
                 }
                 if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) {
                     mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
-                    updateHorizontalPanelPosition(event.getX());
                     handled = true;
                 }
 
@@ -4412,12 +4363,7 @@
         @Override
         public void onThemeChanged() {
             if (DEBUG) Log.d(TAG, "onThemeChanged");
-            final int themeResId = mView.getContext().getThemeResId();
-            if (mThemeResId == themeResId) {
-                return;
-            }
-            mThemeResId = themeResId;
-
+            mThemeResId = mView.getContext().getThemeResId();
             reInflateViews();
         }
 
@@ -4431,12 +4377,6 @@
         }
 
         @Override
-        public void onOverlayChanged() {
-            if (DEBUG) Log.d(TAG, "onOverlayChanged");
-            reInflateViews();
-        }
-
-        @Override
         public void onDensityOrFontScaleChanged() {
             if (DEBUG) Log.d(TAG, "onDensityOrFontScaleChanged");
             reInflateViews();
@@ -4544,7 +4484,6 @@
             // The update needs to happen after the headerSlide in above, otherwise the translation
             // would reset
             maybeAnimateBottomAreaAlpha();
-            resetHorizontalPanelPosition();
             updateQsState();
             mSplitShadeHeaderController.setShadeExpanded(
                     mBarState == SHADE || mBarState == SHADE_LOCKED);
@@ -4795,9 +4734,6 @@
         public void onConfigurationChanged(Configuration newConfig) {
             super.onConfigurationChanged(newConfig);
             mAffordanceHelper.onConfigurationChanged();
-            if (newConfig.orientation != mLastOrientation) {
-                resetHorizontalPanelPosition();
-            }
             mLastOrientation = newConfig.orientation;
         }
     }
@@ -4815,4 +4751,26 @@
             return insets;
         }
     }
+
+    private final PanelBar.PanelStateChangeListener mPanelStateChangeListener =
+            new PanelBar.PanelStateChangeListener() {
+
+                @PanelBar.PanelState
+                private int mCurrentState = STATE_CLOSED;
+
+                @Override
+                public void onStateChanged(@PanelBar.PanelState int state) {
+                    mAmbientState.setIsShadeOpening(state == STATE_OPENING);
+                    updateQSExpansionEnabledAmbient();
+
+                    if (state == STATE_OPEN && mCurrentState != state) {
+                        mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+                    }
+                    mCurrentState = state;
+                }
+            };
+
+    public PanelBar.PanelStateChangeListener getPanelStateChangeListener() {
+        return mPanelStateChangeListener;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
index 03d0bb0..9d06d2b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -171,6 +171,13 @@
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
     }
 
+    /**
+     * @return Location where to place the KeyguardBouncer
+     */
+    public ViewGroup getBouncerContainer() {
+        return mView.findViewById(R.id.keyguard_bouncer_container);
+    }
+
     /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */
     public void setupExpandedStatusBar() {
         mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQSContainerController.kt
new file mode 100644
index 0000000..34bb6d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQSContainerController.kt
@@ -0,0 +1,141 @@
+package com.android.systemui.statusbar.phone
+
+import android.view.WindowInsets
+import com.android.systemui.navigationbar.NavigationModeController
+import com.android.systemui.plugins.qs.QS
+import com.android.systemui.plugins.qs.QSContainerController
+import com.android.systemui.recents.OverviewProxyService
+import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
+import com.android.systemui.shared.system.QuickStepContract
+import com.android.systemui.util.ViewController
+import java.util.function.Consumer
+import javax.inject.Inject
+
+class NotificationsQSContainerController @Inject constructor(
+    view: NotificationsQuickSettingsContainer,
+    private val navigationModeController: NavigationModeController,
+    private val overviewProxyService: OverviewProxyService
+) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
+
+    var qsExpanded = false
+        set(value) {
+            if (field != value) {
+                field = value
+                mView.invalidate()
+            }
+        }
+    var splitShadeEnabled = false
+        set(value) {
+            if (field != value) {
+                field = value
+                // in case device configuration changed while showing QS details/customizer
+                updateBottomSpacing()
+            }
+        }
+
+    private var isQSDetailShowing = false
+    private var isQSCustomizing = false
+    private var isQSCustomizerAnimating = false
+
+    private var notificationsBottomMargin = 0
+    private var bottomStableInsets = 0
+    private var bottomCutoutInsets = 0
+
+    private var isGestureNavigation = true
+    private var taskbarVisible = false
+    private val taskbarVisibilityListener: OverviewProxyListener = object : OverviewProxyListener {
+        override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) {
+            taskbarVisible = visible
+        }
+    }
+    private val windowInsetsListener: Consumer<WindowInsets> = Consumer { insets ->
+        // when taskbar is visible, stableInsetBottom will include its height
+        bottomStableInsets = insets.stableInsetBottom
+        bottomCutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0
+        updateBottomSpacing()
+    }
+
+    override fun onInit() {
+        val currentMode: Int = navigationModeController.addListener { mode: Int ->
+            isGestureNavigation = QuickStepContract.isGesturalMode(mode)
+        }
+        isGestureNavigation = QuickStepContract.isGesturalMode(currentMode)
+    }
+
+    public override fun onViewAttached() {
+        notificationsBottomMargin = mView.defaultNotificationsMarginBottom
+        overviewProxyService.addCallback(taskbarVisibilityListener)
+        mView.setInsetsChangedListener(windowInsetsListener)
+        mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) }
+    }
+
+    override fun onViewDetached() {
+        overviewProxyService.removeCallback(taskbarVisibilityListener)
+        mView.removeOnInsetsChangedListener()
+        mView.removeQSFragmentAttachedListener()
+    }
+
+    override fun setCustomizerAnimating(animating: Boolean) {
+        if (isQSCustomizerAnimating != animating) {
+            isQSCustomizerAnimating = animating
+            mView.invalidate()
+        }
+    }
+
+    override fun setCustomizerShowing(showing: Boolean) {
+        isQSCustomizing = showing
+        updateBottomSpacing()
+    }
+
+    override fun setDetailShowing(showing: Boolean) {
+        isQSDetailShowing = showing
+        updateBottomSpacing()
+    }
+
+    private fun updateBottomSpacing() {
+        val (containerPadding, notificationsMargin) = calculateBottomSpacing()
+        var qsScrollPaddingBottom = 0
+        if (!(splitShadeEnabled || isQSCustomizing || isQSDetailShowing || isGestureNavigation ||
+                        taskbarVisible)) {
+            // no taskbar, portrait, navigation buttons enabled:
+            // padding is needed so QS can scroll up over bottom insets - to reach the point when
+            // the whole QS is above bottom insets
+            qsScrollPaddingBottom = bottomStableInsets
+        }
+        mView.setPadding(0, 0, 0, containerPadding)
+        mView.setNotificationsMarginBottom(notificationsMargin)
+        mView.setQSScrollPaddingBottom(qsScrollPaddingBottom)
+    }
+
+    private fun calculateBottomSpacing(): Pair<Int, Int> {
+        val containerPadding: Int
+        var stackScrollMargin = notificationsBottomMargin
+        if (splitShadeEnabled) {
+            if (isGestureNavigation) {
+                // only default cutout padding, taskbar always hides
+                containerPadding = bottomCutoutInsets
+            } else if (taskbarVisible) {
+                // navigation buttons + visible taskbar means we're NOT on homescreen
+                containerPadding = bottomStableInsets
+            } else {
+                // navigation buttons + hidden taskbar means we're on homescreen
+                containerPadding = 0
+                // we need extra margin for notifications as navigation buttons are below them
+                stackScrollMargin = bottomStableInsets + notificationsBottomMargin
+            }
+        } else {
+            if (isQSCustomizing || isQSDetailShowing) {
+                // Clear out bottom paddings/margins so the qs customization can be full height.
+                containerPadding = 0
+                stackScrollMargin = 0
+            } else if (isGestureNavigation) {
+                containerPadding = bottomCutoutInsets
+            } else if (taskbarVisible) {
+                containerPadding = bottomStableInsets
+            } else {
+                containerPadding = 0
+            }
+        }
+        return containerPadding to stackScrollMargin
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
index 68e28cd..9210a8b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
@@ -33,6 +33,7 @@
 
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.function.Consumer;
 
 /**
  * The container with notification stack scroller and quick settings inside.
@@ -43,17 +44,15 @@
     private View mQsFrame;
     private View mStackScroller;
     private View mKeyguardStatusBar;
-    private boolean mQsExpanded;
-    private boolean mCustomizerAnimating;
-    private boolean mCustomizing;
-    private boolean mDetailShowing;
 
-    private int mBottomPadding;
     private int mStackScrollerMargin;
     private ArrayList<View> mDrawingOrderedChildren = new ArrayList<>();
     private ArrayList<View> mLayoutDrawingOrder = new ArrayList<>();
     private final Comparator<View> mIndexComparator = Comparator.comparingInt(this::indexOfChild);
-    private boolean mSplitShadeEnabled;
+    private Consumer<WindowInsets> mInsetsChangedListener = insets -> {};
+    private Consumer<QS> mQSFragmentAttachedListener = qs -> {};
+    private QS mQs;
+    private View mQSScrollView;
 
     public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -69,6 +68,59 @@
     }
 
     @Override
+    public void onFragmentViewCreated(String tag, Fragment fragment) {
+        mQs = (QS) fragment;
+        mQSFragmentAttachedListener.accept(mQs);
+        mQSScrollView = mQs.getView().findViewById(R.id.expanded_qs_scroll_view);
+    }
+
+    @Override
+    public void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf) {
+        invalidate();
+    }
+
+    public void setNotificationsMarginBottom(int margin) {
+        LayoutParams params = (LayoutParams) mStackScroller.getLayoutParams();
+        params.bottomMargin = margin;
+        mStackScroller.setLayoutParams(params);
+    }
+
+    public void setQSScrollPaddingBottom(int paddingBottom) {
+        if (mQSScrollView != null) {
+            mQSScrollView.setPaddingRelative(
+                    mQSScrollView.getPaddingLeft(),
+                    mQSScrollView.getPaddingTop(),
+                    mQSScrollView.getPaddingRight(),
+                    paddingBottom
+            );
+        }
+    }
+
+    public int getDefaultNotificationsMarginBottom() {
+        return mStackScrollerMargin;
+    }
+
+    public void setInsetsChangedListener(Consumer<WindowInsets> onInsetsChangedListener) {
+        mInsetsChangedListener = onInsetsChangedListener;
+    }
+
+    public void removeOnInsetsChangedListener() {
+        mInsetsChangedListener = insets -> {};
+    }
+
+    public void setQSFragmentAttachedListener(Consumer<QS> qsFragmentAttachedListener) {
+        mQSFragmentAttachedListener = qsFragmentAttachedListener;
+        // listener might be attached after fragment is attached
+        if (mQs != null) {
+            mQSFragmentAttachedListener.accept(mQs);
+        }
+    }
+
+    public void removeQSFragmentAttachedListener() {
+        mQSFragmentAttachedListener = qs -> {};
+    }
+
+    @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         FragmentHostManager.get(this).addTagListener(QS.TAG, this);
@@ -82,8 +134,7 @@
 
     @Override
     public WindowInsets onApplyWindowInsets(WindowInsets insets) {
-        mBottomPadding = insets.getStableInsetBottom();
-        setPadding(0, 0, 0, mBottomPadding);
+        mInsetsChangedListener.accept(insets);
         return insets;
     }
 
@@ -119,66 +170,4 @@
         }
     }
 
-    @Override
-    public void onFragmentViewCreated(String tag, Fragment fragment) {
-        QS container = (QS) fragment;
-        container.setContainer(this);
-    }
-
-    public void setQsExpanded(boolean expanded) {
-        if (mQsExpanded != expanded) {
-            mQsExpanded = expanded;
-            invalidate();
-        }
-    }
-
-    public void setCustomizerAnimating(boolean isAnimating) {
-        if (mCustomizerAnimating != isAnimating) {
-            mCustomizerAnimating = isAnimating;
-            invalidate();
-        }
-    }
-
-    public void setCustomizerShowing(boolean isShowing) {
-        mCustomizing = isShowing;
-        updateBottomMargin();
-    }
-
-    public void setDetailShowing(boolean isShowing) {
-        mDetailShowing = isShowing;
-        updateBottomMargin();
-    }
-
-    /**
-     * Sets if split shade is enabled and adjusts margins/paddings depending on QS details and
-     * customizer state
-     */
-    public void setSplitShadeEnabled(boolean splitShadeEnabled) {
-        mSplitShadeEnabled = splitShadeEnabled;
-        // in case device was rotated while showing QS details/customizer
-        updateBottomMargin();
-    }
-
-    private void updateBottomMargin() {
-        // in split shade, QS state changes should not influence notifications panel
-        if (!mSplitShadeEnabled && (mCustomizing || mDetailShowing)) {
-            // Clear out bottom paddings/margins so the qs customization can be full height.
-            setPadding(0, 0, 0, 0);
-            setBottomMargin(mStackScroller, 0);
-        } else {
-            setPadding(0, 0, 0, mBottomPadding);
-            setBottomMargin(mStackScroller, mStackScrollerMargin);
-        }
-    }
-
-    private void setBottomMargin(View v, int bottomMargin) {
-        LayoutParams params = (LayoutParams) v.getLayoutParams();
-        params.bottomMargin = bottomMargin;
-        v.setLayoutParams(params);
-    }
-
-    @Override
-    public void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf) {
-        invalidate();
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java
index 247ede9..310fe73 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java
@@ -17,8 +17,9 @@
 package com.android.systemui.statusbar.phone;
 
 import static java.lang.Float.isNaN;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
 
-import android.annotation.CallSuper;
+import android.annotation.IntDef;
 import android.content.Context;
 import android.os.Bundle;
 import android.os.Parcelable;
@@ -27,6 +28,10 @@
 import android.view.MotionEvent;
 import android.widget.FrameLayout;
 
+import androidx.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+
 public abstract class PanelBar extends FrameLayout {
     public static final boolean DEBUG = false;
     public static final String TAG = PanelBar.class.getSimpleName();
@@ -40,26 +45,27 @@
         Log.v(TAG, String.format(fmt, args));
     }
 
+    /** Enum for the current state of the panel. */
+    @Retention(SOURCE)
+    @IntDef({STATE_CLOSED, STATE_OPENING, STATE_OPEN})
+    @interface PanelState {}
     public static final int STATE_CLOSED = 0;
     public static final int STATE_OPENING = 1;
     public static final int STATE_OPEN = 2;
 
-    PanelViewController mPanel;
+    private PanelViewController mPanel;
+    @Nullable private PanelStateChangeListener mPanelStateChangeListener;
     private int mState = STATE_CLOSED;
     private boolean mTracking;
 
-    public void go(int state) {
+    private void go(@PanelState int state) {
         if (DEBUG) LOG("go state: %d -> %d", mState, state);
         mState = state;
-        if (mPanel != null) {
-            mPanel.setIsShadeOpening(state == STATE_OPENING);
+        if (mPanelStateChangeListener != null) {
+            mPanelStateChangeListener.onStateChanged(state);
         }
     }
 
-    protected boolean isShadeOpening() {
-        return mState == STATE_OPENING;
-    }
-
     @Override
     protected Parcelable onSaveInstanceState() {
         Bundle bundle = new Bundle();
@@ -97,6 +103,11 @@
         pv.setBar(this);
     }
 
+    /** Sets the listener that will be notified of panel state changes. */
+    public void setPanelStateChangeListener(PanelStateChangeListener listener) {
+        mPanelStateChangeListener = listener;
+    }
+
     public boolean panelEnabled() {
         return true;
     }
@@ -135,14 +146,6 @@
     }
 
     /**
-     * Percentage of panel expansion offset, caused by pulling down on a heads-up.
-     */
-    @CallSuper
-    public void onPanelMinFractionChanged(float minFraction) {
-        mPanel.setMinFraction(minFraction);
-    }
-
-    /**
      * @param frac the fraction from the expansion in [0, 1]
      * @param expanded whether the panel is currently expanded; this is independent from the
      *                 fraction as the panel also might be expanded if the fraction is 0
@@ -166,7 +169,6 @@
         }
         if (fullyOpened && !mTracking) {
             go(STATE_OPEN);
-            onPanelFullyOpened();
         } else if (fullyClosed && !mTracking && mState != STATE_CLOSED) {
             go(STATE_CLOSED);
             onPanelCollapsed();
@@ -207,10 +209,6 @@
         if (DEBUG) LOG("onPanelCollapsed");
     }
 
-    public void onPanelFullyOpened() {
-        if (DEBUG) LOG("onPanelFullyOpened");
-    }
-
     public void onTrackingStarted() {
         mTracking = true;
     }
@@ -226,4 +224,10 @@
     public void onClosingFinished() {
 
     }
+
+    /** An interface that will be notified of panel state changes. */
+    public interface PanelStateChangeListener {
+        /** Called when the state changes. */
+        void onStateChanged(@PanelState int state);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
index b215515..c23577c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
@@ -343,13 +343,6 @@
     protected abstract float getOpeningHeight();
 
     /**
-     * Minimum fraction from where expansion should start. This is set when pulling down on a
-     * heads-up notification.
-     * @param minFraction Fraction from 0 to 1.
-     */
-    public abstract void setMinFraction(float minFraction);
-
-    /**
      * @return whether the swiping direction is upwards and above a 45 degree angle compared to the
      * horizontal direction
      */
@@ -392,11 +385,16 @@
 
             final boolean expand;
             if (event.getActionMasked() == MotionEvent.ACTION_CANCEL || forceCancel) {
-                // If we get a cancel, put the shade back to the state it was in when the gesture
-                // started
-                if (onKeyguard) {
+                // If the keyguard is fading away, don't expand it again. This can happen if you're
+                // swiping to unlock, the app below the keyguard is in landscape, and the screen
+                // rotates while your finger is still down after the swipe to unlock.
+                if (mKeyguardStateController.isKeyguardFadingAway()) {
+                    expand = false;
+                } else if (onKeyguard) {
                     expand = true;
                 } else {
+                    // If we get a cancel, put the shade back to the state it was in when the
+                    // gesture started
                     expand = !mPanelClosedOnDown;
                 }
             } else {
@@ -1171,11 +1169,6 @@
         return new OnConfigurationChangedListener();
     }
 
-    /**
-     * Set that the panel is currently opening and not fully opened or closed.
-     */
-    public abstract void setIsShadeOpening(boolean opening);
-
     public class TouchHandler implements View.OnTouchListener {
         public boolean onInterceptTouchEvent(MotionEvent event) {
             if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index 9ab6cdd..150d9c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -18,8 +18,6 @@
 
 import static com.android.systemui.ScreenDecorations.DisplayCutoutView.boundsFromDirection;
 
-import static java.lang.Float.isNaN;
-
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -56,9 +54,7 @@
 
     StatusBar mBar;
 
-    boolean mIsFullyOpenedPanel = false;
     private ScrimController mScrimController;
-    private float mMinFraction;
     private Runnable mHideExpandedRunnable = new Runnable() {
         @Override
         public void run() {
@@ -216,7 +212,6 @@
         super.onPanelCollapsed();
         // Close the status bar in the next frame so we can show the end of the animation.
         post(mHideExpandedRunnable);
-        mIsFullyOpenedPanel = false;
     }
 
     public void removePendingHideExpandedRunnables() {
@@ -224,15 +219,6 @@
     }
 
     @Override
-    public void onPanelFullyOpened() {
-        super.onPanelFullyOpened();
-        if (!mIsFullyOpenedPanel) {
-            mPanel.getView().sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
-        }
-        mIsFullyOpenedPanel = true;
-    }
-
-    @Override
     public boolean onTouchEvent(MotionEvent event) {
         boolean barConsumedEvent = mBar.interceptTouchEvent(event);
 
@@ -279,21 +265,8 @@
     }
 
     @Override
-    public void onPanelMinFractionChanged(float minFraction) {
-        if (isNaN(minFraction)) {
-            throw new IllegalArgumentException("minFraction cannot be NaN");
-        }
-        super.onPanelMinFractionChanged(minFraction);
-        if (mMinFraction != minFraction) {
-            mMinFraction = minFraction;
-            updateScrimFraction();
-        }
-    }
-
-    @Override
     public void panelExpansionChanged(float frac, boolean expanded) {
         super.panelExpansionChanged(frac, expanded);
-        updateScrimFraction();
         if ((frac == 0 || frac == 1)) {
             if (mPanelExpansionStateChangedListener != null) {
                 mPanelExpansionStateChangedListener.onPanelExpansionStateChanged();
@@ -314,15 +287,6 @@
         mPanelEnabledProvider = panelEnabledProvider;
     }
 
-    private void updateScrimFraction() {
-        float scrimFraction = mPanelFraction;
-        if (mMinFraction < 1.0f) {
-            scrimFraction = Math.max((mPanelFraction - mMinFraction) / (1.0f - mMinFraction),
-                    0);
-        }
-        mScrimController.setPanelExpansion(scrimFraction);
-    }
-
     public void updateResources() {
         mCutoutSideNudge = getResources().getDimensionPixelSize(
                 R.dimen.display_cutout_margin_consumption);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index a564637..a5cea06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -34,6 +34,7 @@
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 
+import androidx.annotation.FloatRange;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -183,8 +184,10 @@
     private float mScrimBehindAlphaKeyguard = KEYGUARD_SCRIM_ALPHA;
     private final float mDefaultScrimAlpha;
 
-    // Assuming the shade is expanded during initialization
-    private float mPanelExpansion = 1f;
+    private float mRawPanelExpansionFraction;
+    private float mPanelScrimMinFraction;
+    // Calculated based on mRawPanelExpansionFraction and mPanelScrimMinFraction
+    private float mPanelExpansionFraction = 1f; // Assume shade is expanded during initialization
     private float mQsExpansion;
     private boolean mQsBottomVisible;
 
@@ -262,11 +265,6 @@
             }
 
             @Override
-            public void onOverlayChanged() {
-                ScrimController.this.onThemeChanged();
-            }
-
-            @Override
             public void onUiModeChanged() {
                 ScrimController.this.onThemeChanged();
             }
@@ -483,14 +481,39 @@
      *
      * The expansion fraction is tied to the scrim opacity.
      *
-     * @param fraction From 0 to 1 where 0 means collapsed and 1 expanded.
+     * See {@link PanelBar#panelExpansionChanged}.
+     *
+     * @param rawPanelExpansionFraction From 0 to 1 where 0 means collapsed and 1 expanded.
      */
-    public void setPanelExpansion(float fraction) {
-        if (isNaN(fraction)) {
-            throw new IllegalArgumentException("Fraction should not be NaN");
+    public void setRawPanelExpansionFraction(
+            @FloatRange(from = 0.0, to = 1.0) float rawPanelExpansionFraction) {
+        if (isNaN(rawPanelExpansionFraction)) {
+            throw new IllegalArgumentException("rawPanelExpansionFraction should not be NaN");
         }
-        if (mPanelExpansion != fraction) {
-            mPanelExpansion = fraction;
+        mRawPanelExpansionFraction = rawPanelExpansionFraction;
+        calculateAndUpdatePanelExpansion();
+    }
+
+    /** See {@link NotificationPanelViewController#setPanelScrimMinFraction(float)}. */
+    public void setPanelScrimMinFraction(float minFraction) {
+        if (isNaN(minFraction)) {
+            throw new IllegalArgumentException("minFraction should not be NaN");
+        }
+        mPanelScrimMinFraction = minFraction;
+        calculateAndUpdatePanelExpansion();
+    }
+
+    private void calculateAndUpdatePanelExpansion() {
+        float panelExpansionFraction = mRawPanelExpansionFraction;
+        if (mPanelScrimMinFraction < 1.0f) {
+            panelExpansionFraction = Math.max(
+                    (mRawPanelExpansionFraction - mPanelScrimMinFraction)
+                            / (1.0f - mPanelScrimMinFraction),
+                    0);
+        }
+
+        if (mPanelExpansionFraction != panelExpansionFraction) {
+            mPanelExpansionFraction = panelExpansionFraction;
 
             boolean relevantState = (mState == ScrimState.UNLOCKED
                     || mState == ScrimState.KEYGUARD
@@ -892,7 +915,8 @@
     }
 
     private float getInterpolatedFraction() {
-        return Interpolators.getNotificationScrimAlpha(mPanelExpansion, false /* notification */);
+        return Interpolators.getNotificationScrimAlpha(
+                mPanelExpansionFraction, false /* notification */);
     }
 
     private void setScrimAlpha(ScrimView scrim, float alpha) {
@@ -1228,8 +1252,8 @@
         pw.println(mTracking);
         pw.print("  mDefaultScrimAlpha=");
         pw.println(mDefaultScrimAlpha);
-        pw.print("  mExpansionFraction=");
-        pw.println(mPanelExpansion);
+        pw.print("  mPanelExpansionFraction=");
+        pw.println(mPanelExpansionFraction);
         pw.print("  mExpansionAffectsAlpha=");
         pw.println(mExpansionAffectsAlpha);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 06c3573..5d2e6f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -336,6 +336,7 @@
     }
 
     private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    private boolean mCallingFadingAwayAfterReveal;
     private StatusBarCommandQueueCallbacks mCommandQueueCallbacks;
 
     void setWindowState(int state) {
@@ -628,6 +629,7 @@
     private final Executor mUiBgExecutor;
 
     protected boolean mDozing;
+    private boolean mIsFullscreen;
 
     private final NotificationMediaManager mMediaManager;
     private final NotificationLockscreenUserManager mLockscreenUserManager;
@@ -898,6 +900,8 @@
         lockscreenShadeTransitionController.setStatusbar(this);
 
         mExpansionChangedListeners = new ArrayList<>();
+        addExpansionChangedListener(
+                (expansion, expanded) -> mScrimController.setRawPanelExpansionFraction(expansion));
 
         mBubbleExpandListener =
                 (isExpanding, key) -> mContext.getMainExecutor().execute(() -> {
@@ -909,6 +913,9 @@
         mActivityLaunchAnimator = activityLaunchAnimator;
         mDialogLaunchAnimator = dialogLaunchAnimator;
 
+        // The status bar background may need updating when the ongoing call status changes.
+        mOngoingCallController.addCallback((animate) -> maybeUpdateBarMode());
+
         // TODO(b/190746471): Find a better home for this.
         DateTimeView.setReceiverHandler(timeTickHandler);
 
@@ -1161,6 +1168,8 @@
                     mStatusBarView = (PhoneStatusBarView) statusBarFragment.getView();
                     mStatusBarView.setBar(this);
                     mStatusBarView.setPanel(mNotificationPanelViewController);
+                    mStatusBarView.setPanelStateChangeListener(
+                            mNotificationPanelViewController.getPanelStateChangeListener());
                     mStatusBarView.setScrimController(mScrimController);
                     mStatusBarView.setExpansionChangedListeners(mExpansionChangedListeners);
                     for (ExpansionChangedListener listener : mExpansionChangedListeners) {
@@ -1398,6 +1407,12 @@
         mDeviceProvisionedController.addCallback(mUserSetupObserver);
         mUserSetupObserver.onUserSetupChanged();
 
+        for (ExpansionChangedListener listener : mExpansionChangedListeners) {
+            // The initial expansion amount comes from mNotificationPanelViewController, so we
+            // should send the amount once we've fully set up that controller.
+            sendInitialExpansionAmount(listener);
+        }
+
         // disable profiling bars, since they overlap and clutter the output on app windows
         ThreadedRenderer.overrideProperty("disableProfileBars", "true");
 
@@ -1550,6 +1565,9 @@
                     time, PowerManager.WAKE_REASON_GESTURE, "com.android.systemui:" + why);
             mWakeUpComingFromTouch = true;
             where.getLocationInWindow(mTmpInt2);
+
+            // NOTE, the incoming view can sometimes be the entire container... unsure if
+            // this location is valuable enough
             mWakeUpTouchLocation = new PointF(mTmpInt2[0] + where.getWidth() / 2,
                     mTmpInt2[1] + where.getHeight() / 2);
             mFalsingCollector.onScreenOnFromTouch();
@@ -1643,7 +1661,7 @@
                     }
                 });
         mStatusBarKeyguardViewManager.registerStatusBar(
-                /* statusBar= */ this, getBouncerContainer(),
+                /* statusBar= */ this,
                 mNotificationPanelViewController, mBiometricUnlockController,
                 mStackScroller, mKeyguardBypassController);
         mKeyguardIndicationController
@@ -1678,8 +1696,8 @@
         return mNotificationPanelViewController;
     }
 
-    protected ViewGroup getBouncerContainer() {
-        return mNotificationShadeWindowView.findViewById(R.id.keyboard_bouncer_container);
+    public ViewGroup getBouncerContainer() {
+        return mNotificationShadeWindowViewController.getBouncerContainer();
     }
 
     public int getStatusBarHeight() {
@@ -2237,7 +2255,7 @@
         if (!mTransientShown) {
             mTransientShown = true;
             mNoAnimationOnNextBarModeChange = true;
-            handleTransientChanged();
+            maybeUpdateBarMode();
         }
     }
 
@@ -2245,11 +2263,11 @@
     void clearTransient() {
         if (mTransientShown) {
             mTransientShown = false;
-            handleTransientChanged();
+            maybeUpdateBarMode();
         }
     }
 
-    private void handleTransientChanged() {
+    private void maybeUpdateBarMode() {
         final int barMode = barMode(mTransientShown, mAppearance);
         if (updateBarMode(barMode)) {
             mLightBarController.onStatusBarModeChanged(barMode);
@@ -2267,9 +2285,11 @@
         return false;
     }
 
-    private static @TransitionMode int barMode(boolean isTransient, int appearance) {
+    private @TransitionMode int barMode(boolean isTransient, int appearance) {
         final int lightsOutOpaque = APPEARANCE_LOW_PROFILE_BARS | APPEARANCE_OPAQUE_STATUS_BARS;
-        if (isTransient) {
+        if (mOngoingCallController.hasOngoingCall() && mIsFullscreen) {
+            return MODE_SEMI_TRANSPARENT;
+        } else if (isTransient) {
             return MODE_SEMI_TRANSPARENT;
         } else if ((appearance & lightsOutOpaque) == lightsOutOpaque) {
             return MODE_LIGHTS_OUT;
@@ -3120,8 +3140,20 @@
     public void fadeKeyguardWhilePulsing() {
         mNotificationPanelViewController.fadeOut(0, FADE_KEYGUARD_DURATION_PULSING,
                 ()-> {
-                hideKeyguard();
-                mStatusBarKeyguardViewManager.onKeyguardFadedAway();
+                Runnable finishFading = () -> {
+                    mCallingFadingAwayAfterReveal = false;
+                    hideKeyguard();
+                    mStatusBarKeyguardViewManager.onKeyguardFadedAway();
+                };
+                if (mLightRevealScrim.getRevealAmount() != 1.0f) {
+                    mCallingFadingAwayAfterReveal = true;
+                    // We're still revealing the Light reveal, let's only go to keyguard once
+                    // that has finished and nothing moves anymore.
+                    // Going there introduces lots of jank
+                    mLightRevealScrim.setFullyRevealedRunnable(finishFading);
+                } else {
+                    finishFading.run();
+                }
             }).start();
     }
 
@@ -3255,16 +3287,16 @@
      * Switches theme from light to dark and vice-versa.
      */
     protected void updateTheme() {
-
         // Lock wallpaper defines the color of the majority of the views, hence we'll use it
         // to set our default theme.
         final boolean lockDarkText = mColorExtractor.getNeutralColors().supportsDarkText();
         final int themeResId = lockDarkText ? R.style.Theme_SystemUI_LightWallpaper
                 : R.style.Theme_SystemUI;
-        if (mContext.getThemeResId() != themeResId) {
-            mContext.setTheme(themeResId);
-            mConfigurationController.notifyThemeChanged();
+        if (mContext.getThemeResId() == themeResId) {
+            return;
         }
+        mContext.setTheme(themeResId);
+        mConfigurationController.notifyThemeChanged();
     }
 
     private void updateDozingState() {
@@ -4222,9 +4254,11 @@
     }
 
     private void sendInitialExpansionAmount(ExpansionChangedListener expansionChangedListener) {
-        expansionChangedListener.onExpansionChanged(
-                mNotificationPanelViewController.getExpandedFraction(),
-                mNotificationPanelViewController.isExpanded());
+        if (mNotificationPanelViewController != null) {
+            expansionChangedListener.onExpansionChanged(
+                    mNotificationPanelViewController.getExpandedFraction(),
+                    mNotificationPanelViewController.isExpanded());
+        }
     }
 
     public void removeExpansionChangedListener(@NonNull ExpansionChangedListener listener) {
@@ -4280,7 +4314,7 @@
                         + "mStatusBarKeyguardViewManager was null");
                 return;
             }
-            if (mKeyguardStateController.isKeyguardFadingAway()) {
+            if (mKeyguardStateController.isKeyguardFadingAway() && !mCallingFadingAwayAfterReveal) {
                 mStatusBarKeyguardViewManager.onKeyguardFadedAway();
             }
         }
@@ -4375,6 +4409,13 @@
 
         @Override
         public void onThemeChanged() {
+            if (mBrightnessMirrorController != null) {
+                mBrightnessMirrorController.onOverlayChanged();
+            }
+            // We need the new R.id.keyguard_indication_area before recreating
+            // mKeyguardIndicationController
+            mNotificationPanelViewController.onThemeChanged();
+
             if (mStatusBarKeyguardViewManager != null) {
                 mStatusBarKeyguardViewManager.onThemeChanged();
             }
@@ -4385,17 +4426,6 @@
         }
 
         @Override
-        public void onOverlayChanged() {
-            if (mBrightnessMirrorController != null) {
-                mBrightnessMirrorController.onOverlayChanged();
-            }
-            // We need the new R.id.keyguard_indication_area before recreating
-            // mKeyguardIndicationController
-            mNotificationPanelViewController.onThemeChanged();
-            onThemeChanged();
-        }
-
-        @Override
         public void onUiModeChanged() {
             if (mBrightnessMirrorController != null) {
                 mBrightnessMirrorController.onUiModeChanged();
@@ -4471,6 +4501,12 @@
                     updateReportRejectedTouchVisibility();
                     Trace.endSection();
                 }
+
+                @Override
+                public void onFullscreenStateChanged(boolean isFullscreen) {
+                    mIsFullscreen = isFullscreen;
+                    maybeUpdateBarMode();
+                }
             };
 
     private final BatteryController.BatteryStateChangeCallback mBatteryStateChangeCallback =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
index 61552f0..98be77d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt
@@ -91,7 +91,7 @@
         clearCachedInsets()
     }
 
-    override fun onOverlayChanged() {
+    override fun onThemeChanged() {
         clearCachedInsets()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 5bc97a5..07489a9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -120,7 +120,8 @@
         @Override
         public void onFullyShown() {
             updateStates();
-            mStatusBar.wakeUpIfDozing(SystemClock.uptimeMillis(), mContainer, "BOUNCER_VISIBLE");
+            mStatusBar.wakeUpIfDozing(SystemClock.uptimeMillis(),
+                    mStatusBar.getBouncerContainer(), "BOUNCER_VISIBLE");
         }
 
         @Override
@@ -174,7 +175,6 @@
     private NotificationPanelViewController mNotificationPanelViewController;
     private BiometricUnlockController mBiometricUnlockController;
 
-    private ViewGroup mContainer;
     private View mNotificationContainer;
 
     protected KeyguardBouncer mBouncer;
@@ -270,14 +270,14 @@
 
     @Override
     public void registerStatusBar(StatusBar statusBar,
-            ViewGroup container,
             NotificationPanelViewController notificationPanelViewController,
             BiometricUnlockController biometricUnlockController,
             View notificationContainer,
             KeyguardBypassController bypassController) {
         mStatusBar = statusBar;
-        mContainer = container;
         mBiometricUnlockController = biometricUnlockController;
+
+        ViewGroup container = mStatusBar.getBouncerContainer();
         mBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
         mNotificationPanelViewController = notificationPanelViewController;
         notificationPanelViewController.addExpansionListener(this);
@@ -356,7 +356,8 @@
         } else if (mPulsing && expansion == KeyguardBouncer.EXPANSION_VISIBLE) {
             // Panel expanded while pulsing but didn't translate the bouncer (because we are
             // unlocked.) Let's simply wake-up to dismiss the lock screen.
-            mStatusBar.wakeUpIfDozing(SystemClock.uptimeMillis(), mContainer, "BOUNCER_VISIBLE");
+            mStatusBar.wakeUpIfDozing(SystemClock.uptimeMillis(), mStatusBar.getBouncerContainer(),
+                    "BOUNCER_VISIBLE");
         }
     }
 
@@ -833,7 +834,7 @@
     }
 
     public void onKeyguardFadedAway() {
-        mContainer.postDelayed(() -> mNotificationShadeWindowController
+        mNotificationContainer.postDelayed(() -> mNotificationShadeWindowController
                         .setKeyguardFadingAway(false), 100);
         ViewGroupFadeHelper.reset(mNotificationPanelViewController.getView());
         mStatusBar.finishKeyguardFadingAway();
@@ -958,10 +959,6 @@
     };
 
     protected void updateStates() {
-        if (mContainer == null ) {
-            return;
-        }
-        int vis = mContainer.getSystemUiVisibility();
         boolean showing = mShowing;
         boolean occluded = mOccluded;
         boolean bouncerShowing = mBouncer.isShowing();
@@ -973,9 +970,9 @@
                 (mLastBouncerDismissible || !mLastShowing || mLastRemoteInputActive)
                 || mFirstUpdate) {
             if (bouncerDismissible || !showing || remoteInputActive) {
-                mContainer.setSystemUiVisibility(vis & ~View.STATUS_BAR_DISABLE_BACK);
+                mBouncer.setBackButtonEnabled(true);
             } else {
-                mContainer.setSystemUiVisibility(vis | View.STATUS_BAR_DISABLE_BACK);
+                mBouncer.setBackButtonEnabled(false);
             }
         }
 
@@ -1042,11 +1039,11 @@
                 if (delay == 0) {
                     mMakeNavigationBarVisibleRunnable.run();
                 } else {
-                    mContainer.postOnAnimationDelayed(mMakeNavigationBarVisibleRunnable,
+                    mNotificationContainer.postOnAnimationDelayed(mMakeNavigationBarVisibleRunnable,
                             delay);
                 }
             } else {
-                mContainer.removeCallbacks(mMakeNavigationBarVisibleRunnable);
+                mNotificationContainer.removeCallbacks(mMakeNavigationBarVisibleRunnable);
                 mStatusBar.getNotificationShadeWindowView().getWindowInsetsController()
                         .hide(navigationBars());
             }
@@ -1149,7 +1146,6 @@
     public void showBouncerMessage(String message, ColorStateList colorState) {
         if (isShowingAlternateAuth()) {
             if (mKeyguardMessageAreaController != null) {
-                mKeyguardMessageAreaController.setNextMessageColor(colorState);
                 mKeyguardMessageAreaController.setMessage(message);
             }
         } else {
@@ -1372,4 +1368,4 @@
         void requestUdfps(boolean requestUdfps, int color);
 
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index 832f317..c655964 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -25,7 +25,6 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
-import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 import android.service.vr.IVrManager;
 import android.service.vr.IVrStateCallbacks;
@@ -248,7 +247,7 @@
     }
 
     @Override
-    public void onOverlayChanged() {
+    public void onThemeChanged() {
         onDensityOrFontScaleChanged();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index d3d9063..eb405e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -83,7 +83,7 @@
             }
 
             @Override
-            public void onOverlayChanged() {
+            public void onThemeChanged() {
                 initResources();
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java
index aec27d0..235a8e8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java
@@ -209,6 +209,21 @@
     }
 
     /**
+     * Sets whether an ongoing process requires the status bar to be forced visible.
+     *
+     * This method is separate from {@link this#setForceStatusBarVisible} because the ongoing
+     * process **takes priority**. For example, if {@link this#setForceStatusBarVisible} is set to
+     * false but this method is set to true, then the status bar **will** be visible.
+     *
+     * TODO(b/195839150): We should likely merge this method and
+     * {@link this#setForceStatusBarVisible} together and use some sort of ranking system instead.
+     */
+    public void setOngoingProcessRequiresStatusBarVisible(boolean visible) {
+        mCurrentState.mOngoingProcessRequiresStatusBarVisible = visible;
+        apply(mCurrentState);
+    }
+
+    /**
      * Return the container in which we should run launch animations started from the status bar and
      * expanding into the opening window.
      *
@@ -248,10 +263,14 @@
     private static class State {
         boolean mForceStatusBarVisible;
         boolean mIsLaunchAnimationRunning;
+        boolean mOngoingProcessRequiresStatusBarVisible;
     }
 
     private void applyForceStatusBarVisibleFlag(State state) {
-        if (state.mForceStatusBarVisible || state.mIsLaunchAnimationRunning) {
+        if (state.mForceStatusBarVisible
+                || state.mIsLaunchAnimationRunning
+                // Don't force-show the status bar if the user has already dismissed it.
+                || state.mOngoingProcessRequiresStatusBarVisible) {
             mLpChanged.privateFlags |= PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
         } else {
             mLpChanged.privateFlags &= ~PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/TapAgainViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/TapAgainViewController.java
index 0c5502b..26ba31c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/TapAgainViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/TapAgainViewController.java
@@ -43,11 +43,6 @@
     @VisibleForTesting
     final ConfigurationListener mConfigurationListener = new ConfigurationListener() {
         @Override
-        public void onOverlayChanged() {
-            mView.updateColor();
-        }
-
-        @Override
         public void onUiModeChanged() {
             mView.updateColor();
         }
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 143aaba..f3f3325 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
@@ -4,10 +4,10 @@
 import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
 import android.content.Context
-import android.content.res.Configuration
 import android.database.ContentObserver
 import android.os.Handler
 import android.provider.Settings
+import android.view.Surface
 import android.view.View
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
@@ -239,10 +239,11 @@
             return false
         }
 
-        // If we're not allowed to rotate the keyguard, then only do the screen off animation if
-        // we're in portrait. Otherwise, AOD will animate in sideways, which looks weird.
+        // If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree
+        // portrait. If we're in another orientation, disable the screen off animation so we don't
+        // animate in the keyguard AOD UI sideways or upside down.
         if (!keyguardStateController.isKeyguardScreenRotationAllowed &&
-                context.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
+            context.display.rotation != Surface.ROTATION_0) {
             return false
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index ecf3b86..2791678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -29,9 +29,9 @@
 import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
 import com.android.systemui.statusbar.phone.NotificationPanelView;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
+import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
 import com.android.systemui.statusbar.phone.TapAgainView;
 import com.android.systemui.util.InjectionInflationController;
 
@@ -67,8 +67,8 @@
     @Provides
     @StatusBarComponent.StatusBarScope
     public static NotificationStackScrollLayout providesNotificationStackScrollLayout(
-            NotificationStackScrollLayoutController notificationStackScrollLayoutController) {
-        return notificationStackScrollLayoutController.getView();
+            NotificationShadeWindowView notificationShadeWindowView) {
+        return notificationShadeWindowView.findViewById(R.id.notification_stack_scroller);
     }
 
     /** */
@@ -149,4 +149,12 @@
     public static TapAgainView getTapAgainView(NotificationPanelView npv) {
         return npv.getTapAgainView();
     }
+
+    /** */
+    @Provides
+    @StatusBarComponent.StatusBarScope
+    public static NotificationsQuickSettingsContainer getNotificationsQuickSettingsContainer(
+            NotificationShadeWindowView notificationShadeWindowView) {
+        return notificationShadeWindowView.findViewById(R.id.notification_container_parent);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index ab6ee89..3806d9a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -34,13 +34,17 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.phone.StatusBarWindowController
 import com.android.systemui.statusbar.policy.CallbackController
 import com.android.systemui.util.time.SystemClock
 import java.io.FileDescriptor
 import java.io.PrintWriter
+import java.util.Optional
 import java.util.concurrent.Executor
 import javax.inject.Inject
 
@@ -57,8 +61,12 @@
     private val iActivityManager: IActivityManager,
     private val logger: OngoingCallLogger,
     private val dumpManager: DumpManager,
+    private val statusBarWindowController: Optional<StatusBarWindowController>,
+    private val swipeStatusBarAwayGestureHandler: Optional<SwipeStatusBarAwayGestureHandler>,
+    private val statusBarStateController: StatusBarStateController,
 ) : CallbackController<OngoingCallListener>, Dumpable {
 
+    private var isFullscreen: Boolean = false
     /** Non-null if there's an active call notification. */
     private var callNotificationInfo: CallNotificationInfo? = null
     /** True if the application managing the call is visible to the user. */
@@ -93,7 +101,8 @@
                         entry.sbn.notification.contentIntent?.intent,
                         entry.sbn.uid,
                         entry.sbn.notification.extras.getInt(
-                                Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING
+                                Notification.EXTRA_CALL_TYPE, -1) == CALL_TYPE_ONGOING,
+                        statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false
                 )
                 if (newOngoingCallInfo == callNotificationInfo) {
                     return
@@ -119,6 +128,7 @@
         dumpManager.registerDumpable(this)
         if (featureFlags.isOngoingCallStatusBarChipEnabled) {
             notifCollection.addCollectionListener(notifListener)
+            statusBarStateController.addCallback(statusBarStateListener)
         }
     }
 
@@ -172,10 +182,8 @@
 
         val currentChipView = chipView
         val timeView = currentChipView?.getTimeView()
-        val backgroundView =
-            currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background)
 
-        if (currentChipView != null && timeView != null && backgroundView != null) {
+        if (currentChipView != null && timeView != null) {
             if (currentCallNotificationInfo.hasValidStartTime()) {
                 timeView.setShouldHideText(false)
                 timeView.base = currentCallNotificationInfo.callStartTime -
@@ -186,22 +194,15 @@
                 timeView.setShouldHideText(true)
                 timeView.stop()
             }
-
-            currentCallNotificationInfo.intent?.let { intent ->
-                currentChipView.setOnClickListener {
-                    logger.logChipClicked()
-                    activityStarter.postStartActivityDismissingKeyguard(
-                            intent,
-                            0,
-                            ActivityLaunchAnimator.Controller.fromView(
-                                    backgroundView,
-                                    InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP)
-                    )
-                }
-            }
+            updateChipClickListener()
 
             setUpUidObserver(currentCallNotificationInfo)
-
+            if (!currentCallNotificationInfo.statusBarSwipedAway) {
+                statusBarWindowController.ifPresent {
+                    it.setOngoingProcessRequiresStatusBarVisible(true)
+                }
+            }
+            updateGestureListening()
             mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
         } else {
             // If we failed to update the chip, don't store the call info. Then [hasOngoingCall]
@@ -215,6 +216,30 @@
         }
     }
 
+    private fun updateChipClickListener() {
+        if (callNotificationInfo == null) { return }
+        if (isFullscreen && !featureFlags.isOngoingCallInImmersiveChipTapEnabled) {
+            chipView?.setOnClickListener(null)
+        } else {
+            val currentChipView = chipView
+            val backgroundView =
+                currentChipView?.findViewById<View>(R.id.ongoing_call_chip_background)
+            val intent = callNotificationInfo?.intent
+            if (currentChipView != null && backgroundView != null && intent != null) {
+                currentChipView.setOnClickListener {
+                    logger.logChipClicked()
+                    activityStarter.postStartActivityDismissingKeyguard(
+                        intent,
+                        0,
+                        ActivityLaunchAnimator.Controller.fromView(
+                            backgroundView,
+                            InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP)
+                    )
+                }
+            }
+        }
+    }
+
     /**
      * Sets up an [IUidObserver] to monitor the status of the application managing the ongoing call.
      */
@@ -265,9 +290,23 @@
         return procState <= ActivityManager.PROCESS_STATE_TOP
     }
 
+    private fun updateGestureListening() {
+        if (callNotificationInfo == null
+            || callNotificationInfo?.statusBarSwipedAway == true
+            || !isFullscreen) {
+            swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) }
+        } else {
+            swipeStatusBarAwayGestureHandler.ifPresent {
+                it.addOnGestureDetectedCallback(TAG, this::onSwipeAwayGestureDetected)
+            }
+        }
+    }
+
     private fun removeChip() {
         callNotificationInfo = null
         tearDownChipView()
+        statusBarWindowController.ifPresent { it.setOngoingProcessRequiresStatusBarVisible(false) }
+        swipeStatusBarAwayGestureHandler.ifPresent { it.removeOnGestureDetectedCallback(TAG) }
         mListeners.forEach { l -> l.onOngoingCallStateChanged(animate = true) }
         if (uidObserver != null) {
             iActivityManager.unregisterUidObserver(uidObserver)
@@ -282,13 +321,42 @@
         return this.findViewById(R.id.ongoing_call_chip_time)
     }
 
+   /**
+    * If there's an active ongoing call, then we will force the status bar to always show, even if
+    * the user is in immersive mode. However, we also want to give users the ability to swipe away
+    * the status bar if they need to access the area under the status bar.
+    *
+    * This method updates the status bar window appropriately when the swipe away gesture is
+    * detected.
+    */
+   private fun onSwipeAwayGestureDetected() {
+       if (DEBUG) { Log.d(TAG, "Swipe away gesture detected") }
+       callNotificationInfo = callNotificationInfo?.copy(statusBarSwipedAway = true)
+       statusBarWindowController.ifPresent {
+           it.setOngoingProcessRequiresStatusBarVisible(false)
+       }
+       swipeStatusBarAwayGestureHandler.ifPresent {
+           it.removeOnGestureDetectedCallback(TAG)
+       }
+   }
+
+    private val statusBarStateListener = object : StatusBarStateController.StateListener {
+        override fun onFullscreenStateChanged(isFullscreen: Boolean) {
+            this@OngoingCallController.isFullscreen = isFullscreen
+            updateChipClickListener()
+            updateGestureListening()
+        }
+    }
+
     private data class CallNotificationInfo(
         val key: String,
         val callStartTime: Long,
         val intent: Intent?,
         val uid: Int,
         /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */
-        val isOngoing: Boolean
+        val isOngoing: Boolean,
+        /** True if the user has swiped away the status bar while in this phone call. */
+        val statusBarSwipedAway: Boolean
     ) {
         /**
          * Returns true if the notification information has a valid call start time.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
index 856853d..2dd4128 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AccessibilityManagerWrapper.java
@@ -14,7 +14,6 @@
 
 package com.android.systemui.statusbar.policy;
 
-import android.content.Context;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener;
@@ -35,8 +34,8 @@
     private final AccessibilityManager mAccessibilityManager;
 
     @Inject
-    public AccessibilityManagerWrapper(Context context) {
-        mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+    public AccessibilityManagerWrapper(AccessibilityManager accessibilityManager) {
+        mAccessibilityManager = accessibilityManager;
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
index e679c4c..6b80a9d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java
@@ -38,7 +38,6 @@
         default void onDensityOrFontScaleChanged() {}
         default void onSmallestScreenWidthChanged() {}
         default void onMaxBoundsChanged() {}
-        default void onOverlayChanged() {}
         default void onUiModeChanged() {}
         default void onThemeChanged() {}
         default void onLocaleListChanged() {}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureController.java
index 8596875..bbba19d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureController.java
@@ -46,6 +46,7 @@
     int DEVICE_POSTURE_HALF_OPENED = 2;
     int DEVICE_POSTURE_OPENED = 3;
     int DEVICE_POSTURE_FLIPPED = 4;
+    int SUPPORTED_POSTURES_SIZE = DEVICE_POSTURE_FLIPPED + 1;
 
     /** Return the current device posture. */
     @DevicePostureInt int getDevicePosture();
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 a8097c4..e0b0dd36 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -38,11 +38,10 @@
 import com.android.systemui.statusbar.AlertingNotificationManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
+import com.android.systemui.util.ListenerSet;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.HashSet;
 
 /**
  * A manager which handles heads up notifications which is a special mode where
@@ -52,7 +51,7 @@
     private static final String TAG = "HeadsUpManager";
     private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms";
 
-    protected final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
+    protected final ListenerSet<OnHeadsUpChangedListener> mListeners = new ListenerSet<>();
 
     protected final Context mContext;
 
@@ -118,7 +117,7 @@
      * Adds an OnHeadUpChangedListener to observe events.
      */
     public void addListener(@NonNull OnHeadsUpChangedListener listener) {
-        mListeners.add(listener);
+        mListeners.addIfAbsent(listener);
     }
 
     /**
@@ -158,7 +157,7 @@
                         NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(),
                         entry.getSbn().getPackageName(), entry.getSbn().getInstanceId());
             }
-            for (OnHeadsUpChangedListener listener : new ArrayList<>(mListeners)) {
+            for (OnHeadsUpChangedListener listener : mListeners) {
                 if (isPinned) {
                     listener.onHeadsUpPinned(entry);
                 } else {
@@ -178,7 +177,7 @@
         entry.setHeadsUp(true);
         setEntryPinned((HeadsUpEntry) alertEntry, shouldHeadsUpBecomePinned(entry));
         EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 1 /* visible */);
-        for (OnHeadsUpChangedListener listener : new ArrayList<>(mListeners)) {
+        for (OnHeadsUpChangedListener listener : mListeners) {
             listener.onHeadsUpStateChanged(entry, true);
         }
     }
@@ -189,7 +188,7 @@
         entry.setHeadsUp(false);
         setEntryPinned((HeadsUpEntry) alertEntry, false /* isPinned */);
         EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */);
-        for (OnHeadsUpChangedListener listener : new ArrayList<>(mListeners)) {
+        for (OnHeadsUpChangedListener listener : mListeners) {
             listener.onHeadsUpStateChanged(entry, false);
         }
     }
@@ -207,7 +206,7 @@
         if (mHasPinnedNotification) {
             MetricsLogger.count(mContext, "note_peek", 1);
         }
-        for (OnHeadsUpChangedListener listener : new ArrayList<>(mListeners)) {
+        for (OnHeadsUpChangedListener listener : mListeners) {
             listener.onHeadsUpPinnedModeChanged(hasPinnedNotification);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java b/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java
index 6c35433..da4ccd0 100644
--- a/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java
@@ -63,8 +63,7 @@
         Intent intent = getIntent();
         Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT);
         if (!(targetParcelable instanceof Intent)) {
-            super.onCreate(savedInstanceState);
-            onStop(); // unregister receiver registered in onCreate (PackageMonitor)
+            super_onCreate(savedInstanceState);
             Log.w("UsbResolverActivity", "Target is not an intent: " + targetParcelable);
             finish();
             return;
@@ -97,8 +96,7 @@
         } else {
             mAccessory = (UsbAccessory)target.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
             if (mAccessory == null) {
-                super.onCreate(savedInstanceState);
-                onStop(); // unregister receiver registered in onCreate (PackageMonitor)
+                super_onCreate(savedInstanceState);
                 Log.e(TAG, "no device or accessory");
                 finish();
                 return;
diff --git a/packages/SystemUI/src/com/android/systemui/util/ListenerSet.kt b/packages/SystemUI/src/com/android/systemui/util/ListenerSet.kt
new file mode 100644
index 0000000..0f4193e9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/ListenerSet.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+import java.util.concurrent.CopyOnWriteArrayList
+
+/**
+ * A collection of listeners, observers, callbacks, etc.
+ *
+ * This container is optimized for infrequent mutation and frequent iteration, with thread safety
+ * and reentrant-safety guarantees as well.
+ */
+class ListenerSet<E> : Iterable<E> {
+    private val listeners: CopyOnWriteArrayList<E> = CopyOnWriteArrayList()
+
+    /**
+     * A thread-safe, reentrant-safe method to add a listener.
+     * Does nothing if the listener is already in the set.
+     */
+    fun addIfAbsent(element: E): Boolean = listeners.addIfAbsent(element)
+
+    /**
+     * A thread-safe, reentrant-safe method to remove a listener.
+     */
+    fun remove(element: E): Boolean = listeners.remove(element)
+
+    /**
+     * Returns an iterator over the listeners currently in the set.  Note that to ensure
+     * [ConcurrentModificationException] is never thrown, this iterator will not reflect changes
+     * made to the set after the iterator is constructed.
+     */
+    override fun iterator(): Iterator<E> = listeners.iterator()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
index 0ba072e..f97f4d0 100644
--- a/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
+++ b/packages/SystemUI/src/com/android/systemui/util/NotificationChannels.java
@@ -31,7 +31,6 @@
 
 public class NotificationChannels extends SystemUI {
     public static String ALERTS      = "ALR";
-    public static String SCREENSHOTS_LEGACY = "SCN";
     public static String SCREENSHOTS_HEADSUP = "SCN_HEADSUP";
     public static String GENERAL     = "GEN";
     public static String STORAGE     = "DSK";
@@ -84,18 +83,11 @@
                 general,
                 storage,
                 createScreenshotChannel(
-                        context.getString(R.string.notification_channel_screenshot),
-                        nm.getNotificationChannel(SCREENSHOTS_LEGACY)),
+                        context.getString(R.string.notification_channel_screenshot)),
                 batteryChannel,
                 hint
         ));
 
-        // Delete older SS channel if present.
-        // Screenshots promoted to heads-up in P, this cleans up the lower priority channel from O.
-        // This line can be deleted in Q.
-        nm.deleteNotificationChannel(SCREENSHOTS_LEGACY);
-
-
         if (isTv(context)) {
             // TV specific notification channel for TV PIP controls.
             // Importance should be {@link NotificationManager#IMPORTANCE_MAX} to have the highest
@@ -113,7 +105,7 @@
      * @return
      */
     @VisibleForTesting static NotificationChannel createScreenshotChannel(
-            String name, NotificationChannel legacySS) {
+            String name) {
         NotificationChannel screenshotChannel = new NotificationChannel(SCREENSHOTS_HEADSUP,
                 name, NotificationManager.IMPORTANCE_HIGH); // pop on screen
 
@@ -121,24 +113,6 @@
                 new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build());
         screenshotChannel.setBlockable(true);
 
-        if (legacySS != null) {
-            // Respect any user modified fields from the old channel.
-            int userlock = legacySS.getUserLockedFields();
-            if ((userlock & NotificationChannel.USER_LOCKED_IMPORTANCE) != 0) {
-                screenshotChannel.setImportance(legacySS.getImportance());
-            }
-            if ((userlock & NotificationChannel.USER_LOCKED_SOUND) != 0)  {
-                screenshotChannel.setSound(legacySS.getSound(), legacySS.getAudioAttributes());
-            }
-            if ((userlock & NotificationChannel.USER_LOCKED_VIBRATION) != 0)  {
-                screenshotChannel.setVibrationPattern(legacySS.getVibrationPattern());
-            }
-            if ((userlock & NotificationChannel.USER_LOCKED_LIGHTS) != 0)  {
-                screenshotChannel.setLightColor(legacySS.getLightColor());
-            }
-            // skip show_badge, irrelevant for system channel
-        }
-
         return screenshotChannel;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt b/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
index 2c01a70..db2aca8 100644
--- a/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
@@ -40,8 +40,8 @@
         this.wallpaperInfo = wallpaperInfo
     }
 
-    private val shouldUseDefaultDisplayStateChangeTransition: Boolean
-        get() = wallpaperInfo?.shouldUseDefaultDisplayStateChangeTransition()
+    private val shouldUseDefaultUnfoldTransition: Boolean
+        get() = wallpaperInfo?.shouldUseDefaultUnfoldTransition()
             ?: true
 
     fun setNotificationShadeZoom(zoomOut: Float) {
@@ -50,7 +50,7 @@
     }
 
     fun setUnfoldTransitionZoom(zoomOut: Float) {
-        if (shouldUseDefaultDisplayStateChangeTransition) {
+        if (shouldUseDefaultUnfoldTransition) {
             unfoldTransitionZoomOut = zoomOut
             updateZoom()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/PostureDependentProximitySensor.java b/packages/SystemUI/src/com/android/systemui/util/sensors/PostureDependentProximitySensor.java
new file mode 100644
index 0000000..40982bb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/PostureDependentProximitySensor.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.sensors;
+
+import android.util.Log;
+
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.policy.DevicePostureController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.concurrency.Execution;
+
+import javax.inject.Inject;
+
+/**
+ * Proximity sensor that changes proximity sensor usage based on the current posture.
+ * Posture -> prox sensor mapping can be found in SystemUI config overlays at:
+ *   - proximity_sensor_posture_mapping
+ *   - proximity_sensor_secondary_posture_mapping.
+ * where the array indices correspond to the following postures:
+ *     [UNKNOWN, CLOSED, HALF_OPENED, OPENED]
+ */
+class PostureDependentProximitySensor extends ProximitySensorImpl {
+    private final ThresholdSensor[] mPostureToPrimaryProxSensorMap;
+    private final ThresholdSensor[] mPostureToSecondaryProxSensorMap;
+
+    @Inject
+    PostureDependentProximitySensor(
+            @PrimaryProxSensor ThresholdSensor[] postureToPrimaryProxSensorMap,
+            @SecondaryProxSensor ThresholdSensor[] postureToSecondaryProxSensorMap,
+            @Main DelayableExecutor delayableExecutor,
+            Execution execution,
+            DevicePostureController devicePostureController
+    ) {
+        super(
+                postureToPrimaryProxSensorMap[0],
+                postureToSecondaryProxSensorMap[0],
+                delayableExecutor,
+                execution
+        );
+        mPostureToPrimaryProxSensorMap = postureToPrimaryProxSensorMap;
+        mPostureToSecondaryProxSensorMap = postureToSecondaryProxSensorMap;
+        mDevicePosture = devicePostureController.getDevicePosture();
+        devicePostureController.addCallback(mDevicePostureCallback);
+
+        chooseSensors();
+    }
+    private void chooseSensors() {
+        if (mDevicePosture >= mPostureToPrimaryProxSensorMap.length
+                || mDevicePosture >= mPostureToSecondaryProxSensorMap.length) {
+            Log.e("PostureDependentProxSensor",
+                    "unsupported devicePosture=" + mDevicePosture);
+            return;
+        }
+
+        ThresholdSensor newPrimaryProx = mPostureToPrimaryProxSensorMap[mDevicePosture];
+        ThresholdSensor newSecondaryProx = mPostureToSecondaryProxSensorMap[mDevicePosture];
+
+        if (newPrimaryProx != mPrimaryThresholdSensor
+                || newSecondaryProx != mSecondaryThresholdSensor) {
+            logDebug("Register new proximity sensors newPosture="
+                    + DevicePostureController.devicePostureToString(mDevicePosture));
+            unregisterInternal();
+
+            if (mPrimaryThresholdSensor != null) {
+                mPrimaryThresholdSensor.unregister(mPrimaryEventListener);
+            }
+            if (mSecondaryThresholdSensor != null) {
+                mSecondaryThresholdSensor.unregister(mSecondaryEventListener);
+            }
+
+            mPrimaryThresholdSensor = newPrimaryProx;
+            mSecondaryThresholdSensor = newSecondaryProx;
+
+            mInitializedListeners = false;
+            registerInternal();
+        }
+    }
+
+    private final DevicePostureController.Callback mDevicePostureCallback =
+            posture -> {
+                if (mDevicePosture == posture) {
+                    return;
+                }
+
+                mDevicePosture = posture;
+                chooseSensors();
+            };
+
+    @Override
+    public String toString() {
+        return String.format("{posture=%s, proximitySensor=%s}",
+                DevicePostureController.devicePostureToString(mDevicePosture), super.toString());
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximityCheck.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximityCheck.java
new file mode 100644
index 0000000..a8a6341
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximityCheck.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.sensors;
+
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * Convenience class allowing for briefly checking the proximity sensor.
+ */
+public class ProximityCheck implements Runnable {
+
+    private final ProximitySensor mSensor;
+    private final DelayableExecutor mDelayableExecutor;
+    private List<Consumer<Boolean>> mCallbacks = new ArrayList<>();
+    private final ThresholdSensor.Listener mListener;
+    private final AtomicBoolean mRegistered = new AtomicBoolean();
+
+    @Inject
+    public ProximityCheck(
+            ProximitySensor sensor,
+            @Main DelayableExecutor delayableExecutor) {
+        mSensor = sensor;
+        mSensor.setTag("prox_check");
+        mDelayableExecutor = delayableExecutor;
+        mListener = this::onProximityEvent;
+    }
+
+    /** Set a descriptive tag for the sensors registration. */
+    public void setTag(String tag) {
+        mSensor.setTag(tag);
+    }
+
+    @Override
+    public void run() {
+        unregister();
+        onProximityEvent(null);
+    }
+
+    /**
+     * Query the proximity sensor, timing out if no result.
+     */
+    public void check(long timeoutMs, Consumer<Boolean> callback) {
+        if (!mSensor.isLoaded()) {
+            callback.accept(null);
+            return;
+        }
+        mCallbacks.add(callback);
+        if (!mRegistered.getAndSet(true)) {
+            mSensor.register(mListener);
+            mDelayableExecutor.executeDelayed(this, timeoutMs);
+        }
+    }
+
+    private void unregister() {
+        mSensor.unregister(mListener);
+        mRegistered.set(false);
+    }
+
+    private void onProximityEvent(ThresholdSensorEvent proximityEvent) {
+        mCallbacks.forEach(
+                booleanConsumer ->
+                        booleanConsumer.accept(
+                                proximityEvent == null ? null : proximityEvent.getBelow()));
+        mCallbacks.clear();
+        unregister();
+        mRegistered.set(false);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java
index bd11039..d3f1c93 100644
--- a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,149 +16,24 @@
 
 package com.android.systemui.util.sensors;
 
-import android.hardware.SensorManager;
-import android.util.Log;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.concurrency.Execution;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
-
-import javax.inject.Inject;
-
 /**
- * Wrapper around SensorManager customized for the Proximity sensor.
- *
- * The ProximitySensor supports the concept of a primary and a
- * secondary hardware sensor. The primary sensor is used for a first
- * pass check if the phone covered. When triggered, it then checks
- * the secondary sensor for confirmation (if there is one). It does
- * not send a proximity event until the secondary sensor confirms (or
- * rejects) the reading. The secondary sensor is, in fact, the source
- * of truth.
- *
- * This is necessary as sometimes keeping the secondary sensor on for
- * extends periods is undesirable. It may, however, result in increased
- * latency for proximity readings.
- *
- * Phones should configure this via a config.xml overlay. If no
- * proximity sensor is set (primary or secondary) we fall back to the
- * default Sensor.TYPE_PROXIMITY. If proximity_sensor_type is set in
- * config.xml, that will be used as the primary sensor. If
- * proximity_sensor_secondary_type is set, that will function as the
- * secondary sensor. If no secondary is set, only the primary will be
- * used.
+ * Wrapper class for a dual ProximitySensor which supports a secondary sensor gated upon the
+ * primary).
  */
-public class ProximitySensor implements ThresholdSensor {
-    private static final String TAG = "ProxSensor";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-    private static final long SECONDARY_PING_INTERVAL_MS = 5000;
-
-    private final ThresholdSensor mPrimaryThresholdSensor;
-    private final ThresholdSensor mSecondaryThresholdSensor;
-    private final DelayableExecutor mDelayableExecutor;
-    private final Execution mExecution;
-    private final List<ThresholdSensor.Listener> mListeners = new ArrayList<>();
-    private String mTag = null;
-    @VisibleForTesting protected boolean mPaused;
-    private ThresholdSensorEvent mLastPrimaryEvent;
-    @VisibleForTesting
-    ThresholdSensorEvent mLastEvent;
-    private boolean mRegistered;
-    private final AtomicBoolean mAlerting = new AtomicBoolean();
-    private Runnable mCancelSecondaryRunnable;
-    private boolean mInitializedListeners = false;
-    private boolean mSecondarySafe = false;
-
-    private final ThresholdSensor.Listener mPrimaryEventListener = this::onPrimarySensorEvent;
-
-    private final ThresholdSensor.Listener mSecondaryEventListener =
-            new ThresholdSensor.Listener() {
-        @Override
-        public void onThresholdCrossed(ThresholdSensorEvent event) {
-            // If we no longer have a "below" signal and the secondary sensor is not
-            // considered "safe", then we need to turn it off.
-            if (!mSecondarySafe
-                    && (mLastPrimaryEvent == null
-                    || !mLastPrimaryEvent.getBelow()
-                    || !event.getBelow())) {
-                chooseSensor();
-                if (mLastPrimaryEvent == null || !mLastPrimaryEvent.getBelow()) {
-                    // Only check the secondary as long as the primary thinks we're near.
-                    if (mCancelSecondaryRunnable != null) {
-                        mCancelSecondaryRunnable.run();
-                        mCancelSecondaryRunnable = null;
-                    }
-                    return;
-                } else {
-                    // Check this sensor again in a moment.
-                    mCancelSecondaryRunnable = mDelayableExecutor.executeDelayed(() -> {
-                        // This is safe because we know that mSecondaryThresholdSensor
-                        // is loaded, otherwise we wouldn't be here.
-                        mPrimaryThresholdSensor.pause();
-                        mSecondaryThresholdSensor.resume();
-                    },
-                        SECONDARY_PING_INTERVAL_MS);
-                }
-            }
-            logDebug("Secondary sensor event: " + event.getBelow() + ".");
-
-            if (!mPaused) {
-                onSensorEvent(event);
-            }
-        }
-    };
-
-    @Inject
-    public ProximitySensor(
-            @PrimaryProxSensor ThresholdSensor primary,
-            @SecondaryProxSensor ThresholdSensor  secondary,
-            @Main DelayableExecutor delayableExecutor,
-            Execution execution) {
-        mPrimaryThresholdSensor = primary;
-        mSecondaryThresholdSensor = secondary;
-        mDelayableExecutor = delayableExecutor;
-        mExecution = execution;
-    }
-
-    @Override
-    public void setTag(String tag) {
-        mTag = tag;
-        mPrimaryThresholdSensor.setTag(tag + ":primary");
-        mSecondaryThresholdSensor.setTag(tag + ":secondary");
-    }
-
-    @Override
-    public void setDelay(int delay) {
-        mExecution.assertIsMainThread();
-        mPrimaryThresholdSensor.setDelay(delay);
-        mSecondaryThresholdSensor.setDelay(delay);
-    }
+public interface ProximitySensor extends ThresholdSensor {
+    /**
+     * Returns true if we are registered with the SensorManager.
+     */
+    boolean isRegistered();
 
     /**
-     * Unregister with the {@link SensorManager} without unsetting listeners on this object.
+     * Whether the proximity sensor reports near. Can return null if no information has been
+     * received yet.
      */
-    @Override
-    public void pause() {
-        mExecution.assertIsMainThread();
-        mPaused = true;
-        unregisterInternal();
-    }
+    Boolean isNear();
 
-    /**
-     * Register with the {@link SensorManager}. No-op if no listeners are registered on this object.
-     */
-    @Override
-    public void resume() {
-        mExecution.assertIsMainThread();
-        mPaused = false;
-        registerInternal();
-    }
+    /** Update all listeners with the last value this class received from the sensor. */
+    void alertListeners();
 
     /**
      * Sets that it is safe to leave the secondary sensor on indefinitely.
@@ -166,249 +41,5 @@
      * The secondary sensor will be turned on if there are any registered listeners, regardless
      * of what is reported by the primary sensor.
      */
-    public void setSecondarySafe(boolean safe) {
-        mSecondarySafe = mSecondaryThresholdSensor.isLoaded() && safe;
-        chooseSensor();
-    }
-
-    /**
-     * Returns true if we are registered with the SensorManager.
-     */
-    public boolean isRegistered() {
-        return mRegistered;
-    }
-
-    /**
-     * Returns {@code false} if a Proximity sensor is not available.
-     */
-    @Override
-    public boolean isLoaded() {
-        return mPrimaryThresholdSensor.isLoaded();
-    }
-
-    /**
-     * Add a listener.
-     *
-     * Registers itself with the {@link SensorManager} if this is the first listener
-     * added. If the ProximitySensor is paused, it will be registered when resumed.
-     */
-    @Override
-    public void register(ThresholdSensor.Listener listener) {
-        mExecution.assertIsMainThread();
-        if (!isLoaded()) {
-            return;
-        }
-
-        if (mListeners.contains(listener)) {
-            logDebug("ProxListener registered multiple times: " + listener);
-        } else {
-            mListeners.add(listener);
-        }
-        registerInternal();
-    }
-
-    protected void registerInternal() {
-        mExecution.assertIsMainThread();
-        if (mRegistered || mPaused || mListeners.isEmpty()) {
-            return;
-        }
-        if (!mInitializedListeners) {
-            mPrimaryThresholdSensor.pause();
-            mSecondaryThresholdSensor.pause();
-            mPrimaryThresholdSensor.register(mPrimaryEventListener);
-            mSecondaryThresholdSensor.register(mSecondaryEventListener);
-            mInitializedListeners = true;
-        }
-        logDebug("Registering sensor listener");
-
-        mRegistered = true;
-        chooseSensor();
-    }
-
-    private void chooseSensor() {
-        mExecution.assertIsMainThread();
-        if (!mRegistered || mPaused || mListeners.isEmpty()) {
-            return;
-        }
-        if (mSecondarySafe) {
-            mSecondaryThresholdSensor.resume();
-            mPrimaryThresholdSensor.pause();
-        } else {
-            mPrimaryThresholdSensor.resume();
-            mSecondaryThresholdSensor.pause();
-        }
-    }
-
-    /**
-     * Remove a listener.
-     *
-     * If all listeners are removed from an instance of this class,
-     * it will unregister itself with the SensorManager.
-     */
-    @Override
-    public void unregister(ThresholdSensor.Listener listener) {
-        mExecution.assertIsMainThread();
-        mListeners.remove(listener);
-        if (mListeners.size() == 0) {
-            unregisterInternal();
-        }
-    }
-
-    protected void unregisterInternal() {
-        mExecution.assertIsMainThread();
-        if (!mRegistered) {
-            return;
-        }
-        logDebug("unregistering sensor listener");
-        mPrimaryThresholdSensor.pause();
-        mSecondaryThresholdSensor.pause();
-        if (mCancelSecondaryRunnable != null) {
-            mCancelSecondaryRunnable.run();
-            mCancelSecondaryRunnable = null;
-        }
-        mLastPrimaryEvent = null;  // Forget what we know.
-        mLastEvent = null;
-        mRegistered = false;
-    }
-
-    public Boolean isNear() {
-        return isLoaded() && mLastEvent != null ? mLastEvent.getBelow() : null;
-    }
-
-    /** Update all listeners with the last value this class received from the sensor. */
-    public void alertListeners() {
-        mExecution.assertIsMainThread();
-        if (mAlerting.getAndSet(true)) {
-            return;
-        }
-        if (mLastEvent != null) {
-            ThresholdSensorEvent lastEvent = mLastEvent;  // Listeners can null out mLastEvent.
-            List<ThresholdSensor.Listener> listeners = new ArrayList<>(mListeners);
-            listeners.forEach(proximitySensorListener ->
-                    proximitySensorListener.onThresholdCrossed(lastEvent));
-        }
-
-        mAlerting.set(false);
-    }
-
-    private void onPrimarySensorEvent(ThresholdSensorEvent event) {
-        mExecution.assertIsMainThread();
-        if (mLastPrimaryEvent != null && event.getBelow() == mLastPrimaryEvent.getBelow()) {
-            return;
-        }
-
-        mLastPrimaryEvent = event;
-
-        if (mSecondarySafe && mSecondaryThresholdSensor.isLoaded()) {
-            logDebug("Primary sensor reported " + (event.getBelow() ? "near" : "far")
-                    + ". Checking secondary.");
-            if (mCancelSecondaryRunnable == null) {
-                mSecondaryThresholdSensor.resume();
-            }
-            return;
-        }
-
-
-        if (!mSecondaryThresholdSensor.isLoaded()) {  // No secondary
-            logDebug("Primary sensor event: " + event.getBelow() + ". No secondary.");
-            onSensorEvent(event);
-        } else if (event.getBelow()) {  // Covered? Check secondary.
-            logDebug("Primary sensor event: " + event.getBelow() + ". Checking secondary.");
-            if (mCancelSecondaryRunnable != null) {
-                mCancelSecondaryRunnable.run();
-            }
-            mSecondaryThresholdSensor.resume();
-        } else {  // Uncovered. Report immediately.
-            onSensorEvent(event);
-        }
-    }
-
-    private void onSensorEvent(ThresholdSensorEvent event) {
-        mExecution.assertIsMainThread();
-        if (mLastEvent != null && event.getBelow() == mLastEvent.getBelow()) {
-            return;
-        }
-
-        if (!mSecondarySafe && !event.getBelow()) {
-            chooseSensor();
-        }
-
-        mLastEvent = event;
-        alertListeners();
-    }
-
-    @Override
-    public String toString() {
-        return String.format("{registered=%s, paused=%s, near=%s, primarySensor=%s, "
-                + "secondarySensor=%s secondarySafe=%s}",
-                isRegistered(), mPaused, isNear(), mPrimaryThresholdSensor,
-                mSecondaryThresholdSensor, mSecondarySafe);
-    }
-
-    /**
-     * Convenience class allowing for briefly checking the proximity sensor.
-     */
-    public static class ProximityCheck implements Runnable {
-
-        private final ProximitySensor mSensor;
-        private final DelayableExecutor mDelayableExecutor;
-        private List<Consumer<Boolean>> mCallbacks = new ArrayList<>();
-        private final ThresholdSensor.Listener mListener;
-        private final AtomicBoolean mRegistered = new AtomicBoolean();
-
-        @Inject
-        public ProximityCheck(ProximitySensor sensor, @Main DelayableExecutor delayableExecutor) {
-            mSensor = sensor;
-            mSensor.setTag("prox_check");
-            mDelayableExecutor = delayableExecutor;
-            mListener = this::onProximityEvent;
-        }
-
-        /** Set a descriptive tag for the sensors registration. */
-        public void setTag(String tag) {
-            mSensor.setTag(tag);
-        }
-
-        @Override
-        public void run() {
-            unregister();
-            onProximityEvent(null);
-        }
-
-        /**
-         * Query the proximity sensor, timing out if no result.
-         */
-        public void check(long timeoutMs, Consumer<Boolean> callback) {
-            if (!mSensor.isLoaded()) {
-                callback.accept(null);
-                return;
-            }
-            mCallbacks.add(callback);
-            if (!mRegistered.getAndSet(true)) {
-                mSensor.register(mListener);
-                mDelayableExecutor.executeDelayed(this, timeoutMs);
-            }
-        }
-
-        private void unregister() {
-            mSensor.unregister(mListener);
-            mRegistered.set(false);
-        }
-
-        private void onProximityEvent(ThresholdSensorEvent proximityEvent) {
-            mCallbacks.forEach(
-                    booleanConsumer ->
-                            booleanConsumer.accept(
-                                    proximityEvent == null ? null : proximityEvent.getBelow()));
-            mCallbacks.clear();
-            unregister();
-            mRegistered.set(false);
-        }
-    }
-
-    private void logDebug(String msg) {
-        if (DEBUG) {
-            Log.d(TAG, (mTag != null ? "[" + mTag + "] " : "") + msg);
-        }
-    }
+    void setSecondarySafe(boolean safe);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensorImpl.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensorImpl.java
new file mode 100644
index 0000000..e639313a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ProximitySensorImpl.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.sensors;
+
+import android.hardware.SensorManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.policy.DevicePostureController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.concurrency.Execution;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.inject.Inject;
+
+/**
+ * Wrapper around SensorManager customized for the Proximity sensor.
+ *
+ * The ProximitySensor supports the concept of a primary and a
+ * secondary hardware sensor. The primary sensor is used for a first
+ * pass check if the phone covered. When triggered, it then checks
+ * the secondary sensor for confirmation (if there is one). It does
+ * not send a proximity event until the secondary sensor confirms (or
+ * rejects) the reading. The secondary sensor is, in fact, the source
+ * of truth.
+ *
+ * This is necessary as sometimes keeping the secondary sensor on for
+ * extends periods is undesirable. It may, however, result in increased
+ * latency for proximity readings.
+ *
+ * Phones should configure this via a config.xml overlay. If no
+ * proximity sensor is set (primary or secondary) we fall back to the
+ * default Sensor.TYPE_PROXIMITY. If proximity_sensor_type is set in
+ * config.xml, that will be used as the primary sensor. If
+ * proximity_sensor_secondary_type is set, that will function as the
+ * secondary sensor. If no secondary is set, only the primary will be
+ * used.
+ */
+class ProximitySensorImpl implements ProximitySensor {
+    private static final String TAG = "ProxSensor";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+    private static final long SECONDARY_PING_INTERVAL_MS = 5000;
+
+    ThresholdSensor mPrimaryThresholdSensor;
+    ThresholdSensor mSecondaryThresholdSensor;
+    private final DelayableExecutor mDelayableExecutor;
+    private final Execution mExecution;
+    private final List<ThresholdSensor.Listener> mListeners = new ArrayList<>();
+    private String mTag = null;
+    @VisibleForTesting protected boolean mPaused;
+    private ThresholdSensorEvent mLastPrimaryEvent;
+    @VisibleForTesting
+    ThresholdSensorEvent mLastEvent;
+    private boolean mRegistered;
+    private final AtomicBoolean mAlerting = new AtomicBoolean();
+    private Runnable mCancelSecondaryRunnable;
+    boolean mInitializedListeners = false;
+    private boolean mSecondarySafe = false; // safe to skip primary sensor check and use secondary
+    protected @DevicePostureController.DevicePostureInt int mDevicePosture;
+
+    final ThresholdSensor.Listener mPrimaryEventListener = this::onPrimarySensorEvent;
+
+    final ThresholdSensor.Listener mSecondaryEventListener =
+            new ThresholdSensor.Listener() {
+        @Override
+        public void onThresholdCrossed(ThresholdSensorEvent event) {
+            // If we no longer have a "below" signal and the secondary sensor is not
+            // considered "safe", then we need to turn it off.
+            if (!mSecondarySafe
+                    && (mLastPrimaryEvent == null
+                    || !mLastPrimaryEvent.getBelow()
+                    || !event.getBelow())) {
+                chooseSensor();
+                if (mLastPrimaryEvent == null || !mLastPrimaryEvent.getBelow()) {
+                    // Only check the secondary as long as the primary thinks we're near.
+                    if (mCancelSecondaryRunnable != null) {
+                        mCancelSecondaryRunnable.run();
+                        mCancelSecondaryRunnable = null;
+                    }
+                    return;
+                } else {
+                    // Check this sensor again in a moment.
+                    mCancelSecondaryRunnable = mDelayableExecutor.executeDelayed(() -> {
+                        // This is safe because we know that mSecondaryThresholdSensor
+                        // is loaded, otherwise we wouldn't be here.
+                        mPrimaryThresholdSensor.pause();
+                        mSecondaryThresholdSensor.resume();
+                    },
+                        SECONDARY_PING_INTERVAL_MS);
+                }
+            }
+            logDebug("Secondary sensor event: " + event.getBelow() + ".");
+
+            if (!mPaused) {
+                onSensorEvent(event);
+            }
+        }
+    };
+
+    @Inject
+    ProximitySensorImpl(
+            @PrimaryProxSensor ThresholdSensor primary,
+            @SecondaryProxSensor ThresholdSensor  secondary,
+            @Main DelayableExecutor delayableExecutor,
+            Execution execution) {
+        mPrimaryThresholdSensor = primary;
+        mSecondaryThresholdSensor = secondary;
+        mDelayableExecutor = delayableExecutor;
+        mExecution = execution;
+    }
+
+    @Override
+    public void setTag(String tag) {
+        mTag = tag;
+        mPrimaryThresholdSensor.setTag(tag + ":primary");
+        mSecondaryThresholdSensor.setTag(tag + ":secondary");
+    }
+
+    @Override
+    public void setDelay(int delay) {
+        mExecution.assertIsMainThread();
+        mPrimaryThresholdSensor.setDelay(delay);
+        mSecondaryThresholdSensor.setDelay(delay);
+    }
+
+    /**
+     * Unregister with the {@link SensorManager} without unsetting listeners on this object.
+     */
+    @Override
+    public void pause() {
+        mExecution.assertIsMainThread();
+        mPaused = true;
+        unregisterInternal();
+    }
+
+    /**
+     * Register with the {@link SensorManager}. No-op if no listeners are registered on this object.
+     */
+    @Override
+    public void resume() {
+        mExecution.assertIsMainThread();
+        mPaused = false;
+        registerInternal();
+    }
+
+    @Override
+    public void setSecondarySafe(boolean safe) {
+        mSecondarySafe = mSecondaryThresholdSensor.isLoaded() && safe;
+        chooseSensor();
+    }
+
+    /**
+     * Returns true if we are registered with the SensorManager.
+     */
+    @Override
+    public boolean isRegistered() {
+        return mRegistered;
+    }
+
+    /**
+     * Returns {@code false} if a Proximity sensor is not available.
+     */
+    @Override
+    public boolean isLoaded() {
+        return mPrimaryThresholdSensor.isLoaded();
+    }
+
+    /**
+     * Add a listener.
+     *
+     * Registers itself with the {@link SensorManager} if this is the first listener
+     * added. If the ProximitySensor is paused, it will be registered when resumed.
+     */
+    @Override
+    public void register(ThresholdSensor.Listener listener) {
+        mExecution.assertIsMainThread();
+        if (!isLoaded()) {
+            return;
+        }
+
+        if (mListeners.contains(listener)) {
+            logDebug("ProxListener registered multiple times: " + listener);
+        } else {
+            mListeners.add(listener);
+        }
+        registerInternal();
+    }
+
+    protected void registerInternal() {
+        mExecution.assertIsMainThread();
+        if (mRegistered || mPaused || mListeners.isEmpty()) {
+            return;
+        }
+        if (!mInitializedListeners) {
+            mPrimaryThresholdSensor.pause();
+            mSecondaryThresholdSensor.pause();
+            mPrimaryThresholdSensor.register(mPrimaryEventListener);
+            mSecondaryThresholdSensor.register(mSecondaryEventListener);
+            mInitializedListeners = true;
+        }
+
+        mRegistered = true;
+        chooseSensor();
+    }
+
+    private void chooseSensor() {
+        mExecution.assertIsMainThread();
+        if (!mRegistered || mPaused || mListeners.isEmpty()) {
+            return;
+        }
+        if (mSecondarySafe) {
+            mSecondaryThresholdSensor.resume();
+            mPrimaryThresholdSensor.pause();
+        } else {
+            mPrimaryThresholdSensor.resume();
+            mSecondaryThresholdSensor.pause();
+        }
+    }
+
+    /**
+     * Remove a listener.
+     *
+     * If all listeners are removed from an instance of this class,
+     * it will unregister itself with the SensorManager.
+     */
+    @Override
+    public void unregister(ThresholdSensor.Listener listener) {
+        mExecution.assertIsMainThread();
+        mListeners.remove(listener);
+        if (mListeners.size() == 0) {
+            unregisterInternal();
+        }
+    }
+
+    @Override
+    public String getName() {
+        return mPrimaryThresholdSensor.getName();
+    }
+
+    @Override
+    public String getType() {
+        return mPrimaryThresholdSensor.getType();
+    }
+
+    protected void unregisterInternal() {
+        mExecution.assertIsMainThread();
+        if (!mRegistered) {
+            return;
+        }
+        logDebug("unregistering sensor listener");
+        mPrimaryThresholdSensor.pause();
+        mSecondaryThresholdSensor.pause();
+        if (mCancelSecondaryRunnable != null) {
+            mCancelSecondaryRunnable.run();
+            mCancelSecondaryRunnable = null;
+        }
+        mLastPrimaryEvent = null;  // Forget what we know.
+        mLastEvent = null;
+        mRegistered = false;
+    }
+
+    @Override
+    public Boolean isNear() {
+        return isLoaded() && mLastEvent != null ? mLastEvent.getBelow() : null;
+    }
+
+    @Override
+    public void alertListeners() {
+        mExecution.assertIsMainThread();
+        if (mAlerting.getAndSet(true)) {
+            return;
+        }
+        if (mLastEvent != null) {
+            ThresholdSensorEvent lastEvent = mLastEvent;  // Listeners can null out mLastEvent.
+            List<ThresholdSensor.Listener> listeners = new ArrayList<>(mListeners);
+            listeners.forEach(proximitySensorListener ->
+                    proximitySensorListener.onThresholdCrossed(lastEvent));
+        }
+
+        mAlerting.set(false);
+    }
+
+    private void onPrimarySensorEvent(ThresholdSensorEvent event) {
+        mExecution.assertIsMainThread();
+        if (mLastPrimaryEvent != null && event.getBelow() == mLastPrimaryEvent.getBelow()) {
+            return;
+        }
+
+        mLastPrimaryEvent = event;
+
+        if (mSecondarySafe && mSecondaryThresholdSensor.isLoaded()) {
+            logDebug("Primary sensor reported " + (event.getBelow() ? "near" : "far")
+                    + ". Checking secondary.");
+            if (mCancelSecondaryRunnable == null) {
+                mSecondaryThresholdSensor.resume();
+            }
+            return;
+        }
+
+
+        if (!mSecondaryThresholdSensor.isLoaded()) {  // No secondary
+            logDebug("Primary sensor event: " + event.getBelow() + ". No secondary.");
+            onSensorEvent(event);
+        } else if (event.getBelow()) {  // Covered? Check secondary.
+            logDebug("Primary sensor event: " + event.getBelow() + ". Checking secondary.");
+            if (mCancelSecondaryRunnable != null) {
+                mCancelSecondaryRunnable.run();
+            }
+            mSecondaryThresholdSensor.resume();
+        } else {  // Uncovered. Report immediately.
+            onSensorEvent(event);
+        }
+    }
+
+    private void onSensorEvent(ThresholdSensorEvent event) {
+        mExecution.assertIsMainThread();
+        if (mLastEvent != null && event.getBelow() == mLastEvent.getBelow()) {
+            return;
+        }
+
+        if (!mSecondarySafe && !event.getBelow()) {
+            chooseSensor();
+        }
+
+        mLastEvent = event;
+        alertListeners();
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{registered=%s, paused=%s, near=%s, posture=%s, primarySensor=%s, "
+                + "secondarySensor=%s secondarySafe=%s}",
+                isRegistered(), mPaused, isNear(), mDevicePosture, mPrimaryThresholdSensor,
+                mSecondaryThresholdSensor, mSecondarySafe);
+    }
+
+    void logDebug(String msg) {
+        if (DEBUG) {
+            Log.d(TAG, (mTag != null ? "[" + mTag + "] " : "") + msg);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java b/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java
index 11e7df8..0be6068c 100644
--- a/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/SensorModule.java
@@ -16,11 +16,23 @@
 
 package com.android.systemui.util.sensors;
 
+import android.content.res.Resources;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
 
 import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.policy.DevicePostureController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
 
+import java.util.HashMap;
+import java.util.Map;
+
+import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 
@@ -31,8 +43,10 @@
 public class SensorModule {
     @Provides
     @PrimaryProxSensor
-    static ThresholdSensor providePrimaryProxSensor(SensorManager sensorManager,
-            ThresholdSensorImpl.Builder thresholdSensorBuilder) {
+    static ThresholdSensor providePrimaryProximitySensor(
+            SensorManager sensorManager,
+            ThresholdSensorImpl.Builder thresholdSensorBuilder
+    ) {
         try {
             return thresholdSensorBuilder
                     .setSensorDelay(SensorManager.SENSOR_DELAY_NORMAL)
@@ -52,8 +66,9 @@
 
     @Provides
     @SecondaryProxSensor
-    static ThresholdSensor provideSecondaryProxSensor(
-            ThresholdSensorImpl.Builder thresholdSensorBuilder) {
+    static ThresholdSensor provideSecondaryProximitySensor(
+            ThresholdSensorImpl.Builder thresholdSensorBuilder
+    ) {
         try {
             return thresholdSensorBuilder
                     .setSensorResourceId(R.string.proximity_sensor_secondary_type, true)
@@ -64,4 +79,153 @@
             return thresholdSensorBuilder.setSensor(null).setThresholdValue(0).build();
         }
     }
+
+    /**
+     * If postures are supported on the device, returns a posture dependent proximity sensor
+     * which switches proximity sensors based on the current posture.
+     *
+     * If postures are not supported the regular {@link ProximitySensorImpl} will be returned.
+     */
+    @Provides
+    static ProximitySensor provideProximitySensor(
+            @Main Resources resources,
+            Lazy<PostureDependentProximitySensor> postureDependentProximitySensorProvider,
+            Lazy<ProximitySensorImpl> proximitySensorProvider
+    ) {
+        if (hasPostureSupport(
+                resources.getStringArray(R.array.proximity_sensor_posture_mapping))) {
+            return postureDependentProximitySensorProvider.get();
+        } else  {
+            return proximitySensorProvider.get();
+        }
+    }
+
+    @Provides
+    static ProximityCheck provideProximityCheck(
+            ProximitySensor proximitySensor,
+            @Main DelayableExecutor delayableExecutor
+    ) {
+        return new ProximityCheck(
+                proximitySensor,
+                delayableExecutor
+        );
+    }
+
+    @Provides
+    @PrimaryProxSensor
+    @NonNull
+    static ThresholdSensor[] providePostureToProximitySensorMapping(
+            ThresholdSensorImpl.BuilderFactory thresholdSensorImplBuilderFactory,
+            @Main Resources resources
+    ) {
+        return createPostureToSensorMapping(
+                thresholdSensorImplBuilderFactory,
+                resources.getStringArray(R.array.proximity_sensor_posture_mapping),
+                R.dimen.proximity_sensor_threshold,
+                R.dimen.proximity_sensor_threshold_latch
+        );
+    }
+
+    @Provides
+    @SecondaryProxSensor
+    @NonNull
+    static ThresholdSensor[] providePostureToSecondaryProximitySensorMapping(
+            ThresholdSensorImpl.BuilderFactory thresholdSensorImplBuilderFactory,
+            @Main Resources resources
+    ) {
+        return createPostureToSensorMapping(
+                thresholdSensorImplBuilderFactory,
+                resources.getStringArray(R.array.proximity_sensor_secondary_posture_mapping),
+                R.dimen.proximity_sensor_secondary_threshold,
+                R.dimen.proximity_sensor_secondary_threshold_latch
+        );
+    }
+
+    /**
+     * Builds sensors to use per posture.
+     *
+     * @param sensorTypes an array where the index represents
+     *                    {@link DevicePostureController.DevicePostureInt} and the value
+     *                    at the given index is the sensorType. Empty values represent
+     *                    no sensor desired.
+     * @param proximitySensorThresholdResourceId resource id for the threshold for all sensor
+     *                                           postures. This currently only supports one value.
+     *                                           This needs to be updated in the future if postures
+     *                                           use different sensors with differing thresholds.
+     * @param proximitySensorThresholdLatchResourceId resource id for the latch for all sensor
+     *                                                postures. This currently only supports one
+     *                                                value. This needs to be updated in the future
+     *                                                if postures use different sensors with
+     *                                                differing latches.
+     * @return an array where the index represents the device posture
+     * {@link DevicePostureController.DevicePostureInt} and the value at the index is the sensor to
+     * use when the device is in that posture.
+     */
+    @NonNull
+    private static ThresholdSensor[] createPostureToSensorMapping(
+            ThresholdSensorImpl.BuilderFactory thresholdSensorImplBuilderFactory,
+            String[] sensorTypes,
+            int proximitySensorThresholdResourceId,
+            int proximitySensorThresholdLatchResourceId
+
+    ) {
+        ThresholdSensor noProxSensor = thresholdSensorImplBuilderFactory
+                .createBuilder()
+                .setSensor(null).setThresholdValue(0).build();
+
+
+        // length and index of sensorMap correspond to DevicePostureController.DevicePostureInt:
+        final ThresholdSensor[] sensorMap =
+                new ThresholdSensor[DevicePostureController.SUPPORTED_POSTURES_SIZE];
+        for (int i = 0; i < DevicePostureController.SUPPORTED_POSTURES_SIZE; i++) {
+            sensorMap[i] = noProxSensor;
+        }
+
+        if (!hasPostureSupport(sensorTypes)) {
+            Log.e("SensorModule", "config doesn't support postures,"
+                    + " but attempting to retrieve proxSensorMapping");
+            return sensorMap;
+        }
+
+        // Map of sensorType => Sensor, so we reuse the same sensor if it's the same between
+        // postures
+        Map<String, ThresholdSensor> typeToSensorMap = new HashMap<>();
+        for (int i = 0; i < sensorTypes.length; i++) {
+            try {
+                final String sensorType = sensorTypes[i];
+                if (typeToSensorMap.containsKey(sensorType)) {
+                    sensorMap[i] = typeToSensorMap.get(sensorType);
+                } else {
+                    sensorMap[i] = thresholdSensorImplBuilderFactory
+                            .createBuilder()
+                            .setSensorType(sensorTypes[i], true)
+                            .setThresholdResourceId(proximitySensorThresholdResourceId)
+                            .setThresholdLatchResourceId(proximitySensorThresholdLatchResourceId)
+                            .build();
+                    typeToSensorMap.put(sensorType, sensorMap[i]);
+                }
+            } catch (IllegalStateException e) {
+                // do nothing, sensor at this posture is already set to noProxSensor
+            }
+        }
+
+        return sensorMap;
+    }
+
+    /**
+     * Returns true if there's at least one non-empty sensor type in the given array.
+     */
+    private static boolean hasPostureSupport(String[] postureToSensorTypeMapping) {
+        if (postureToSensorTypeMapping == null || postureToSensorTypeMapping.length == 0) {
+            return false;
+        }
+
+        for (String sensorType : postureToSensorTypeMapping) {
+            if (!TextUtils.isEmpty(sensorType)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensor.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensor.java
index 363a734..d81a8d5 100644
--- a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensor.java
@@ -16,8 +16,6 @@
 
 package com.android.systemui.util.sensors;
 
-import java.util.Locale;
-
 /**
  * A wrapper class for sensors that have a boolean state - above/below.
  */
@@ -77,6 +75,16 @@
     void unregister(Listener listener);
 
     /**
+     * Name of the sensor.
+     */
+    String getName();
+
+    /**
+     * Type of the sensor.
+     */
+    String getType();
+
+    /**
      * Interface for listening to events on {@link ThresholdSensor}
      */
     interface Listener {
@@ -85,34 +93,4 @@
          */
         void onThresholdCrossed(ThresholdSensorEvent event);
     }
-
-    /**
-     * Returned when the below/above state of a {@link ThresholdSensor} changes.
-     */
-    class ThresholdSensorEvent {
-        private final boolean mBelow;
-        private final long mTimestampNs;
-
-        public ThresholdSensorEvent(boolean below, long timestampNs) {
-            mBelow = below;
-            mTimestampNs = timestampNs;
-        }
-
-        public boolean getBelow() {
-            return mBelow;
-        }
-
-        public long getTimestampNs() {
-            return mTimestampNs;
-        }
-
-        public long getTimestampMs() {
-            return mTimestampNs / 1000000;
-        }
-
-        @Override
-        public String toString() {
-            return String.format((Locale) null, "{near=%s, timestamp_ns=%d}", mBelow, mTimestampNs);
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorEvent.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorEvent.java
new file mode 100644
index 0000000..afce09f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorEvent.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.sensors;
+
+import java.util.Locale;
+
+/**
+ * Returned when the below/above state of a {@link ThresholdSensor} changes.
+ */
+public class ThresholdSensorEvent {
+    private final boolean mBelow;
+    private final long mTimestampNs;
+
+    public ThresholdSensorEvent(boolean below, long timestampNs) {
+        mBelow = below;
+        mTimestampNs = timestampNs;
+    }
+
+    public boolean getBelow() {
+        return mBelow;
+    }
+
+    public long getTimestampNs() {
+        return mTimestampNs;
+    }
+
+    public long getTimestampMs() {
+        return mTimestampNs / 1000000;
+    }
+
+    @Override
+    public String toString() {
+        return String.format((Locale) null, "{near=%s, timestamp_ns=%d}", mBelow, mTimestampNs);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java
index d10cf9b..a9086b1 100644
--- a/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/sensors/ThresholdSensorImpl.java
@@ -21,6 +21,7 @@
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
+import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -32,7 +33,10 @@
 
 import javax.inject.Inject;
 
-class ThresholdSensorImpl implements ThresholdSensor {
+/**
+ * Sensor that will only trigger beyond some lower and upper threshold.
+ */
+public class ThresholdSensorImpl implements ThresholdSensor {
     private static final String TAG = "ThresholdSensor";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
@@ -198,6 +202,15 @@
         alertListenersInternal(belowThreshold, timestampNs);
     }
 
+    @Override
+    public String getName() {
+        return mSensor != null ? mSensor.getName() : null;
+    }
+
+    @Override
+    public String getType() {
+        return mSensor != null ? mSensor.getStringType() : null;
+    }
 
     @Override
     public String toString() {
@@ -211,7 +224,12 @@
         }
     }
 
-    static class Builder {
+    /**
+     * Use to build a ThresholdSensor. Should only be used once per sensor built, since
+     * parameters are not reset after calls to build(). For ease of retrievingnew Builders, use
+     * {@link BuilderFactory}.
+     */
+    public static class Builder {
         private final Resources mResources;
         private final AsyncSensorManager mSensorManager;
         private final Execution mExecution;
@@ -318,7 +336,7 @@
 
         @VisibleForTesting
         Sensor findSensorByType(String sensorType, boolean requireWakeUp) {
-            if (sensorType.isEmpty()) {
+            if (TextUtils.isEmpty(sensorType)) {
                 return null;
             }
 
@@ -336,4 +354,29 @@
             return sensor;
         }
     }
+
+    /**
+     * Factory that creates a new ThresholdSensorImpl.Builder. In general, Builders should not be
+     * reused after creating a ThresholdSensor or else there may be default threshold and sensor
+     * values set from the previous built sensor.
+     */
+    public static class BuilderFactory {
+        private final Resources mResources;
+        private final AsyncSensorManager mSensorManager;
+        private final Execution mExecution;
+
+        @Inject
+        BuilderFactory(
+                @Main Resources resources,
+                AsyncSensorManager sensorManager,
+                Execution execution) {
+            mResources = resources;
+            mSensorManager = sensorManager;
+            mExecution = execution;
+        }
+
+        ThresholdSensorImpl.Builder createBuilder() {
+            return new Builder(mResources, mSensorManager, mExecution);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index db965db..c776ab9 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -212,7 +212,7 @@
             }
 
             @Override
-            public void onOverlayChanged() {
+            public void onThemeChanged() {
                 pip.onOverlayChanged();
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java
index ff26310..ff1929c 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java
@@ -26,6 +26,7 @@
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.statusbar.IStatusBarService;
+import com.android.launcher3.icons.IconProvider;
 import com.android.systemui.dagger.WMComponent;
 import com.android.systemui.dagger.WMSingleton;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
@@ -73,6 +74,7 @@
 import com.android.wm.shell.sizecompatui.SizeCompatUIController;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.splitscreen.StageTaskUnfoldController;
 import com.android.wm.shell.startingsurface.StartingSurface;
 import com.android.wm.shell.startingsurface.StartingWindowController;
 import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm;
@@ -81,10 +83,14 @@
 import com.android.wm.shell.transition.ShellTransitions;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
+import com.android.wm.shell.unfold.UnfoldBackgroundController;
 
 import java.util.Optional;
 
+import javax.inject.Provider;
+
 import dagger.BindsOptionalOf;
+import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
 
@@ -161,6 +167,12 @@
         return new SystemWindows(displayController, wmService);
     }
 
+    @WMSingleton
+    @Provides
+    static IconProvider provideIconProvider(Context context) {
+        return new IconProvider(context);
+    }
+
     // We currently dedupe multiple messages, so we use the shell main handler directly
     @WMSingleton
     @Provides
@@ -234,16 +246,48 @@
     static Optional<FullscreenUnfoldController> provideFullscreenUnfoldController(
             Context context,
             Optional<ShellUnfoldProgressProvider> progressProvider,
-            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+            Lazy<UnfoldBackgroundController> unfoldBackgroundController,
             DisplayInsetsController displayInsetsController,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
         return progressProvider.map(shellUnfoldTransitionProgressProvider ->
                 new FullscreenUnfoldController(context, mainExecutor,
-                        shellUnfoldTransitionProgressProvider, rootTaskDisplayAreaOrganizer,
+                        unfoldBackgroundController.get(), shellUnfoldTransitionProgressProvider,
                         displayInsetsController));
     }
 
+    @Provides
+    static Optional<StageTaskUnfoldController> provideStageTaskUnfoldController(
+            Optional<ShellUnfoldProgressProvider> progressProvider,
+            Context context,
+            TransactionPool transactionPool,
+            Lazy<UnfoldBackgroundController> unfoldBackgroundController,
+            DisplayInsetsController displayInsetsController,
+            @ShellMainThread ShellExecutor mainExecutor
+    ) {
+        return progressProvider.map(shellUnfoldTransitionProgressProvider ->
+                new StageTaskUnfoldController(
+                        context,
+                        transactionPool,
+                        shellUnfoldTransitionProgressProvider,
+                        displayInsetsController,
+                        unfoldBackgroundController.get(),
+                        mainExecutor
+                ));
+    }
+
+    @WMSingleton
+    @Provides
+    static UnfoldBackgroundController provideUnfoldBackgroundController(
+            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+            Context context
+    ) {
+        return new UnfoldBackgroundController(
+                context,
+                rootTaskDisplayAreaOrganizer
+        );
+    }
+
     //
     // Freeform (optional feature)
     //
@@ -401,12 +445,13 @@
             @ShellMainThread ShellExecutor mainExecutor,
             DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController, Transitions transitions,
-            TransactionPool transactionPool) {
+            TransactionPool transactionPool,
+            Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) {
         if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
             return Optional.of(new SplitScreenController(shellTaskOrganizer, syncQueue, context,
                     rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController,
                     displayInsetsController, transitions,
-                    transactionPool));
+                    transactionPool, stageTaskUnfoldControllerProvider));
         } else {
             return Optional.empty();
         }
@@ -448,9 +493,10 @@
     @Provides
     static StartingWindowController provideStartingWindowController(Context context,
             @ShellSplashscreenThread ShellExecutor splashScreenExecutor,
-            StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, TransactionPool pool) {
+            StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider,
+            TransactionPool pool) {
         return new StartingWindowController(context, splashScreenExecutor,
-                startingWindowTypeAlgorithm, pool);
+                startingWindowTypeAlgorithm, iconProvider, pool);
     }
 
     //
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 88b596c..fe0a2a4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -30,6 +29,7 @@
 import android.testing.AndroidTestingRunner;
 import android.view.View;
 import android.widget.FrameLayout;
+import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
 
 import androidx.test.filters.SmallTest;
@@ -108,7 +108,7 @@
     private final View mFakeSmartspaceView = new View(mContext);
 
     private KeyguardClockSwitchController mController;
-    private View mStatusArea;
+    private View mSliceView;
 
     @Before
     public void setup() {
@@ -142,14 +142,17 @@
                 mBypassController,
                 mSmartspaceController,
                 mKeyguardUnlockAnimationController,
-                mSmartSpaceTransitionController
+                mSmartSpaceTransitionController,
+                mResources
         );
 
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
         when(mColorExtractor.getColors(anyInt())).thenReturn(mGradientColors);
 
-        mStatusArea = new View(getContext());
-        when(mView.findViewById(R.id.keyguard_status_area)).thenReturn(mStatusArea);
+        mSliceView = new View(getContext());
+        when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView);
+        when(mView.findViewById(R.id.keyguard_status_area)).thenReturn(
+                new LinearLayout(getContext()));
     }
 
     @Test
@@ -214,7 +217,7 @@
         when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
         mController.init();
 
-        assertEquals(View.GONE, mStatusArea.getVisibility());
+        assertEquals(View.GONE, mSliceView.getVisibility());
     }
 
     @Test
@@ -222,22 +225,7 @@
         when(mSmartspaceController.isEnabled()).thenReturn(false);
         mController.init();
 
-        assertEquals(View.VISIBLE, mStatusArea.getVisibility());
-    }
-
-    @Test
-    public void testDetachDisconnectsSmartspace() {
-        when(mSmartspaceController.isEnabled()).thenReturn(true);
-        when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
-        mController.init();
-        verify(mView).addView(eq(mFakeSmartspaceView), anyInt(), any());
-
-        ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor =
-                ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
-        verify(mView).addOnAttachStateChangeListener(listenerArgumentCaptor.capture());
-
-        listenerArgumentCaptor.getValue().onViewDetachedFromWindow(mView);
-        verify(mSmartspaceController).disconnect();
+        assertEquals(View.VISIBLE, mSliceView.getVisibility());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java
index 1ab08c2..77302ce 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewTest.java
@@ -54,7 +54,7 @@
         MockitoAnnotations.initMocks(this);
         LayoutInflater layoutInflater = LayoutInflater.from(getContext());
         mKeyguardSliceView = (KeyguardSliceView) layoutInflater
-                .inflate(R.layout.keyguard_status_area, null);
+                .inflate(R.layout.keyguard_slice_view, null);
         mSliceUri = Uri.parse(KeyguardSliceProvider.KEYGUARD_SLICE_URI);
         SliceProvider.setSpecs(new HashSet<>(Collections.singletonList(SliceSpecs.LIST)));
     }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 0772b20..3edfd03 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -64,7 +64,6 @@
 import android.os.IRemoteCallback;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.os.Vibrator;
 import android.telephony.ServiceState;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
@@ -173,8 +172,6 @@
     private FeatureFlags mFeatureFlags;
     @Mock
     private InteractionJankMonitor mInteractionJankMonitor;
-    @Mock
-    private Vibrator mVibrator;
     @Captor
     private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateListenerCaptor;
     // Direct executor
@@ -242,8 +239,6 @@
 
         when(mRingerModeTracker.getRingerMode()).thenReturn(mRingerModeLiveData);
 
-        when(mFeatureFlags.isKeyguardLayoutEnabled()).thenReturn(false);
-
         mMockitoSession = ExtendedMockito.mockitoSession()
                 .spyStatic(SubscriptionManager.class).startMocking();
         ExtendedMockito.doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
@@ -1064,7 +1059,7 @@
                     mRingerModeTracker, mBackgroundExecutor,
                     mStatusBarStateController, mLockPatternUtils,
                     mAuthController, mTelephonyListenerManager, mFeatureFlags,
-                    mInteractionJankMonitor, mVibrator);
+                    mInteractionJankMonitor);
             setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
index 6237031..cdf40a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuControllerTest.java
@@ -41,6 +41,7 @@
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
 import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -80,6 +81,14 @@
         };
     }
 
+    @After
+    public void tearDown() {
+        if (mController != null) {
+            mController.onAccessibilityButtonTargetsChanged("");
+            mController = null;
+        }
+    }
+
     @Test
     public void initController_registerListeners() {
         mController = setUpController();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalStateControllerTest.java
index 7f85c35..42abff0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/CommunalStateControllerTest.java
@@ -48,13 +48,13 @@
     @Test
     public void testDefaultCommunalViewShowingState() {
         // The state controller should report the communal view as not showing by default.
-        final CommunalStateController stateController = new CommunalStateController();
+        final CommunalStateController stateController = new CommunalStateController(getContext());
         assertThat(stateController.getCommunalViewShowing()).isFalse();
     }
 
     @Test
     public void testNotifyCommunalSurfaceShow() {
-        final CommunalStateController stateController = new CommunalStateController();
+        final CommunalStateController stateController = new CommunalStateController(getContext());
         stateController.addCallback(mCallback);
 
         // Verify setting communal view to showing propagates to callback.
@@ -72,7 +72,7 @@
 
     @Test
     public void testCallbackRegistration() {
-        final CommunalStateController stateController = new CommunalStateController();
+        final CommunalStateController stateController = new CommunalStateController(getContext());
         stateController.addCallback(mCallback);
 
         // Verify setting communal view to showing propagates to callback.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt
new file mode 100644
index 0000000..87b9172
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.controls.ui
+
+import android.app.PendingIntent
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.wm.shell.TaskView
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class DetailDialogTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var taskView: TaskView
+    @Mock
+    private lateinit var controlViewHolder: ControlViewHolder
+    @Mock
+    private lateinit var pendingIntent: PendingIntent
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun testPendingIntentIsUnModified() {
+        // GIVEN the dialog is created with a PendingIntent
+        val dialog = createDialog(pendingIntent)
+
+        // WHEN the TaskView is initialized
+        dialog.stateCallback.onInitialized()
+
+        // THEN the PendingIntent used to call startActivity is unmodified by systemui
+        verify(taskView).startActivity(eq(pendingIntent), any(), any(), any())
+    }
+
+    private fun createDialog(pendingIntent: PendingIntent): DetailDialog {
+        return DetailDialog(
+            mContext,
+            taskView,
+            pendingIntent,
+            controlViewHolder
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index b688fcc..31fa3f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -53,7 +53,8 @@
 import com.android.systemui.util.sensors.FakeProximitySensor;
 import com.android.systemui.util.sensors.FakeSensorManager;
 import com.android.systemui.util.sensors.FakeThresholdSensor;
-import com.android.systemui.util.sensors.ProximitySensor;
+import com.android.systemui.util.sensors.ProximityCheck;
+import com.android.systemui.util.sensors.ThresholdSensorEvent;
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.util.wakelock.WakeLock;
@@ -80,7 +81,7 @@
     @Mock
     private DockManager mDockManager;
     @Mock
-    private ProximitySensor.ProximityCheck mProximityCheck;
+    private ProximityCheck mProximityCheck;
     @Mock
     private AuthController mAuthController;
     @Mock
@@ -136,14 +137,14 @@
         mTriggers.transitionTo(DozeMachine.State.INITIALIZED, DozeMachine.State.DOZE);
         clearInvocations(mMachine);
 
-        mProximitySensor.setLastEvent(new ProximitySensor.ThresholdSensorEvent(true, 1));
+        mProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 1));
         captor.getValue().onNotificationAlerted(null /* pulseSuppressedListener */);
         mProximitySensor.alertListeners();
 
         verify(mMachine, never()).requestState(any());
         verify(mMachine, never()).requestPulse(anyInt());
 
-        mProximitySensor.setLastEvent(new ProximitySensor.ThresholdSensorEvent(false, 2));
+        mProximitySensor.setLastEvent(new ThresholdSensorEvent(false, 2));
         mProximitySensor.alertListeners();
         waitForSensorManager();
         captor.getValue().onNotificationAlerted(null /* pulseSuppressedListener */);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagManagerTest.java
new file mode 100644
index 0000000..172dcda
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagManagerTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+public class FeatureFlagManagerTest extends SysuiTestCase {
+    FeatureFlagManager mFeatureFlagManager;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mFeatureFlagManager = new FeatureFlagManager();
+    }
+
+    @Test
+    public void testIsEnabled() {
+        mFeatureFlagManager.setEnabled(1, true);
+        // Again, nothing changes.
+        assertThat(mFeatureFlagManager.isEnabled(1, false)).isFalse();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/idle/AmbientLightModeMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/idle/AmbientLightModeMonitorTest.kt
new file mode 100644
index 0000000..9bd5bc7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/idle/AmbientLightModeMonitorTest.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.idle
+
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.sensors.AsyncSensorManager
+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.verify
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.any
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AmbientLightModeMonitorTest : SysuiTestCase() {
+    @Mock private lateinit var sensorManager: AsyncSensorManager
+    @Mock private lateinit var sensor: Sensor
+
+    private lateinit var ambientLightModeMonitor: AmbientLightModeMonitor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        `when`(sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)).thenReturn(sensor)
+        ambientLightModeMonitor = AmbientLightModeMonitor(sensorManager)
+    }
+
+    @Test
+    fun shouldTriggerCallbackImmediatelyOnStart() {
+        val callback = mock(AmbientLightModeMonitor.Callback::class.java)
+        ambientLightModeMonitor.start(callback)
+
+        // Monitor just started, should receive UNDECIDED.
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_UNDECIDED)
+
+        // Receives SensorEvent, and now mode is LIGHT.
+        val sensorEventListener = captureSensorEventListener()
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(15f))
+
+        // Stop monitor.
+        ambientLightModeMonitor.stop()
+
+        // Restart monitor.
+        reset(callback)
+        ambientLightModeMonitor.start(callback)
+
+        // Verify receiving current mode (LIGHT) immediately.
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT)
+    }
+
+    @Test
+    fun shouldReportDarkModeWhenSensorValueIsLessThanTen() {
+        val callback = mock(AmbientLightModeMonitor.Callback::class.java)
+        ambientLightModeMonitor.start(callback)
+
+        val sensorEventListener = captureSensorEventListener()
+        reset(callback)
+
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(0f))
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK)
+
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(1f))
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK)
+
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(5f))
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK)
+
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(9.9f))
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK)
+    }
+
+    @Test
+    fun shouldReportLightModeWhenSensorValueIsGreaterThanOrEqualToTen() {
+        val callback = mock(AmbientLightModeMonitor.Callback::class.java)
+        ambientLightModeMonitor.start(callback)
+
+        val sensorEventListener = captureSensorEventListener()
+        reset(callback)
+
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(10f))
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT)
+
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(10.1f))
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT)
+
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(15f))
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT)
+
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(100f))
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT)
+    }
+
+    @Test
+    fun shouldOnlyTriggerCallbackWhenValueChanges() {
+        val callback = mock(AmbientLightModeMonitor.Callback::class.java)
+        ambientLightModeMonitor.start(callback)
+
+        val sensorEventListener = captureSensorEventListener()
+
+        // Light mode, should trigger callback.
+        reset(callback)
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(20f))
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT)
+
+        // Light mode again, should NOT trigger callback.
+        reset(callback)
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(25f))
+        verify(callback, never()).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT)
+
+        // Dark mode, should trigger callback.
+        reset(callback)
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(2f))
+        verify(callback).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK)
+
+        // Dark mode again, should not trigger callback.
+        reset(callback)
+        sensorEventListener.onSensorChanged(sensorEventWithSingleValue(3f))
+        verify(callback, never()).onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK)
+    }
+
+    // Captures [SensorEventListener], assuming it has been registered with [sensorManager].
+    private fun captureSensorEventListener(): SensorEventListener {
+        val captor = ArgumentCaptor.forClass(SensorEventListener::class.java)
+        verify(sensorManager).registerListener(captor.capture(), any(), anyInt())
+        return captor.value
+    }
+
+    // Returns a [SensorEvent] with a single [value].
+    private fun sensorEventWithSingleValue(value: Float): SensorEvent {
+        return SensorEvent(sensor, 1, 1, FloatArray(1) { value })
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/idle/IdleHostViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/idle/IdleHostViewControllerTest.java
index a685e20..8ff33a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/idle/IdleHostViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/idle/IdleHostViewControllerTest.java
@@ -28,8 +28,6 @@
 import android.content.BroadcastReceiver;
 import android.content.Intent;
 import android.content.res.Resources;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
 import android.os.Looper;
 import android.os.PowerManager;
 import android.testing.AndroidTestingRunner;
@@ -46,7 +44,6 @@
 import com.android.systemui.shared.system.InputMonitorCompat;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.sensors.AsyncSensorManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -55,8 +52,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.time.Instant;
-
 import javax.inject.Provider;
 
 @SmallTest
@@ -64,7 +59,6 @@
 public class IdleHostViewControllerTest extends SysuiTestCase {
     @Mock private BroadcastDispatcher mBroadcastDispatcher;
     @Mock private PowerManager mPowerManager;
-    @Mock private AsyncSensorManager mSensorManager;
     @Mock private IdleHostView mIdleHostView;
     @Mock private InputMonitorFactory mInputMonitorFactory;
     @Mock private DelayableExecutor mDelayableExecutor;
@@ -74,12 +68,11 @@
     @Mock private Choreographer mChoreographer;
     @Mock private KeyguardStateController mKeyguardStateController;
     @Mock private StatusBarStateController mStatusBarStateController;
-    @Mock private Sensor mSensor;
     @Mock private DreamHelper mDreamHelper;
     @Mock private InputMonitorCompat mInputMonitor;
     @Mock private InputChannelCompat.InputEventReceiver mInputEventReceiver;
+    @Mock private AmbientLightModeMonitor mAmbientLightModeMonitor;
 
-    private final long mTimestamp = Instant.now().toEpochMilli();
     private KeyguardStateController.Callback mKeyguardStateCallback;
     private StatusBarStateController.StateListener mStatusBarStateListener;
     private IdleHostViewController mController;
@@ -90,15 +83,15 @@
 
         when(mResources.getBoolean(R.bool.config_enableIdleMode)).thenReturn(true);
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)).thenReturn(mSensor);
         when(mInputMonitorFactory.getInputMonitor("IdleHostViewController"))
                 .thenReturn(mInputMonitor);
         when(mInputMonitor.getInputReceiver(any(), any(), any())).thenReturn(mInputEventReceiver);
 
         mController = new IdleHostViewController(mContext,
-                mBroadcastDispatcher, mPowerManager, mSensorManager, mIdleHostView,
+                mBroadcastDispatcher, mPowerManager, mIdleHostView,
                 mInputMonitorFactory, mDelayableExecutor, mResources, mLooper, mViewProvider,
-                mChoreographer, mKeyguardStateController, mStatusBarStateController, mDreamHelper);
+                mChoreographer, mKeyguardStateController, mStatusBarStateController, mDreamHelper,
+                mAmbientLightModeMonitor);
         mController.init();
         mController.onViewAttached();
 
@@ -122,9 +115,9 @@
         mKeyguardStateCallback.onKeyguardShowingChanged();
 
         // Regular ambient lighting.
-        final SensorEvent sensorEvent = new SensorEvent(mSensor, 3, mTimestamp,
-                new float[]{90});
-        mController.onSensorChanged(sensorEvent);
+        final AmbientLightModeMonitor.Callback lightMonitorCallback =
+                captureAmbientLightModeMonitorCallback();
+        lightMonitorCallback.onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT);
 
         // Times out.
         ArgumentCaptor<Runnable> callbackCapture = ArgumentCaptor.forClass(Runnable.class);
@@ -149,9 +142,9 @@
         final BroadcastReceiver dreamBroadcastReceiver = dreamBroadcastReceiverCaptor.getValue();
 
         // Low ambient lighting.
-        final SensorEvent sensorEvent = new SensorEvent(mSensor, 3, mTimestamp,
-                new float[]{5});
-        mController.onSensorChanged(sensorEvent);
+        final AmbientLightModeMonitor.Callback lightMonitorCallback =
+                captureAmbientLightModeMonitorCallback();
+        lightMonitorCallback.onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK);
 
         // Verifies it goes to sleep because of low light.
         verify(mPowerManager).goToSleep(anyLong(), anyInt(), anyInt());
@@ -179,15 +172,15 @@
 
     @Test
     public void testTransitionBetweenIdleAndLowLightMode() {
-        // Regular ambient lighting.
-        final SensorEvent sensorEventRegularLight = new SensorEvent(mSensor, 3, mTimestamp,
-                new float[]{90});
-        mController.onSensorChanged(sensorEventRegularLight);
-
         // Keyguard showing.
         when(mKeyguardStateController.isShowing()).thenReturn(true);
         mKeyguardStateCallback.onKeyguardShowingChanged();
 
+        // Regular ambient lighting.
+        final AmbientLightModeMonitor.Callback lightMonitorCallback =
+                captureAmbientLightModeMonitorCallback();
+        lightMonitorCallback.onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT);
+
         // Times out.
         ArgumentCaptor<Runnable> callbackCapture = ArgumentCaptor.forClass(Runnable.class);
         verify(mDelayableExecutor).executeDelayed(callbackCapture.capture(), anyLong());
@@ -198,15 +191,13 @@
         clearInvocations(mDreamHelper);
 
         // Ambient lighting becomes dim.
-        final SensorEvent sensorEventLowLight = new SensorEvent(mSensor, 3, mTimestamp,
-                new float[]{5});
-        mController.onSensorChanged(sensorEventLowLight);
+        lightMonitorCallback.onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK);
 
         // Verifies in low light mode (dozing).
         verify(mPowerManager).goToSleep(anyLong(), anyInt(), anyInt());
 
         // Ambient lighting becomes bright again.
-        mController.onSensorChanged(sensorEventRegularLight);
+        lightMonitorCallback.onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT);
 
         // Verifies in idle mode (dreaming).
         verify(mDreamHelper).startDreaming(any());
@@ -214,22 +205,20 @@
 
     @Test
     public void testStartDozingWhenLowLight() {
-        // Regular ambient lighting.
-        final SensorEvent sensorEventRegularLight = new SensorEvent(mSensor, 3, mTimestamp,
-                new float[]{90});
-        mController.onSensorChanged(sensorEventRegularLight);
-
         // Keyguard showing.
         when(mKeyguardStateController.isShowing()).thenReturn(true);
         mKeyguardStateCallback.onKeyguardShowingChanged();
 
+        // Regular ambient lighting.
+        final AmbientLightModeMonitor.Callback lightMonitorCallback =
+                captureAmbientLightModeMonitorCallback();
+        lightMonitorCallback.onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_LIGHT);
+
         // Verifies it doesn't go to sleep yet.
         verify(mPowerManager, never()).goToSleep(anyLong(), anyInt(), anyInt());
 
         // Ambient lighting becomes dim.
-        final SensorEvent sensorEventLowLight = new SensorEvent(mSensor, 3, mTimestamp,
-                new float[]{5});
-        mController.onSensorChanged(sensorEventLowLight);
+        lightMonitorCallback.onChange(AmbientLightModeMonitor.AMBIENT_LIGHT_MODE_DARK);
 
         // Verifies it goes to sleep.
         verify(mPowerManager).goToSleep(anyLong(), anyInt(), anyInt());
@@ -251,4 +240,13 @@
         // Should dispose input event receiver.
         verify(mInputEventReceiver).dispose();
     }
+
+    // Captures [AmbientLightModeMonitor.Callback] assuming that the ambient light mode monitor
+    // has been started.
+    private AmbientLightModeMonitor.Callback captureAmbientLightModeMonitorCallback() {
+        ArgumentCaptor<AmbientLightModeMonitor.Callback> captor =
+                ArgumentCaptor.forClass(AmbientLightModeMonitor.Callback.class);
+        verify(mAmbientLightModeMonitor).start(captor.capture());
+        return captor.getValue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java
new file mode 100644
index 0000000..df11284
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/AnimatableClockControllerTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.Resources;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.keyguard.AnimatableClockController;
+import com.android.keyguard.AnimatableClockView;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.settingslib.Utils;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.policy.BatteryController;
+
+import org.junit.After;
+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;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class AnimatableClockControllerTest extends SysuiTestCase {
+    @Mock
+    private AnimatableClockView mClockView;
+    @Mock
+    private StatusBarStateController mStatusBarStateController;
+    @Mock
+    private BroadcastDispatcher mBroadcastDispatcher;
+    @Mock
+    private BatteryController mBatteryController;
+    @Mock
+    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Mock
+    private KeyguardBypassController mBypassController;
+    @Mock
+    private Resources mResources;
+
+    private MockitoSession mStaticMockSession;
+    private AnimatableClockController mAnimatableClockController;
+
+    // Capture listeners so that they can be used to send events
+    @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
+            ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+    private View.OnAttachStateChangeListener mAttachListener;
+
+    @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor;
+    private StatusBarStateController.StateListener mStatusBarStateCallback;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mStaticMockSession = mockitoSession()
+                .mockStatic(Utils.class)
+                .strictness(Strictness.LENIENT) // it's ok if mocked classes aren't used
+                .startMocking();
+        when(Utils.getColorAttrDefaultColor(anyObject(), anyInt())).thenReturn(0);
+
+        mAnimatableClockController = new AnimatableClockController(
+                mClockView,
+                mStatusBarStateController,
+                mBroadcastDispatcher,
+                mBatteryController,
+                mKeyguardUpdateMonitor,
+                mBypassController,
+                mResources
+        );
+        mAnimatableClockController.init();
+        captureAttachListener();
+    }
+
+    @After
+    public void tearDown() {
+        mStaticMockSession.finishMocking();
+    }
+
+    @Test
+    public void testOnAttachedUpdatesDozeStateToTrue() {
+        // GIVEN dozing
+        when(mStatusBarStateController.isDozing()).thenReturn(true);
+        when(mStatusBarStateController.getDozeAmount()).thenReturn(1f);
+
+        // WHEN the clock view gets attached
+        mAttachListener.onViewAttachedToWindow(mClockView);
+
+        // THEN the clock controller updated its dozing state to true
+        assertTrue(mAnimatableClockController.isDozing());
+    }
+
+    @Test
+    public void testOnAttachedUpdatesDozeStateToFalse() {
+        // GIVEN not dozing
+        when(mStatusBarStateController.isDozing()).thenReturn(false);
+        when(mStatusBarStateController.getDozeAmount()).thenReturn(0f);
+
+        // WHEN the clock view gets attached
+        mAttachListener.onViewAttachedToWindow(mClockView);
+
+        // THEN the clock controller updated its dozing state to false
+        assertFalse(mAnimatableClockController.isDozing());
+    }
+
+    private void captureAttachListener() {
+        verify(mClockView).addOnAttachStateChangeListener(mAttachCaptor.capture());
+        mAttachListener = mAttachCaptor.getValue();
+    }
+}
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 c464cad..90e3db7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
@@ -18,14 +18,19 @@
 
 import static junit.framework.Assert.assertEquals;
 
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.PointF;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Vibrator;
@@ -33,21 +38,25 @@
 import android.testing.TestableLooper;
 import android.util.DisplayMetrics;
 import android.util.Pair;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.keyguard.KeyguardViewController;
 import com.android.keyguard.LockIconView;
 import com.android.keyguard.LockIconViewController;
+import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.biometrics.AuthRippleController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -69,7 +78,10 @@
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class LockIconViewControllerTest extends SysuiTestCase {
+    private static final String UNLOCKED_LABEL = "unlocked";
+
     private @Mock LockIconView mLockIconView;
+    private @Mock AnimatedVectorDrawable mIconDrawable;
     private @Mock Context mContext;
     private @Mock Resources mResources;
     private @Mock DisplayMetrics mDisplayMetrics;
@@ -86,6 +98,7 @@
     private @Mock Vibrator mVibrator;
     private @Mock AuthRippleController mAuthRippleController;
     private @Mock LottieAnimationView mAodFp;
+    private @Mock LayoutInflater mLayoutInflater;
 
     private LockIconViewController mLockIconViewController;
 
@@ -97,6 +110,11 @@
     @Captor private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor;
     private AuthController.Callback mAuthControllerCallback;
 
+    @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback>
+            mKeyguardUpdateMonitorCallbackCaptor =
+            ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+    private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
+
     @Captor private ArgumentCaptor<PointF> mPointCaptor;
 
     @Before
@@ -105,9 +123,16 @@
 
         when(mLockIconView.getResources()).thenReturn(mResources);
         when(mLockIconView.getContext()).thenReturn(mContext);
+        when(mLockIconView.findViewById(R.layout.udfps_aod_lock_icon)).thenReturn(mAodFp);
         when(mContext.getResources()).thenReturn(mResources);
         when(mResources.getDisplayMetrics()).thenReturn(mDisplayMetrics);
-        when(mLockIconView.findViewById(anyInt())).thenReturn(mAodFp);
+        when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL);
+        when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable);
+
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+        when(mStatusBarStateController.isDozing()).thenReturn(false);
+        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
 
         mLockIconViewController = new LockIconViewController(
                 mLockIconView,
@@ -122,11 +147,42 @@
                 mConfigurationController,
                 mDelayableExecutor,
                 mVibrator,
-                mAuthRippleController
+                mAuthRippleController,
+                mResources,
+                mLayoutInflater
         );
     }
 
     @Test
+    public void testIgnoreUdfpsWhenNotSupported() {
+        // GIVEN Udpfs sensor is NOT available
+        mLockIconViewController.init();
+        captureAttachListener();
+
+        // WHEN the view is attached
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+
+        // THEN lottie animation should NOT be inflated
+        verify(mLayoutInflater, never()).inflate(eq(R.layout.udfps_aod_lock_icon), any());
+    }
+
+    @Test
+    public void testInflateUdfpsWhenSupported() {
+        // GIVEN Udpfs sensor is available
+        setupUdfps();
+        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
+
+        mLockIconViewController.init();
+        captureAttachListener();
+
+        // WHEN the view is attached
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+
+        // THEN lottie animation should be inflated
+        verify(mLayoutInflater).inflate(eq(R.layout.udfps_aod_lock_icon), any());
+    }
+
+    @Test
     public void testUpdateFingerprintLocationOnInit() {
         // GIVEN fp sensor location is available pre-attached
         Pair<Integer, PointF> udfps = setupUdfps();
@@ -192,6 +248,29 @@
         verify(mLockIconView).setUseBackground(false);
     }
 
+    @Test
+    public void testUnlockIconShows_biometricUnlockedTrue() {
+        // GIVEN UDFPS sensor location is available
+        setupUdfps();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        mLockIconViewController.init();
+        captureAttachListener();
+        mAttachListener.onViewAttachedToWindow(mLockIconView);
+        captureKeyguardUpdateMonitorCallback();
+
+        // GIVEN user has unlocked with a biometric auth (ie: face auth)
+        when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true);
+        reset(mLockIconView);
+
+        // WHEN face auth's biometric running state changes
+        mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false,
+                BiometricSourceType.FACE);
+
+        // THEN the unlock icon is shown
+        verify(mLockIconView).setContentDescription(UNLOCKED_LABEL);
+    }
+
     private Pair<Integer, PointF> setupUdfps() {
         final PointF udfpsLocation = new PointF(50, 75);
         final int radius = 33;
@@ -220,4 +299,10 @@
         verify(mLockIconView).addOnAttachStateChangeListener(mAttachCaptor.capture());
         mAttachListener = mAttachCaptor.getValue();
     }
+
+    private void captureKeyguardUpdateMonitorCallback() {
+        verify(mKeyguardUpdateMonitor).registerCallback(
+                mKeyguardUpdateMonitorCallbackCaptor.capture());
+        mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
new file mode 100644
index 0000000..175ec87f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import javax.inject.Provider
+
+private val DATA = MediaData(
+    userId = -1,
+    initialized = false,
+    backgroundColor = 0,
+    app = null,
+    appIcon = null,
+    artist = null,
+    song = null,
+    artwork = null,
+    actions = emptyList(),
+    actionsToShowInCompact = emptyList(),
+    packageName = "INVALID",
+    token = null,
+    clickIntent = null,
+    device = null,
+    active = true,
+    resumeAction = null)
+
+private val SMARTSPACE_KEY = "smartspace"
+
+@SmallTest
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class MediaCarouselControllerTest : SysuiTestCase() {
+
+    @Mock lateinit var mediaControlPanelFactory: Provider<MediaControlPanel>
+    @Mock lateinit var panel: MediaControlPanel
+    @Mock lateinit var visualStabilityManager: VisualStabilityManager
+    @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
+    @Mock lateinit var activityStarter: ActivityStarter
+    @Mock @Main private lateinit var executor: DelayableExecutor
+    @Mock lateinit var mediaDataManager: MediaDataManager
+    @Mock lateinit var configurationController: ConfigurationController
+    @Mock lateinit var falsingCollector: FalsingCollector
+    @Mock lateinit var falsingManager: FalsingManager
+    @Mock lateinit var dumpManager: DumpManager
+
+    private val clock = FakeSystemClock()
+    private lateinit var mediaCarouselController: MediaCarouselController
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        mediaCarouselController = MediaCarouselController(
+            context,
+            mediaControlPanelFactory,
+            visualStabilityManager,
+            mediaHostStatesManager,
+            activityStarter,
+            clock,
+            executor,
+            mediaDataManager,
+            configurationController,
+            falsingCollector,
+            falsingManager,
+            dumpManager
+        )
+
+        MediaPlayerData.clear()
+    }
+
+    @Test
+    fun testPlayerOrdering() {
+        // Test values: key, data, last active time
+        val playingLocal = Triple("playing local",
+            DATA.copy(active = true, isPlaying = true, isLocalSession = true, resumption = false),
+            4500L)
+
+        val playingRemote = Triple("playing remote",
+            DATA.copy(active = true, isPlaying = true, isLocalSession = false, resumption = false),
+            5000L)
+
+        val pausedLocal = Triple("paused local",
+            DATA.copy(active = true, isPlaying = false, isLocalSession = true, resumption = false),
+            1000L)
+
+        val pausedRemote = Triple("paused remote",
+            DATA.copy(active = true, isPlaying = false, isLocalSession = false, resumption = false),
+            2000L)
+
+        val resume1 = Triple("resume 1",
+            DATA.copy(active = false, isPlaying = false, isLocalSession = true, resumption = true),
+            500L)
+
+        val resume2 = Triple("resume 2",
+            DATA.copy(active = false, isPlaying = false, isLocalSession = true, resumption = true),
+            1000L)
+
+        // Expected ordering for media players:
+        // Actively playing local sessions
+        // Actively playing remote sessions
+        // Paused sessions, by last active
+        // Resume controls, by last active
+
+        val expected = listOf(playingLocal, playingRemote, pausedRemote, pausedLocal, resume2,
+            resume1)
+
+        expected.forEach {
+            clock.setCurrentTimeMillis(it.third)
+            MediaPlayerData.addMediaPlayer(it.first, it.second.copy(notificationKey = it.first),
+                panel, clock)
+        }
+
+        for ((index, key) in MediaPlayerData.playerKeys().withIndex()) {
+            assertEquals(expected.get(index).first, key.data.notificationKey)
+        }
+    }
+
+    @Test
+    fun testOrderWithSmartspace_prioritized() {
+        testPlayerOrdering()
+
+        // If smartspace is prioritized
+        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
+            true, clock)
+
+        // Then it should be shown immediately after any actively playing controls
+        assertTrue(MediaPlayerData.playerKeys().elementAt(2).isSsMediaRec)
+    }
+
+    @Test
+    fun testOrderWithSmartspace_notPrioritized() {
+        testPlayerOrdering()
+
+        // If smartspace is not prioritized
+        MediaPlayerData.addMediaRecommendation(SMARTSPACE_KEY, EMPTY_SMARTSPACE_MEDIA_DATA, panel,
+            false, clock)
+
+        // Then it should be shown at the end of the carousel
+        val size = MediaPlayerData.playerKeys().size
+        assertTrue(MediaPlayerData.playerKeys().elementAt(size - 1).isSsMediaRec)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index 42629f5..bf5a6e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.dialog.MediaOutputDialogFactory
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil
 import com.android.systemui.util.animation.TransitionLayout
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -95,6 +96,7 @@
     @Mock private lateinit var collapsedSet: ConstraintSet
     @Mock private lateinit var mediaOutputDialogFactory: MediaOutputDialogFactory
     @Mock private lateinit var mediaCarouselController: MediaCarouselController
+    @Mock private lateinit var falsingManager: FalsingManager
     private lateinit var appIcon: ImageView
     private lateinit var albumView: ImageView
     private lateinit var titleText: TextView
@@ -131,8 +133,8 @@
         whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
 
         player = MediaControlPanel(context, bgExecutor, activityStarter, mediaViewController,
-                seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil,
-                mediaOutputDialogFactory, mediaCarouselController)
+            seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil,
+            mediaOutputDialogFactory, mediaCarouselController, falsingManager)
         whenever(seekBarViewModel.progress).thenReturn(seekBarData)
 
         // Mock out a view holder for the player to attach to.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
index 28aed20..a435e79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
@@ -86,6 +86,7 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        MediaPlayerData.clear()
         mediaDataFilter = MediaDataFilter(context, broadcastDispatcher, mediaResumeListener,
                 lockscreenUserManager, executor, clock)
         mediaDataFilter.mediaDataManager = mediaDataManager
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 d864aae..2b2fc51 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -255,6 +255,7 @@
             .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true),
                     eq(false))
         assertThat(mediaDataCaptor.value.resumption).isTrue()
+        assertThat(mediaDataCaptor.value.isPlaying).isFalse()
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
index e9e965e..8dc9eff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaPlayerDataTest.kt
@@ -127,7 +127,7 @@
         val players = MediaPlayerData.players()
         assertThat(players).hasSize(6)
         assertThat(players).containsExactly(playerIsPlaying, playerIsPlayingAndRemote,
-            playerIsStoppedAndLocal, playerCanResume, playerIsStoppedAndRemote,
+            playerIsStoppedAndRemote, playerIsStoppedAndLocal, playerCanResume,
             playerUndetermined).inOrder()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
index 7d8728e..e77802f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarObserverTest.kt
@@ -76,6 +76,7 @@
         assertThat(seekBarView.getThumb().getAlpha()).isEqualTo(0)
         assertThat(elapsedTimeView.getText()).isEqualTo("")
         assertThat(totalTimeView.getText()).isEqualTo("")
+        assertThat(seekBarView.contentDescription).isEqualTo("")
         assertThat(seekBarView.maxHeight).isEqualTo(disabledHeight)
     }
 
@@ -102,6 +103,9 @@
         assertThat(seekBarView.max).isEqualTo(120000)
         assertThat(elapsedTimeView.getText()).isEqualTo("00:03")
         assertThat(totalTimeView.getText()).isEqualTo("02:00")
+
+        val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00")
+        assertThat(seekBarView.contentDescription).isEqualTo(desc)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 25ca8c9..750600ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -119,7 +119,6 @@
         assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
         assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
@@ -181,7 +180,6 @@
         when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(mMediaDevices);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.VISIBLE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.VISIBLE);
     }
 
@@ -190,7 +188,6 @@
         when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(new ArrayList<>());
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
 
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
     }
 
@@ -198,7 +195,6 @@
     public void onBindViewHolder_bindNonActiveConnectedDevice_verifyView() {
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
@@ -215,7 +211,6 @@
         when(mMediaDevice2.isConnected()).thenReturn(false);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
@@ -231,7 +226,6 @@
                 LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED);
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
-        assertThat(mViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
index 1f85112..ca5d570 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupAdapterTest.java
@@ -99,7 +99,6 @@
         assertThat(mGroupViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.VISIBLE);
         assertThat(mGroupViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
@@ -114,7 +113,6 @@
 
         assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
@@ -141,7 +139,6 @@
 
         assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
@@ -167,7 +164,6 @@
 
         assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
@@ -186,7 +182,6 @@
 
         assertThat(mGroupViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mGroupViewHolder.mDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mAddIcon.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mBottomDivider.getVisibility()).isEqualTo(View.GONE);
         assertThat(mGroupViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
index c1a9739..4fc329f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
@@ -35,42 +35,24 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 import android.util.SparseArray;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
-import com.android.systemui.accessibility.SystemActions;
-import com.android.systemui.assist.AssistManager;
-import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.recents.OverviewProxyService;
-import com.android.systemui.recents.Recents;
-import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.NotificationShadeDepthController;
-import com.android.systemui.statusbar.phone.ShadeController;
-import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
+import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
-import com.android.wm.shell.pip.Pip;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-
-import java.util.Optional;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 /** atest NavigationBarControllerTest */
 @RunWith(AndroidTestingRunner.class)
@@ -78,47 +60,33 @@
 @SmallTest
 public class NavigationBarControllerTest extends SysuiTestCase {
 
+    private static final int SECONDARY_DISPLAY = 1;
+
     private NavigationBarController mNavigationBarController;
     private NavigationBar mDefaultNavBar;
     private NavigationBar mSecondaryNavBar;
 
-    private CommandQueue mCommandQueue = mock(CommandQueue.class);
-
-    private static final int SECONDARY_DISPLAY = 1;
+    @Mock
+    private CommandQueue mCommandQueue;
+    @Mock
+    private NavigationBar.Factory mNavigationBarFactory;
 
     @Before
     public void setUp() {
+        MockitoAnnotations.initMocks(this);
         mNavigationBarController = spy(
                 new NavigationBarController(mContext,
-                        mock(WindowManager.class),
-                        () -> mock(AssistManager.class),
-                        mock(AccessibilityManager.class),
-                        mock(AccessibilityManagerWrapper.class),
-                        mock(DeviceProvisionedController.class),
-                        mock(MetricsLogger.class),
                         mock(OverviewProxyService.class),
                         mock(NavigationModeController.class),
-                        mock(AccessibilityButtonModeObserver.class),
-                        mock(StatusBarStateController.class),
                         mock(SysUiState.class),
-                        mock(BroadcastDispatcher.class),
                         mCommandQueue,
-                        Optional.of(mock(Pip.class)),
-                        Optional.of(mock(LegacySplitScreen.class)),
-                        Optional.of(mock(Recents.class)),
-                        () -> Optional.of(mock(StatusBar.class)),
-                        mock(ShadeController.class),
-                        mock(NotificationRemoteInputManager.class),
-                        mock(NotificationShadeDepthController.class),
-                        mock(SystemActions.class),
                         Dependency.get(Dependency.MAIN_HANDLER),
-                        mock(UiEventLogger.class),
-                        mock(NavigationBarOverlayController.class),
                         mock(ConfigurationController.class),
                         mock(NavigationBarA11yHelper.class),
                         mock(TaskbarDelegate.class),
-                        mock(UserTracker.class),
-                        mock(DumpManager.class)));
+                        mNavigationBarFactory,
+                        mock(DumpManager.class),
+                        mock(AutoHideController.class)));
         initializeNavigationBars();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index e37f422..223ffbd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -19,6 +19,7 @@
 import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
 import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
 import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
+import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
 import static android.inputmethodservice.InputMethodService.IME_VISIBLE;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
@@ -28,7 +29,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -45,11 +45,11 @@
 import android.content.IntentFilter;
 import android.hardware.display.DisplayManagerGlobal;
 import android.os.Handler;
-import android.os.Looper;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
+import android.telecom.TelecomManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
@@ -60,12 +60,12 @@
 import android.view.WindowManager;
 import android.view.WindowMetrics;
 import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.InputMethodManager;
 
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.SysuiTestableContext;
 import com.android.systemui.accessibility.AccessibilityButtonModeObserver;
@@ -81,9 +81,10 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.phone.AutoHideController;
+import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
@@ -110,7 +111,13 @@
     private NavigationBar mExternalDisplayNavigationBar;
 
     private SysuiTestableContext mSysuiTestableContextExternal;
+    @Mock
     private OverviewProxyService mOverviewProxyService;
+    @Mock
+    private StatusBarStateController mStatusBarStateController;
+    @Mock
+    private NavigationModeController mNavigationModeController;
+    @Mock
     private CommandQueue mCommandQueue;
     private SysUiState mMockSysUiState;
     @Mock
@@ -125,11 +132,25 @@
     EdgeBackGestureHandler mEdgeBackGestureHandler;
     @Mock
     NavigationBarA11yHelper mNavigationBarA11yHelper;
+    @Mock
+    private LightBarController mLightBarController;
+    @Mock
+    private LightBarController.Factory mLightBarcontrollerFactory;
+    @Mock
+    private AutoHideController mAutoHideController;
+    @Mock
+    private AutoHideController.Factory mAutoHideControllerFactory;
+    @Mock
+    private WindowManager mWindowManager;
+    @Mock
+    private TelecomManager mTelecomManager;
+    @Mock
+    private InputMethodManager mInputMethodManager;
+    @Mock
+    private AssistManager mAssistManager;
 
     @Rule
     public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck();
-    private AccessibilityManagerWrapper mAccessibilityWrapper =
-            new AccessibilityManagerWrapper(mContext);
 
     @Before
     public void setup() throws Exception {
@@ -137,15 +158,19 @@
 
         when(mEdgeBackGestureHandlerFactory.create(any(Context.class)))
                 .thenReturn(mEdgeBackGestureHandler);
-        mCommandQueue = new CommandQueue(mContext);
+        when(mLightBarcontrollerFactory.create(any(Context.class))).thenReturn(mLightBarController);
+        when(mAutoHideControllerFactory.create(any(Context.class))).thenReturn(mAutoHideController);
         setupSysuiDependency();
-        mDependency.injectMockDependency(AssistManager.class);
+        // This class inflates views that call Dependency.get, thus these injections are still
+        // necessary.
+        mDependency.injectTestDependency(AssistManager.class, mAssistManager);
         mDependency.injectMockDependency(KeyguardStateController.class);
-        mDependency.injectMockDependency(StatusBarStateController.class);
+        mDependency.injectTestDependency(StatusBarStateController.class, mStatusBarStateController);
         mDependency.injectMockDependency(NavigationBarController.class);
-        mOverviewProxyService = mDependency.injectMockDependency(OverviewProxyService.class);
         mDependency.injectTestDependency(EdgeBackGestureHandler.Factory.class,
                 mEdgeBackGestureHandlerFactory);
+        mDependency.injectTestDependency(OverviewProxyService.class, mOverviewProxyService);
+        mDependency.injectTestDependency(NavigationModeController.class, mNavigationModeController);
         TestableLooper.get(this).runWithLooper(() -> {
             mNavigationBar = createNavBar(mContext);
             mExternalDisplayNavigationBar = createNavBar(mSysuiTestableContextExternal);
@@ -164,25 +189,21 @@
         mSysuiTestableContextExternal = (SysuiTestableContext) getContext().createDisplayContext(
                 display);
 
-        WindowManager windowManager = mock(WindowManager.class);
-        Display defaultDisplay = mContext.getSystemService(WindowManager.class).getDefaultDisplay();
-        when(windowManager.getDefaultDisplay()).thenReturn(
-                defaultDisplay);
-        WindowMetrics maximumWindowMetrics = mContext.getSystemService(WindowManager.class)
+        Display defaultDisplay = mContext.getDisplay();
+        when(mWindowManager.getDefaultDisplay()).thenReturn(defaultDisplay);
+        WindowMetrics metrics = mContext.getSystemService(WindowManager.class)
                 .getMaximumWindowMetrics();
-        when(windowManager.getMaximumWindowMetrics()).thenReturn(maximumWindowMetrics);
+        when(mWindowManager.getMaximumWindowMetrics()).thenReturn(metrics);
         WindowMetrics currentWindowMetrics = mContext.getSystemService(WindowManager.class)
                 .getCurrentWindowMetrics();
-        when(windowManager.getCurrentWindowMetrics()).thenReturn(currentWindowMetrics);
-        doNothing().when(windowManager).addView(any(), any());
-        mContext.addMockSystemService(Context.WINDOW_SERVICE, windowManager);
-        mSysuiTestableContextExternal.addMockSystemService(Context.WINDOW_SERVICE, windowManager);
-
-        mDependency.injectTestDependency(Dependency.BG_LOOPER, Looper.getMainLooper());
-        mDependency.injectTestDependency(AccessibilityManagerWrapper.class, mAccessibilityWrapper);
-
+        when(mWindowManager.getCurrentWindowMetrics()).thenReturn(currentWindowMetrics);
+        doNothing().when(mWindowManager).addView(any(), any());
+        doNothing().when(mWindowManager).removeViewImmediate(any());
         mMockSysUiState = mock(SysUiState.class);
         when(mMockSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mMockSysUiState);
+
+        mContext.addMockSystemService(WindowManager.class, mWindowManager);
+        mSysuiTestableContextExternal.addMockSystemService(WindowManager.class, mWindowManager);
     }
 
     @Test
@@ -239,10 +260,8 @@
         defaultNavBar.createView(null);
         externalNavBar.createView(null);
 
-        // Set IME window status for default NavBar.
-        mCommandQueue.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
-                BACK_DISPOSITION_DEFAULT, true, false);
-        processAllMessages();
+        defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
+                BACK_DISPOSITION_DEFAULT, true);
 
         // Verify IME window state will be updated in default NavBar & external NavBar state reset.
         assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN,
@@ -250,11 +269,10 @@
         assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
         assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
 
-        // Set IME window status for external NavBar.
-        mCommandQueue.setImeWindowStatus(EXTERNAL_DISPLAY_ID, null,
-                IME_VISIBLE, BACK_DISPOSITION_DEFAULT, true, false);
-        processAllMessages();
-
+        externalNavBar.setImeWindowStatus(EXTERNAL_DISPLAY_ID, null, IME_VISIBLE,
+                BACK_DISPOSITION_DEFAULT, true);
+        defaultNavBar.setImeWindowStatus(
+                DEFAULT_DISPLAY, null, IME_INVISIBLE, BACK_DISPOSITION_DEFAULT, false);
         // Verify IME window state will be updated in external NavBar & default NavBar state reset.
         assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN,
                 externalNavBar.getNavigationIconHints());
@@ -280,19 +298,15 @@
         DeviceProvisionedController deviceProvisionedController =
                 mock(DeviceProvisionedController.class);
         when(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
-        assertNotNull(mAccessibilityWrapper);
-        return spy(new NavigationBar(context,
-                mock(WindowManager.class),
-                () -> mock(AssistManager.class),
+        NavigationBar.Factory factory = new NavigationBar.Factory(
+                () -> mAssistManager,
                 mock(AccessibilityManager.class),
-                context.getDisplayId() == DEFAULT_DISPLAY ? mAccessibilityWrapper
-                        : mock(AccessibilityManagerWrapper.class),
                 deviceProvisionedController,
                 new MetricsLogger(),
                 mOverviewProxyService,
-                mock(NavigationModeController.class),
+                mNavigationModeController,
                 mock(AccessibilityButtonModeObserver.class),
-                mock(StatusBarStateController.class),
+                mStatusBarStateController,
                 mMockSysUiState,
                 mBroadcastDispatcher,
                 mCommandQueue,
@@ -308,7 +322,14 @@
                 mock(NavigationBarOverlayController.class),
                 mUiEventLogger,
                 mNavigationBarA11yHelper,
-                mock(UserTracker.class)));
+                mock(UserTracker.class),
+                mLightBarController,
+                mLightBarcontrollerFactory,
+                mAutoHideController,
+                mAutoHideControllerFactory,
+                Optional.of(mTelecomManager),
+                mInputMethodManager);
+        return spy(factory.create(context));
     }
 
     private void processAllMessages() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
index a1760a7..7e900c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt
@@ -22,6 +22,7 @@
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.qs.PseudoGridView
@@ -68,6 +69,8 @@
     private lateinit var launchView: View
     @Mock
     private lateinit var gridView: PseudoGridView
+    @Mock
+    private lateinit var dialogLaunchAnimator: DialogLaunchAnimator
     @Captor
     private lateinit var clickCaptor: ArgumentCaptor<View.OnClickListener>
 
@@ -87,6 +90,7 @@
                 { userDetailViewAdapter },
                 activityStarter,
                 falsingManager,
+                dialogLaunchAnimator,
                 { dialog }
         )
     }
@@ -94,7 +98,7 @@
     @Test
     fun showDialog_callsDialogShow() {
         controller.showDialog(launchView)
-        verify(dialog).show()
+        verify(dialogLaunchAnimator).showFromView(dialog, launchView)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 2416132..af624ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -15,6 +15,8 @@
 package com.android.systemui.statusbar;
 
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
+import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
+import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
 import static android.view.InsetsState.ITYPE_STATUS_BAR;
@@ -188,8 +190,13 @@
 
     @Test
     public void testShowImeButtonForSecondaryDisplay() {
+        // First show in default display to update the "last updated ime display"
+        testShowImeButton();
+
         mCommandQueue.setImeWindowStatus(SECONDARY_DISPLAY, null, 1, 2, true, false);
         waitForIdleSync();
+        verify(mCallbacks).setImeWindowStatus(eq(DEFAULT_DISPLAY), eq(null), eq(IME_INVISIBLE),
+                eq(BACK_DISPOSITION_DEFAULT), eq(false));
         verify(mCallbacks).setImeWindowStatus(
                 eq(SECONDARY_DISPLAY), eq(null), eq(1), eq(2), eq(true));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index f5cab1d..01f7fae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -166,7 +166,7 @@
     private BroadcastReceiver mBroadcastReceiver;
     private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
 
-    private KeyguardIndicationTextView mTextView;
+    private KeyguardIndicationTextView mTextView; // AOD text
 
     private KeyguardIndicationController mController;
     private WakeLockFake.Builder mWakeLockBuilder;
@@ -412,41 +412,32 @@
 
     @Test
     public void transientIndication_holdsWakeLock_whenDozing() {
+        // GIVEN animations are enabled and text is visible
+        mTextView.setAnimationsEnabled(true);
         createController();
+        mController.setVisible(true);
 
+        // WHEN transient text is shown
         mStatusBarStateListener.onDozingChanged(true);
         mController.showTransientIndication("Test");
 
-        assertTrue(mWakeLock.isHeld());
+        // THEN wake lock is held while the animation is running
+        assertTrue("WakeLock expected: HELD, was: RELEASED", mWakeLock.isHeld());
     }
 
     @Test
-    public void transientIndication_releasesWakeLock_afterHiding() {
+    public void transientIndication_releasesWakeLock_whenDozing() {
+        // GIVEN animations aren't enabled
+        mTextView.setAnimationsEnabled(false);
         createController();
+        mController.setVisible(true);
 
+        // WHEN we show the transient indication
         mStatusBarStateListener.onDozingChanged(true);
         mController.showTransientIndication("Test");
-        mController.hideTransientIndication();
 
-        assertFalse(mWakeLock.isHeld());
-    }
-
-    @Test
-    public void transientIndication_releasesWakeLock_afterHidingDelayed() throws Throwable {
-        mInstrumentation.runOnMainSync(() -> {
-            createController();
-
-            mStatusBarStateListener.onDozingChanged(true);
-            mController.showTransientIndication("Test");
-            mController.hideTransientIndicationDelayed(0);
-        });
-        mInstrumentation.waitForIdleSync();
-
-        Boolean[] held = new Boolean[1];
-        mInstrumentation.runOnMainSync(() -> {
-            held[0] = mWakeLock.isHeld();
-        });
-        assertFalse("WakeLock expected: RELEASED, was: HELD", held[0]);
+        // THEN wake lock is RELEASED, not held
+        assertFalse("WakeLock expected: RELEASED, was: HELD", mWakeLock.isHeld());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index c50296b..7938511 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -209,7 +209,7 @@
         verify(scrimController, never()).setTransitionToFullShadeProgress(anyFloat())
         verify(notificationPanelController, never()).setTransitionToFullShadeAmount(anyFloat(),
                 anyBoolean(), anyLong())
-        verify(qS, never()).setTransitionToFullShadeAmount(anyFloat(), anyBoolean())
+        verify(qS, never()).setTransitionToFullShadeAmount(anyFloat(), anyFloat())
     }
 
     @Test
@@ -220,7 +220,7 @@
         verify(scrimController).setTransitionToFullShadeProgress(anyFloat())
         verify(notificationPanelController).setTransitionToFullShadeAmount(anyFloat(),
                 anyBoolean(), anyLong())
-        verify(qS).setTransitionToFullShadeAmount(anyFloat(), anyBoolean())
+        verify(qS).setTransitionToFullShadeAmount(anyFloat(), anyFloat())
         verify(depthController).transitionToFullShadeProgress = anyFloat()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 5d50485..b13c4d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -246,6 +246,7 @@
         clearInvocations(plugin)
 
         // WHEN the session is closed
+        controller.stateChangeListener.onViewDetachedFromWindow(smartspaceView as View)
         controller.disconnect()
 
         // THEN the listener receives an empty list of targets
@@ -417,6 +418,7 @@
         connectSession()
 
         // WHEN we are told to cleanup
+        controller.stateChangeListener.onViewDetachedFromWindow(smartspaceView as View)
         controller.disconnect()
 
         // THEN we disconnect from the session and unregister any listeners
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
index bca1227..bafbccd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
@@ -198,7 +198,6 @@
         mHeadsUpAppearanceController.destroy();
         verify(mHeadsUpManager).removeListener(any());
         verify(mDarkIconDispatcher).removeDarkReceiver((DarkIconDispatcher.DarkReceiver) any());
-        verify(mPanelView).setVerticalTranslationListener(isNull());
         verify(mPanelView).removeTrackingHeadsUpListener(any());
         verify(mPanelView).setHeadsUpAppearanceController(isNull());
         verify(mStackScrollerController).removeOnExpandedHeightChangedListener(any());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
index 1de4647..52c6bac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
@@ -345,6 +345,8 @@
     private LockscreenGestureLogger mLockscreenGestureLogger;
     @Mock
     private DumpManager mDumpManager;
+    @Mock
+    private NotificationsQSContainerController mNotificationsQSContainerController;
 
     private SysuiStatusBarStateController mStatusBarStateController;
     private NotificationPanelViewController mNotificationPanelViewController;
@@ -479,6 +481,7 @@
                 () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager,
                 mConversationNotificationManager, mMediaHiearchyManager,
                 mStatusBarKeyguardViewManager,
+                mNotificationsQSContainerController,
                 mNotificationStackScrollLayoutController,
                 mKeyguardStatusViewComponentFactory,
                 mKeyguardQsUserSwitchComponentFactory,
@@ -536,8 +539,8 @@
     }
 
     @Test
-    public void testSetMinFraction() {
-        mNotificationPanelViewController.setMinFraction(0.5f);
+    public void testSetPanelScrimMinFraction() {
+        mNotificationPanelViewController.setPanelScrimMinFraction(0.5f);
         verify(mNotificationShadeDepthController).setPanelPullDownMinFraction(eq(0.5f));
     }
 
@@ -722,18 +725,6 @@
     }
 
     @Test
-    public void testOnDragDownEvent_horizontalTranslationIsZeroForSplitShade() {
-        when(mNotificationStackScrollLayoutController.getWidth()).thenReturn(350f);
-        when(mView.getWidth()).thenReturn(800);
-        enableSplitShade(/* enabled= */ true);
-
-        onTouchEvent(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN,
-                200f /* x position */, 0f, 0));
-
-        verify(mQsFrame).setTranslationX(0);
-    }
-
-    @Test
     public void testCanCollapsePanelOnTouch_trueForKeyGuard() {
         mStatusBarStateController.setState(KEYGUARD);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationQSContainerControllerTest.kt
new file mode 100644
index 0000000..337e64592
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationQSContainerControllerTest.kt
@@ -0,0 +1,254 @@
+package com.android.systemui.statusbar.phone
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.WindowInsets
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.navigationbar.NavigationModeController
+import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener
+import com.android.systemui.recents.OverviewProxyService
+import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
+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.Mockito
+import org.mockito.Mockito.RETURNS_DEEP_STUBS
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import java.util.function.Consumer
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class NotificationQSContainerControllerTest : SysuiTestCase() {
+
+    companion object {
+        const val STABLE_INSET_BOTTOM = 100
+        const val CUTOUT_HEIGHT = 50
+        const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL
+        const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON
+        const val NOTIFICATIONS_MARGIN = 50
+    }
+
+    @Mock
+    private lateinit var navigationModeController: NavigationModeController
+    @Mock
+    private lateinit var overviewProxyService: OverviewProxyService
+    @Mock
+    private lateinit var notificationsQSContainer: NotificationsQuickSettingsContainer
+    @Captor
+    lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
+    @Captor
+    lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener>
+    @Captor
+    lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>>
+
+    private lateinit var notificationsQSContainerController: NotificationsQSContainerController
+    private lateinit var navigationModeCallback: ModeChangedListener
+    private lateinit var taskbarVisibilityCallback: OverviewProxyListener
+    private lateinit var windowInsetsCallback: Consumer<WindowInsets>
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        notificationsQSContainerController = NotificationsQSContainerController(
+                notificationsQSContainer,
+                navigationModeController,
+                overviewProxyService
+        )
+        whenever(notificationsQSContainer.defaultNotificationsMarginBottom)
+                .thenReturn(NOTIFICATIONS_MARGIN)
+        whenever(navigationModeController.addListener(navigationModeCaptor.capture()))
+                .thenReturn(GESTURES_NAVIGATION)
+        doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture())
+        doNothing().`when`(notificationsQSContainer)
+                .setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
+
+        notificationsQSContainerController.init()
+        notificationsQSContainerController.onViewAttached()
+
+        navigationModeCallback = navigationModeCaptor.value
+        taskbarVisibilityCallback = taskbarVisibilityCaptor.value
+        windowInsetsCallback = windowInsetsCallbackCaptor.value
+    }
+
+    @Test
+    fun testTaskbarVisibleInSplitShade() {
+        notificationsQSContainerController.splitShadeEnabled = true
+        given(taskbarVisible = true,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0, // taskbar should disappear when shade is expanded
+                expectedNotificationsMargin = NOTIFICATIONS_MARGIN)
+
+        given(taskbarVisible = true,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = STABLE_INSET_BOTTOM,
+                expectedNotificationsMargin = NOTIFICATIONS_MARGIN)
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSplitShade() {
+        // when taskbar is not visible, it means we're on the home screen
+        notificationsQSContainerController.splitShadeEnabled = true
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0)
+
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons
+                expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN)
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSplitShadeWithCutout() {
+        notificationsQSContainerController.splitShadeEnabled = true
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withCutout())
+        then(expectedContainerPadding = CUTOUT_HEIGHT)
+
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = windowInsets().withCutout().withStableBottom())
+        then(expectedContainerPadding = 0,
+                expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN)
+    }
+
+    @Test
+    fun testTaskbarVisibleInSinglePaneShade() {
+        notificationsQSContainerController.splitShadeEnabled = false
+        given(taskbarVisible = true,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0)
+
+        given(taskbarVisible = true,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = STABLE_INSET_BOTTOM)
+    }
+
+    @Test
+    fun testTaskbarNotVisibleInSinglePaneShade() {
+        notificationsQSContainerController.splitShadeEnabled = false
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = emptyInsets())
+        then(expectedContainerPadding = 0)
+
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withCutout().withStableBottom())
+        then(expectedContainerPadding = CUTOUT_HEIGHT)
+
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0,
+                expectedQsPadding = STABLE_INSET_BOTTOM)
+    }
+
+    @Test
+    fun testCustomizingInSinglePaneShade() {
+        notificationsQSContainerController.splitShadeEnabled = false
+        notificationsQSContainerController.setCustomizerShowing(true)
+        // always sets spacings to 0
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0,
+                expectedNotificationsMargin = 0)
+
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = emptyInsets())
+        then(expectedContainerPadding = 0,
+                expectedNotificationsMargin = 0)
+    }
+
+    @Test
+    fun testDetailShowingInSinglePaneShade() {
+        notificationsQSContainerController.splitShadeEnabled = false
+        notificationsQSContainerController.setDetailShowing(true)
+        // always sets spacings to 0
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0,
+                expectedNotificationsMargin = 0)
+
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = emptyInsets())
+        then(expectedContainerPadding = 0,
+                expectedNotificationsMargin = 0)
+    }
+
+    @Test
+    fun testDetailShowingInSplitShade() {
+        notificationsQSContainerController.splitShadeEnabled = true
+        given(taskbarVisible = false,
+                navigationMode = GESTURES_NAVIGATION,
+                insets = windowInsets().withStableBottom())
+        then(expectedContainerPadding = 0)
+
+        notificationsQSContainerController.setDetailShowing(true)
+        // should not influence spacing
+        given(taskbarVisible = false,
+                navigationMode = BUTTONS_NAVIGATION,
+                insets = emptyInsets())
+        then(expectedContainerPadding = 0)
+    }
+
+    private fun given(
+        taskbarVisible: Boolean,
+        navigationMode: Int,
+        insets: WindowInsets
+    ) {
+        Mockito.clearInvocations(notificationsQSContainer)
+        taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false)
+        navigationModeCallback.onNavigationModeChanged(navigationMode)
+        windowInsetsCallback.accept(insets)
+    }
+
+    fun then(
+        expectedContainerPadding: Int,
+        expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN,
+        expectedQsPadding: Int = 0
+    ) {
+        verify(notificationsQSContainer)
+                .setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding))
+        verify(notificationsQSContainer).setNotificationsMarginBottom(expectedNotificationsMargin)
+        verify(notificationsQSContainer).setQSScrollPaddingBottom(expectedQsPadding)
+        Mockito.clearInvocations(notificationsQSContainer)
+    }
+
+    private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS)
+
+    private fun emptyInsets() = mock(WindowInsets::class.java)
+
+    private fun WindowInsets.withCutout(): WindowInsets {
+        whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
+        return this
+    }
+
+    private fun WindowInsets.withStableBottom(): WindowInsets {
+        whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM)
+        return this
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
index aee9f12..ec7e07f90 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
@@ -35,6 +35,8 @@
     private lateinit var panelView: ViewGroup
     @Mock
     private lateinit var scrimController: ScrimController
+    @Mock
+    private lateinit var statusBar: StatusBar
 
     private lateinit var view: PhoneStatusBarView
 
@@ -48,6 +50,7 @@
         view = PhoneStatusBarView(mContext, null)
         view.setPanel(panelViewController)
         view.setScrimController(scrimController)
+        view.setBar(statusBar)
     }
 
     @Test
@@ -72,7 +75,7 @@
 
     @Test
     fun panelExpansionChanged_fracZero_stateChangeListenerNotified() {
-        val listener = TestStateChangedListener()
+        val listener = TestExpansionStateChangedListener()
         view.setPanelExpansionStateChangedListener(listener)
 
         view.panelExpansionChanged(0f, false)
@@ -82,7 +85,7 @@
 
     @Test
     fun panelExpansionChanged_fracOne_stateChangeListenerNotified() {
-        val listener = TestStateChangedListener()
+        val listener = TestExpansionStateChangedListener()
         view.setPanelExpansionStateChangedListener(listener)
 
         view.panelExpansionChanged(1f, false)
@@ -92,7 +95,7 @@
 
     @Test
     fun panelExpansionChanged_fracHalf_stateChangeListenerNotNotified() {
-        val listener = TestStateChangedListener()
+        val listener = TestExpansionStateChangedListener()
         view.setPanelExpansionStateChangedListener(listener)
 
         view.panelExpansionChanged(0.5f, false)
@@ -106,11 +109,59 @@
         // No assert needed, just testing no crash
     }
 
-    private class TestStateChangedListener : PhoneStatusBarView.PanelExpansionStateChangedListener {
+    @Test
+    fun panelStateChanged_toStateOpening_listenerNotified() {
+        val listener = TestStateChangedListener()
+        view.setPanelStateChangeListener(listener)
+
+        view.panelExpansionChanged(0.5f, true)
+
+        assertThat(listener.state).isEqualTo(PanelBar.STATE_OPENING)
+    }
+
+    @Test
+    fun panelStateChanged_toStateOpen_listenerNotified() {
+        val listener = TestStateChangedListener()
+        view.setPanelStateChangeListener(listener)
+
+        view.panelExpansionChanged(1f, true)
+
+        assertThat(listener.state).isEqualTo(PanelBar.STATE_OPEN)
+    }
+
+    @Test
+    fun panelStateChanged_toStateClosed_listenerNotified() {
+        val listener = TestStateChangedListener()
+        view.setPanelStateChangeListener(listener)
+
+        // First, open the panel
+        view.panelExpansionChanged(1f, true)
+
+        // Then, close it again
+        view.panelExpansionChanged(0f, false)
+
+        assertThat(listener.state).isEqualTo(PanelBar.STATE_CLOSED)
+    }
+
+    @Test
+    fun panelStateChanged_noListener_noCrash() {
+        view.panelExpansionChanged(1f, true)
+        // No assert needed, just testing no crash
+    }
+
+    private class TestExpansionStateChangedListener
+        : PhoneStatusBarView.PanelExpansionStateChangedListener {
         var stateChangeCalled: Boolean = false
 
         override fun onPanelExpansionStateChanged() {
             stateChangeCalled = true
         }
     }
+
+    private class TestStateChangedListener : PanelBar.PanelStateChangeListener {
+        var state: Int = 0
+        override fun onStateChanged(state: Int) {
+            this.state = state
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 5ebe900..705112a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -623,7 +623,7 @@
 
     @Test
     public void transitionToUnlocked() {
-        mScrimController.setPanelExpansion(0f);
+        mScrimController.setRawPanelExpansionFraction(0f);
         mScrimController.transitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
         assertScrimAlpha(Map.of(
@@ -638,7 +638,7 @@
         ));
 
         // Back scrim should be visible after start dragging
-        mScrimController.setPanelExpansion(0.3f);
+        mScrimController.setRawPanelExpansionFraction(0.3f);
         assertScrimAlpha(Map.of(
                 mScrimInFront, TRANSPARENT,
                 mNotificationsScrim, SEMI_TRANSPARENT,
@@ -663,20 +663,20 @@
 
     @Test
     public void panelExpansion() {
-        mScrimController.setPanelExpansion(0f);
-        mScrimController.setPanelExpansion(0.5f);
+        mScrimController.setRawPanelExpansionFraction(0f);
+        mScrimController.setRawPanelExpansionFraction(0.5f);
         mScrimController.transitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
 
         reset(mScrimBehind);
-        mScrimController.setPanelExpansion(0f);
-        mScrimController.setPanelExpansion(1.0f);
+        mScrimController.setRawPanelExpansionFraction(0f);
+        mScrimController.setRawPanelExpansionFraction(1.0f);
         finishAnimationsImmediately();
 
         assertEquals("Scrim alpha should change after setPanelExpansion",
                 mScrimBehindAlpha, mScrimBehind.getViewAlpha(), 0.01f);
 
-        mScrimController.setPanelExpansion(0f);
+        mScrimController.setRawPanelExpansionFraction(0f);
         finishAnimationsImmediately();
 
         assertEquals("Scrim alpha should change after setPanelExpansion",
@@ -723,21 +723,21 @@
 
     @Test
     public void panelExpansionAffectsAlpha() {
-        mScrimController.setPanelExpansion(0f);
-        mScrimController.setPanelExpansion(0.5f);
+        mScrimController.setRawPanelExpansionFraction(0f);
+        mScrimController.setRawPanelExpansionFraction(0.5f);
         mScrimController.transitionTo(ScrimState.UNLOCKED);
         finishAnimationsImmediately();
 
         final float scrimAlpha = mScrimBehind.getViewAlpha();
         reset(mScrimBehind);
         mScrimController.setExpansionAffectsAlpha(false);
-        mScrimController.setPanelExpansion(0.8f);
+        mScrimController.setRawPanelExpansionFraction(0.8f);
         verifyZeroInteractions(mScrimBehind);
         assertEquals("Scrim opacity shouldn't change when setExpansionAffectsAlpha "
                 + "is false", scrimAlpha, mScrimBehind.getViewAlpha(), 0.01f);
 
         mScrimController.setExpansionAffectsAlpha(true);
-        mScrimController.setPanelExpansion(0.1f);
+        mScrimController.setRawPanelExpansionFraction(0.1f);
         finishAnimationsImmediately();
         Assert.assertNotEquals("Scrim opacity should change when setExpansionAffectsAlpha "
                 + "is true", scrimAlpha, mScrimBehind.getViewAlpha(), 0.01f);
@@ -747,7 +747,7 @@
     public void transitionToUnlockedFromOff() {
         // Simulate unlock with fingerprint without AOD
         mScrimController.transitionTo(ScrimState.OFF);
-        mScrimController.setPanelExpansion(0f);
+        mScrimController.setRawPanelExpansionFraction(0f);
         finishAnimationsImmediately();
         mScrimController.transitionTo(ScrimState.UNLOCKED);
 
@@ -769,7 +769,7 @@
     public void transitionToUnlockedFromAod() {
         // Simulate unlock with fingerprint
         mScrimController.transitionTo(ScrimState.AOD);
-        mScrimController.setPanelExpansion(0f);
+        mScrimController.setRawPanelExpansionFraction(0f);
         finishAnimationsImmediately();
         mScrimController.transitionTo(ScrimState.UNLOCKED);
 
@@ -948,7 +948,7 @@
     @Test
     public void testConservesExpansionOpacityAfterTransition() {
         mScrimController.transitionTo(ScrimState.UNLOCKED);
-        mScrimController.setPanelExpansion(0.5f);
+        mScrimController.setRawPanelExpansionFraction(0.5f);
         finishAnimationsImmediately();
 
         final float expandedAlpha = mScrimBehind.getViewAlpha();
@@ -1075,7 +1075,7 @@
     @Test
     public void testScrimsOpaque_whenShadeFullyExpanded() {
         mScrimController.transitionTo(ScrimState.UNLOCKED);
-        mScrimController.setPanelExpansion(1);
+        mScrimController.setRawPanelExpansionFraction(1);
         // notifications scrim alpha change require calling setQsPosition
         mScrimController.setQsPosition(0, 300);
         finishAnimationsImmediately();
@@ -1089,7 +1089,7 @@
     @Test
     public void testScrimsVisible_whenShadeVisible() {
         mScrimController.transitionTo(ScrimState.UNLOCKED);
-        mScrimController.setPanelExpansion(0.3f);
+        mScrimController.setRawPanelExpansionFraction(0.3f);
         // notifications scrim alpha change require calling setQsPosition
         mScrimController.setQsPosition(0, 300);
         finishAnimationsImmediately();
@@ -1124,7 +1124,7 @@
     public void testScrimsVisible_whenShadeVisible_clippingQs() {
         mScrimController.setClipsQsScrim(true);
         mScrimController.transitionTo(ScrimState.UNLOCKED);
-        mScrimController.setPanelExpansion(0.3f);
+        mScrimController.setRawPanelExpansionFraction(0.3f);
         // notifications scrim alpha change require calling setQsPosition
         mScrimController.setQsPosition(0.5f, 300);
         finishAnimationsImmediately();
@@ -1150,7 +1150,7 @@
     public void testNotificationScrimTransparent_whenOnLockscreen() {
         mScrimController.transitionTo(ScrimState.KEYGUARD);
         // even if shade is not pulled down, panel has expansion of 1 on the lockscreen
-        mScrimController.setPanelExpansion(1);
+        mScrimController.setRawPanelExpansionFraction(1);
         mScrimController.setQsPosition(0f, /*qs panel bottom*/ 0);
 
         assertScrimAlpha(Map.of(
@@ -1160,7 +1160,7 @@
 
     @Test
     public void testNotificationScrimVisible_afterOpeningShadeFromLockscreen() {
-        mScrimController.setPanelExpansion(1);
+        mScrimController.setRawPanelExpansionFraction(1);
         mScrimController.transitionTo(ScrimState.SHADE_LOCKED);
         finishAnimationsImmediately();
 
@@ -1203,11 +1203,11 @@
     @Test
     public void testNotificationTransparency_followsTransitionToFullShade() {
         mScrimController.transitionTo(ScrimState.SHADE_LOCKED);
-        mScrimController.setPanelExpansion(1.0f);
+        mScrimController.setRawPanelExpansionFraction(1.0f);
         finishAnimationsImmediately();
         float shadeLockedAlpha = mNotificationsScrim.getViewAlpha();
         mScrimController.transitionTo(ScrimState.KEYGUARD);
-        mScrimController.setPanelExpansion(1.0f);
+        mScrimController.setRawPanelExpansionFraction(1.0f);
         finishAnimationsImmediately();
         float keyguardAlpha = mNotificationsScrim.getViewAlpha();
 
@@ -1227,7 +1227,7 @@
     }
 
     private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) {
-        mScrimController.setPanelExpansion(expansion);
+        mScrimController.setRawPanelExpansionFraction(expansion);
         finishAnimationsImmediately();
         // alpha is not changing linearly thus 0.2 of leeway when asserting
         assertEquals(expectedAlpha, mNotificationsScrim.getViewAlpha(), 0.2);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 6e3d5ce..3fcd071 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -16,15 +16,12 @@
 
 package com.android.systemui.statusbar.phone;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -119,7 +116,7 @@
                 any(ViewGroup.class),
                 any(KeyguardBouncer.BouncerExpansionCallback.class)))
                 .thenReturn(mBouncer);
-
+        when(mStatusBar.getBouncerContainer()).thenReturn(mContainer);
         when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea);
         mWakefulnessLifecycle = new WakefulnessLifecycle(
                 getContext(),
@@ -143,7 +140,7 @@
                 mUnlockedScreenOffAnimationController,
                 mKeyguardMessageAreaFactory,
                 mShadeController);
-        mStatusBarKeyguardViewManager.registerStatusBar(mStatusBar, mContainer,
+        mStatusBarKeyguardViewManager.registerStatusBar(mStatusBar,
                 mNotificationPanelView, mBiometrucUnlockController,
                 mNotificationContainer, mBypassController);
         mStatusBarKeyguardViewManager.show(null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index 6868e18..741d95f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -459,7 +459,7 @@
                 mock(DumpManager.class),
                 mActivityLaunchAnimator,
                 mDialogLaunchAnimator);
-        when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class), any(ViewGroup.class),
+        when(mKeyguardViewMediator.registerStatusBar(any(StatusBar.class),
                 any(NotificationPanelViewController.class), any(BiometricUnlockController.class),
                 any(ViewGroup.class), any(KeyguardBypassController.class)))
                 .thenReturn(mStatusBarKeyguardViewManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index fe7ec68..3d2ff47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -36,10 +36,13 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.phone.StatusBarWindowController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.time.FakeSystemClock
@@ -49,8 +52,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.anyBoolean
-import org.mockito.ArgumentMatchers.nullable
+import org.mockito.ArgumentMatchers.*
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.eq
@@ -60,6 +62,7 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import java.util.Optional
 
 private const val CALL_UID = 900
 
@@ -81,9 +84,13 @@
     private lateinit var controller: OngoingCallController
     private lateinit var notifCollectionListener: NotifCollectionListener
 
+    @Mock private lateinit var mockFeatureFlags: FeatureFlags
+    @Mock private lateinit var mockSwipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler
     @Mock private lateinit var mockOngoingCallListener: OngoingCallListener
     @Mock private lateinit var mockActivityStarter: ActivityStarter
     @Mock private lateinit var mockIActivityManager: IActivityManager
+    @Mock private lateinit var mockStatusBarWindowController: StatusBarWindowController
+    @Mock private lateinit var mockStatusBarStateController: StatusBarStateController
 
     private lateinit var chipView: View
 
@@ -95,20 +102,22 @@
         }
 
         MockitoAnnotations.initMocks(this)
-        val featureFlags = mock(FeatureFlags::class.java)
-        `when`(featureFlags.isOngoingCallStatusBarChipEnabled).thenReturn(true)
+        `when`(mockFeatureFlags.isOngoingCallStatusBarChipEnabled).thenReturn(true)
         val notificationCollection = mock(CommonNotifCollection::class.java)
 
         controller = OngoingCallController(
                 notificationCollection,
-                featureFlags,
+                mockFeatureFlags,
                 clock,
                 mockActivityStarter,
                 mainExecutor,
                 mockIActivityManager,
                 OngoingCallLogger(uiEventLoggerFake),
                 DumpManager(),
-        )
+                Optional.of(mockStatusBarWindowController),
+                Optional.of(mockSwipeStatusBarAwayGestureHandler),
+                mockStatusBarStateController,
+            )
         controller.init()
         controller.addCallback(mockOngoingCallListener)
         controller.setChipView(chipView)
@@ -134,6 +143,13 @@
     }
 
     @Test
+    fun onEntryUpdated_isOngoingCallNotif_windowControllerUpdated() {
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(true)
+    }
+
+    @Test
     fun onEntryUpdated_notOngoingCallNotif_listenerNotNotified() {
         notifCollectionListener.onEntryUpdated(createNotCallNotifEntry())
 
@@ -224,6 +240,16 @@
         verify(mockOngoingCallListener).onOngoingCallStateChanged(anyBoolean())
     }
 
+    @Test
+    fun onEntryUpdated_callNotifAddedThenRemoved_windowControllerUpdated() {
+        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
+        notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
+
+        notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
+
+        verify(mockStatusBarWindowController).setOngoingProcessRequiresStatusBarVisible(false)
+    }
+
     /** Regression test for b/188491504. */
     @Test
     fun onEntryRemoved_removedNotifHasSameKeyAsAddedNotif_listenerNotified() {
@@ -414,6 +440,120 @@
     // Other tests for notifyChipVisibilityChanged are in [OngoingCallLogger], since
     // [OngoingCallController.notifyChipVisibilityChanged] just delegates to that class.
 
+    @Test
+    fun callNotificationAdded_chipIsClickable() {
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        assertThat(chipView.hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun fullscreenIsTrue_thenCallNotificationAdded_chipNotClickable() {
+        `when`(mockFeatureFlags.isOngoingCallInImmersiveChipTapEnabled).thenReturn(false)
+
+        getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        assertThat(chipView.hasOnClickListeners()).isFalse()
+    }
+
+    @Test
+    fun callNotificationAdded_thenFullscreenIsTrue_chipNotClickable() {
+        `when`(mockFeatureFlags.isOngoingCallInImmersiveChipTapEnabled).thenReturn(false)
+
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+        getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
+
+        assertThat(chipView.hasOnClickListeners()).isFalse()
+    }
+
+    @Test
+    fun fullscreenChangesToFalse_chipClickable() {
+        `when`(mockFeatureFlags.isOngoingCallInImmersiveChipTapEnabled).thenReturn(false)
+
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+        // First, update to true
+        getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
+        // Then, update to false
+        getStateListener().onFullscreenStateChanged(/* isFullscreen= */ false)
+
+        assertThat(chipView.hasOnClickListeners()).isTrue()
+    }
+
+    @Test
+    fun fullscreenIsTrue_butChipClickInImmersiveEnabled_chipClickable() {
+        `when`(mockFeatureFlags.isOngoingCallInImmersiveChipTapEnabled).thenReturn(true)
+
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+        getStateListener().onFullscreenStateChanged(/* isFullscreen= */ true)
+
+        assertThat(chipView.hasOnClickListeners()).isTrue()
+    }
+
+    // Swipe gesture tests
+
+    @Test
+    fun callStartedInImmersiveMode_swipeGestureCallbackAdded() {
+        getStateListener().onFullscreenStateChanged(true)
+
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        verify(mockSwipeStatusBarAwayGestureHandler)
+            .addOnGestureDetectedCallback(anyString(), any())
+    }
+
+    @Test
+    fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() {
+        getStateListener().onFullscreenStateChanged(false)
+
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        verify(mockSwipeStatusBarAwayGestureHandler, never())
+            .addOnGestureDetectedCallback(anyString(), any())
+    }
+
+    @Test
+    fun transitionToImmersiveMode_swipeGestureCallbackAdded() {
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+
+        getStateListener().onFullscreenStateChanged(true)
+
+        verify(mockSwipeStatusBarAwayGestureHandler)
+            .addOnGestureDetectedCallback(anyString(), any())
+    }
+
+    @Test
+    fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() {
+        getStateListener().onFullscreenStateChanged(true)
+        notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
+        reset(mockSwipeStatusBarAwayGestureHandler)
+
+        getStateListener().onFullscreenStateChanged(false)
+
+        verify(mockSwipeStatusBarAwayGestureHandler)
+            .removeOnGestureDetectedCallback(anyString())
+    }
+
+    @Test
+    fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() {
+        getStateListener().onFullscreenStateChanged(true)
+        val ongoingCallNotifEntry = createOngoingCallNotifEntry()
+        notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
+        reset(mockSwipeStatusBarAwayGestureHandler)
+
+        notifCollectionListener.onEntryRemoved(ongoingCallNotifEntry, REASON_USER_STOPPED)
+
+        verify(mockSwipeStatusBarAwayGestureHandler)
+            .removeOnGestureDetectedCallback(anyString())
+    }
+
+    // TODO(b/195839150): Add test
+    //  swipeGesturedTriggeredPreviously_entersImmersiveModeAgain_callbackNotAdded(). That's
+    //  difficult to add now because we have no way to trigger [SwipeStatusBarAwayGestureHandler]'s
+    //  callbacks in test.
+
+    // END swipe gesture tests
+
     private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle)
 
     private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle)
@@ -438,6 +578,13 @@
     }
 
     private fun createNotCallNotifEntry() = NotificationEntryBuilder().build()
+
+    private fun getStateListener(): StatusBarStateController.StateListener {
+        val statusBarStateListenerCaptor = ArgumentCaptor.forClass(
+            StatusBarStateController.StateListener::class.java)
+        verify(mockStatusBarStateController).addCallback(statusBarStateListenerCaptor.capture())
+        return statusBarStateListenerCaptor.value!!
+    }
 }
 
 private val person = Person.Builder().setName("name").build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
index eebcbe6..3d55488 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/usb/UsbPermissionActivityTest.kt
@@ -31,7 +31,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.lang.Exception
 
 /**
  * UsbPermissionActivityTest
@@ -54,7 +53,9 @@
                 putExtra(Intent.EXTRA_INTENT, PendingIntent.getBroadcast(
                         mContext,
                         334,
-                        Intent("NO_ACTION"),
+                        Intent("NO_ACTION").apply {
+                                setPackage("com.android.systemui.tests")
+                        },
                         PendingIntent.FLAG_MUTABLE))
                 putExtra(UsbManager.EXTRA_ACCESSORY, UsbAccessory(
                         "manufacturer",
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/ChannelsTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/ChannelsTest.java
index f1c9980..c7bcdef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/ChannelsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/ChannelsTest.java
@@ -22,7 +22,6 @@
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.content.Context;
-import android.provider.Settings;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.util.ArraySet;
 
@@ -68,51 +67,4 @@
         list.forEach((chan) -> assertTrue(ALL_CHANNELS.contains(chan.getId())));
     }
 
-    @Test
-    public void testChannelSetup_noLegacyScreenshot() {
-        // Assert old channel cleaned up.
-        // TODO: remove that code + this test after P.
-        NotificationChannels.createAll(mContext);
-        ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
-        verify(mMockNotificationManager).deleteNotificationChannel(
-                NotificationChannels.SCREENSHOTS_LEGACY);
-    }
-
-    @Test
-    public void testInheritFromLegacy_keepsUserLockedLegacySettings() {
-        NotificationChannel legacyChannel = new NotificationChannel("id", "oldName",
-                NotificationManager.IMPORTANCE_MIN);
-        legacyChannel.setImportance(NotificationManager.IMPORTANCE_NONE);;
-        legacyChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
-                legacyChannel.getAudioAttributes());
-        legacyChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE |
-                NotificationChannel.USER_LOCKED_SOUND);
-        NotificationChannel newChannel =
-                NotificationChannels.createScreenshotChannel("newName", legacyChannel);
-        // NONE importance user locked, so don't use HIGH for new channel.
-        assertEquals(NotificationManager.IMPORTANCE_NONE, newChannel.getImportance());
-        assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, newChannel.getSound());
-    }
-
-    @Test
-    public void testInheritFromLegacy_dropsUnlockedLegacySettings() {
-        NotificationChannel legacyChannel = new NotificationChannel("id", "oldName",
-                NotificationManager.IMPORTANCE_MIN);
-        NotificationChannel newChannel =
-                NotificationChannels.createScreenshotChannel("newName", legacyChannel);
-        assertEquals(null, newChannel.getSound());
-        assertEquals("newName", newChannel.getName());
-        // MIN importance not user locked, so HIGH wins out.
-        assertEquals(NotificationManager.IMPORTANCE_HIGH, newChannel.getImportance());
-    }
-
-    @Test
-    public void testInheritFromLegacy_noLegacyExists() {
-        NotificationChannel newChannel =
-                NotificationChannels.createScreenshotChannel("newName", null);
-        assertEquals(null, newChannel.getSound());
-        assertEquals("newName", newChannel.getName());
-        assertEquals(NotificationManager.IMPORTANCE_HIGH, newChannel.getImportance());
-    }
-
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/ListenerSetTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/ListenerSetTest.kt
new file mode 100644
index 0000000..2662da2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/ListenerSetTest.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ListenerSetTest : SysuiTestCase() {
+
+    var runnableSet: ListenerSet<Runnable> = ListenerSet()
+
+    @Before
+    fun setup() {
+        runnableSet = ListenerSet()
+    }
+
+    @Test
+    fun addIfAbsent_doesNotDoubleAdd() {
+        // setup & preconditions
+        val runnable1 = Runnable { }
+        val runnable2 = Runnable { }
+        assertThat(runnableSet.toList()).isEmpty()
+
+        // Test that an element can be added
+        assertThat(runnableSet.addIfAbsent(runnable1)).isTrue()
+        assertThat(runnableSet.toList()).containsExactly(runnable1)
+
+        // Test that a second element can be added
+        assertThat(runnableSet.addIfAbsent(runnable2)).isTrue()
+        assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2)
+
+        // Test that re-adding the first element does nothing and returns false
+        assertThat(runnableSet.addIfAbsent(runnable1)).isFalse()
+        assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2)
+    }
+
+    @Test
+    fun remove_removesListener() {
+        // setup and preconditions
+        val runnable1 = Runnable { }
+        val runnable2 = Runnable { }
+        assertThat(runnableSet.toList()).isEmpty()
+        runnableSet.addIfAbsent(runnable1)
+        runnableSet.addIfAbsent(runnable2)
+        assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2)
+
+        // Test that removing the first runnable only removes that one runnable
+        assertThat(runnableSet.remove(runnable1)).isTrue()
+        assertThat(runnableSet.toList()).containsExactly(runnable2)
+
+        // Test that removing a non-present runnable does not error
+        assertThat(runnableSet.remove(runnable1)).isFalse()
+        assertThat(runnableSet.toList()).containsExactly(runnable2)
+
+        // Test that removing the other runnable succeeds
+        assertThat(runnableSet.remove(runnable2)).isTrue()
+        assertThat(runnableSet.toList()).isEmpty()
+    }
+
+    @Test
+    fun remove_isReentrantSafe() {
+        // Setup and preconditions
+        val runnablesCalled = mutableListOf<Int>()
+        // runnable1 is configured to remove itself
+        val runnable1 = object : Runnable {
+            override fun run() {
+                runnableSet.remove(this)
+                runnablesCalled.add(1)
+            }
+        }
+        val runnable2 = Runnable {
+            runnablesCalled.add(2)
+        }
+        assertThat(runnableSet.toList()).isEmpty()
+        runnableSet.addIfAbsent(runnable1)
+        runnableSet.addIfAbsent(runnable2)
+        assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2)
+
+        // Test that both runnables are called and 1 was removed
+        for (runnable in runnableSet) {
+            runnable.run()
+        }
+        assertThat(runnablesCalled).containsExactly(1, 2)
+        assertThat(runnableSet.toList()).containsExactly(runnable2)
+    }
+
+    @Test
+    fun addIfAbsent_isReentrantSafe() {
+        // Setup and preconditions
+        val runnablesCalled = mutableListOf<Int>()
+        val runnable99 = Runnable {
+            runnablesCalled.add(99)
+        }
+        // runnable1 is configured to add runnable99
+        val runnable1 = Runnable {
+            runnableSet.addIfAbsent(runnable99)
+            runnablesCalled.add(1)
+        }
+        val runnable2 = Runnable {
+            runnablesCalled.add(2)
+        }
+        assertThat(runnableSet.toList()).isEmpty()
+        runnableSet.addIfAbsent(runnable1)
+        runnableSet.addIfAbsent(runnable2)
+        assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2)
+
+        // Test that both original runnables are called and 99 was added but not called
+        for (runnable in runnableSet) {
+            runnable.run()
+        }
+        assertThat(runnablesCalled).containsExactly(1, 2)
+        assertThat(runnableSet.toList()).containsExactly(runnable1, runnable2, runnable99)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt
index b54aadb..d8e418a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/WallpaperControllerTest.kt
@@ -138,7 +138,7 @@
 
     private fun createWallpaperInfo(useDefaultTransition: Boolean = true): WallpaperInfo {
         val info = mock(WallpaperInfo::class.java)
-        whenever(info.shouldUseDefaultDisplayStateChangeTransition())
+        whenever(info.shouldUseDefaultUnfoldTransition())
             .thenReturn(useDefaultTransition)
         return info
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeProximitySensor.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeProximitySensor.java
index 50947ab..22cf744 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeProximitySensor.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeProximitySensor.java
@@ -19,14 +19,21 @@
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.concurrency.FakeExecution;
 
-public class FakeProximitySensor extends ProximitySensor {
+public class FakeProximitySensor extends ProximitySensorImpl {
     private boolean mAvailable;
     private boolean mRegistered;
 
-    public FakeProximitySensor(ThresholdSensor primary, ThresholdSensor secondary,
-            DelayableExecutor delayableExecutor) {
-        super(primary, secondary == null ? new FakeThresholdSensor() : secondary,
-                delayableExecutor, new FakeExecution());
+    public FakeProximitySensor(
+            ThresholdSensor primary,
+            ThresholdSensor secondary,
+            DelayableExecutor delayableExecutor
+    ) {
+        super(
+                primary,
+                secondary == null ? new FakeThresholdSensor() : secondary,
+                delayableExecutor,
+                new FakeExecution()
+        );
         mAvailable = true;
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeThresholdSensor.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeThresholdSensor.java
index d9f9789..0d4a6c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeThresholdSensor.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeThresholdSensor.java
@@ -59,6 +59,16 @@
         mListeners.remove(listener);
     }
 
+    @Override
+    public String getName() {
+        return "FakeThresholdSensorName";
+    }
+
+    @Override
+    public String getType() {
+        return "FakeThresholdSensorType";
+    }
+
     public void setLoaded(boolean loaded) {
         mIsLoaded = loaded;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/PostureDependentProximitySensorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/PostureDependentProximitySensorTest.java
new file mode 100644
index 0000000..075f393
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/PostureDependentProximitySensorTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.sensors;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.verify;
+
+import android.content.res.Resources;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.policy.DevicePostureController;
+import com.android.systemui.util.concurrency.FakeExecution;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+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;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class PostureDependentProximitySensorTest extends SysuiTestCase {
+    @Mock private Resources mResources;
+    @Mock private DevicePostureController mDevicePostureController;
+    @Mock private AsyncSensorManager mSensorManager;
+
+    @Captor private ArgumentCaptor<DevicePostureController.Callback> mPostureListenerCaptor =
+            ArgumentCaptor.forClass(DevicePostureController.Callback.class);
+    private DevicePostureController.Callback mPostureListener;
+
+    private PostureDependentProximitySensor mProximitySensor;
+    private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        allowTestableLooperAsMainThread();
+
+        mProximitySensor = new PostureDependentProximitySensor(
+                new ThresholdSensor[DevicePostureController.SUPPORTED_POSTURES_SIZE],
+                new ThresholdSensor[DevicePostureController.SUPPORTED_POSTURES_SIZE],
+                mFakeExecutor,
+                new FakeExecution(),
+                mDevicePostureController
+        );
+    }
+
+    @Test
+    public void testPostureChangeListenerAdded() {
+        capturePostureListener();
+    }
+
+    @Test
+    public void testPostureChangeListenerUpdatesPosture() {
+        // GIVEN posture listener is registered
+        capturePostureListener();
+
+        // WHEN the posture changes to DEVICE_POSTURE_OPENED
+        mPostureListener.onPostureChanged(DevicePostureController.DEVICE_POSTURE_OPENED);
+
+        // THEN device posture is updated to DEVICE_POSTURE_OPENED
+        assertEquals(DevicePostureController.DEVICE_POSTURE_OPENED,
+                mProximitySensor.mDevicePosture);
+
+        // WHEN the posture changes to DEVICE_POSTURE_CLOSED
+        mPostureListener.onPostureChanged(DevicePostureController.DEVICE_POSTURE_CLOSED);
+
+        // THEN device posture is updated to DEVICE_POSTURE_CLOSED
+        assertEquals(DevicePostureController.DEVICE_POSTURE_CLOSED,
+                mProximitySensor.mDevicePosture);
+
+        // WHEN the posture changes to DEVICE_POSTURE_FLIPPED
+        mPostureListener.onPostureChanged(DevicePostureController.DEVICE_POSTURE_FLIPPED);
+
+        // THEN device posture is updated to DEVICE_POSTURE_FLIPPED
+        assertEquals(DevicePostureController.DEVICE_POSTURE_FLIPPED,
+                mProximitySensor.mDevicePosture);
+
+        // WHEN the posture changes to DEVICE_POSTURE_HALF_OPENED
+        mPostureListener.onPostureChanged(DevicePostureController.DEVICE_POSTURE_HALF_OPENED);
+
+        // THEN device posture is updated to DEVICE_POSTURE_HALF_OPENED
+        assertEquals(DevicePostureController.DEVICE_POSTURE_HALF_OPENED,
+                mProximitySensor.mDevicePosture);
+    }
+
+    private void capturePostureListener() {
+        verify(mDevicePostureController).addCallback(mPostureListenerCaptor.capture());
+        mPostureListener = mPostureListenerCaptor.getValue();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximityCheckTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximityCheckTest.java
index 242fe9f..19dbf9a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximityCheckTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximityCheckTest.java
@@ -49,7 +49,7 @@
 
     private TestableCallback mTestableCallback = new TestableCallback();
 
-    private ProximitySensor.ProximityCheck mProximityCheck;
+    private ProximityCheck mProximityCheck;
 
     @Before
     public void setUp() throws Exception {
@@ -58,7 +58,7 @@
         thresholdSensor.setLoaded(true);
         mFakeProximitySensor = new FakeProximitySensor(thresholdSensor, null, mFakeExecutor);
 
-        mProximityCheck = new ProximitySensor.ProximityCheck(mFakeProximitySensor, mFakeExecutor);
+        mProximityCheck = new ProximityCheck(mFakeProximitySensor, mFakeExecutor);
     }
 
     @Test
@@ -67,7 +67,7 @@
 
         assertNull(mTestableCallback.mLastResult);
 
-        mFakeProximitySensor.setLastEvent(new ProximitySensor.ThresholdSensorEvent(true, 0));
+        mFakeProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 0));
         mFakeProximitySensor.alertListeners();
 
         assertTrue(mTestableCallback.mLastResult);
@@ -103,7 +103,7 @@
 
         mProximityCheck.check(100, mTestableCallback);
 
-        mFakeProximitySensor.setLastEvent(new ProximitySensor.ThresholdSensorEvent(true, 0));
+        mFakeProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 0));
         mFakeProximitySensor.alertListeners();
 
         assertThat(mTestableCallback.mLastResult).isNotNull();
@@ -123,7 +123,7 @@
 
         assertNull(mTestableCallback.mLastResult);
 
-        mFakeProximitySensor.setLastEvent(new ProximitySensor.ThresholdSensorEvent(true, 0));
+        mFakeProximitySensor.setLastEvent(new ThresholdSensorEvent(true, 0));
         mFakeProximitySensor.alertListeners();
 
         assertTrue(mTestableCallback.mLastResult);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorDualTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplDualTest.java
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorDualTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplDualTest.java
index 0e9d96c..5e75578 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorDualTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplDualTest.java
@@ -42,7 +42,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-public class ProximitySensorDualTest extends SysuiTestCase {
+public class ProximitySensorImplDualTest extends SysuiTestCase {
     private ProximitySensor mProximitySensor;
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     private FakeThresholdSensor mThresholdSensorPrimary;
@@ -57,7 +57,7 @@
         mThresholdSensorSecondary = new FakeThresholdSensor();
         mThresholdSensorSecondary.setLoaded(true);
 
-        mProximitySensor = new ProximitySensor(
+        mProximitySensor = new ProximitySensorImpl(
                 mThresholdSensorPrimary, mThresholdSensorSecondary, mFakeExecutor,
                 new FakeExecution());
     }
@@ -430,11 +430,11 @@
     }
 
     private static class TestableListener implements ThresholdSensor.Listener {
-        ThresholdSensor.ThresholdSensorEvent mLastEvent;
+        ThresholdSensorEvent mLastEvent;
         int mCallCount = 0;
 
         @Override
-        public void onThresholdCrossed(ThresholdSensor.ThresholdSensorEvent proximityEvent) {
+        public void onThresholdCrossed(ThresholdSensorEvent proximityEvent) {
             mLastEvent = proximityEvent;
             mCallCount++;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorSingleTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplSingleTest.java
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorSingleTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplSingleTest.java
index 6c6d355..752cd32 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorSingleTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ProximitySensorImplSingleTest.java
@@ -42,7 +42,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-public class ProximitySensorSingleTest extends SysuiTestCase {
+public class ProximitySensorImplSingleTest extends SysuiTestCase {
     private ProximitySensor mProximitySensor;
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     private FakeThresholdSensor mThresholdSensor;
@@ -54,7 +54,7 @@
         mThresholdSensor = new FakeThresholdSensor();
         mThresholdSensor.setLoaded(true);
 
-        mProximitySensor = new ProximitySensor(
+        mProximitySensor = new ProximitySensorImpl(
                 mThresholdSensor, new FakeThresholdSensor(), mFakeExecutor, new FakeExecution());
     }
 
@@ -215,7 +215,7 @@
     public void testPreventRecursiveAlert() {
         TestableListener listenerA = new TestableListener() {
             @Override
-            public void onThresholdCrossed(ProximitySensor.ThresholdSensorEvent proximityEvent) {
+            public void onThresholdCrossed(ThresholdSensorEvent proximityEvent) {
                 super.onThresholdCrossed(proximityEvent);
                 if (mCallCount < 2) {
                     mProximitySensor.alertListeners();
@@ -231,11 +231,11 @@
     }
 
     private static class TestableListener implements ThresholdSensor.Listener {
-        ThresholdSensor.ThresholdSensorEvent mLastEvent;
+        ThresholdSensorEvent mLastEvent;
         int mCallCount = 0;
 
         @Override
-        public void onThresholdCrossed(ThresholdSensor.ThresholdSensorEvent proximityEvent) {
+        public void onThresholdCrossed(ThresholdSensorEvent proximityEvent) {
             mLastEvent = proximityEvent;
             mCallCount++;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ThresholdSensorImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ThresholdSensorImplTest.java
index 125063a..b10f16c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ThresholdSensorImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/sensors/ThresholdSensorImplTest.java
@@ -380,7 +380,7 @@
         int mCallCount;
 
         @Override
-        public void onThresholdCrossed(ThresholdSensor.ThresholdSensorEvent event) {
+        public void onThresholdCrossed(ThresholdSensorEvent event) {
             mBelow = event.getBelow();
             mTimestampNs = event.getTimestampNs();
             mCallCount++;
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index b0893cc..ed37d7e 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.graphics.Camera;
 import android.graphics.GraphicBuffer;
 import android.graphics.Rect;
 import android.hardware.camera2.CameraAccessException;
@@ -135,6 +136,7 @@
 
     private HashMap<String, CameraCharacteristics> mCharacteristicsHashMap = new HashMap<>();
     private HashMap<String, Long> mMetadataVendorIdMap = new HashMap<>();
+    private CameraManager mCameraManager;
 
     private static boolean checkForAdvancedAPI() {
         if (EXTENSIONS_PRESENT && EXTENSIONS_VERSION.startsWith(ADVANCED_VERSION_PREFIX)) {
@@ -460,12 +462,12 @@
         // This will setup the camera vendor tag descriptor in the service process
         // along with all camera characteristics.
         try {
-            CameraManager manager = getSystemService(CameraManager.class);
+            mCameraManager = getSystemService(CameraManager.class);
 
-            String [] cameraIds = manager.getCameraIdListNoLazy();
+            String [] cameraIds = mCameraManager.getCameraIdListNoLazy();
             if (cameraIds != null) {
                 for (String cameraId : cameraIds) {
-                    CameraCharacteristics chars = manager.getCameraCharacteristics(cameraId);
+                    CameraCharacteristics chars = mCameraManager.getCameraCharacteristics(cameraId);
                     mCharacteristicsHashMap.put(cameraId, chars);
                     Object thisClass = CameraCharacteristics.Key.class;
                     Class<CameraCharacteristics.Key<?>> keyClass =
@@ -1174,8 +1176,9 @@
         @Override
         public void onInit(String cameraId, CameraMetadataNative cameraCharacteristics) {
             mCameraId = cameraId;
-            mPreviewExtender.onInit(cameraId, new CameraCharacteristics(cameraCharacteristics),
-                    CameraExtensionsProxyService.this);
+            CameraCharacteristics chars = new CameraCharacteristics(cameraCharacteristics);
+            mCameraManager.registerDeviceStateListener(chars);
+            mPreviewExtender.onInit(cameraId, chars, CameraExtensionsProxyService.this);
         }
 
         @Override
@@ -1200,13 +1203,16 @@
 
         @Override
         public void init(String cameraId, CameraMetadataNative chars) {
-            mPreviewExtender.init(cameraId, new CameraCharacteristics(chars));
+            CameraCharacteristics c = new CameraCharacteristics(chars);
+            mCameraManager.registerDeviceStateListener(c);
+            mPreviewExtender.init(cameraId, c);
         }
 
         @Override
         public boolean isExtensionAvailable(String cameraId, CameraMetadataNative chars) {
-            return mPreviewExtender.isExtensionAvailable(cameraId,
-                    new CameraCharacteristics(chars));
+            CameraCharacteristics c = new CameraCharacteristics(chars);
+            mCameraManager.registerDeviceStateListener(c);
+            return mPreviewExtender.isExtensionAvailable(cameraId, c);
         }
 
         @Override
@@ -1283,8 +1289,9 @@
 
         @Override
         public void onInit(String cameraId, CameraMetadataNative cameraCharacteristics) {
-            mImageExtender.onInit(cameraId, new CameraCharacteristics(cameraCharacteristics),
-                    CameraExtensionsProxyService.this);
+            CameraCharacteristics chars = new CameraCharacteristics(cameraCharacteristics);
+            mCameraManager.registerDeviceStateListener(chars);
+            mImageExtender.onInit(cameraId, chars, CameraExtensionsProxyService.this);
             mCameraId = cameraId;
         }
 
@@ -1310,13 +1317,16 @@
 
         @Override
         public void init(String cameraId, CameraMetadataNative chars) {
-            mImageExtender.init(cameraId, new CameraCharacteristics(chars));
+            CameraCharacteristics c = new CameraCharacteristics(chars);
+            mCameraManager.registerDeviceStateListener(c);
+            mImageExtender.init(cameraId, c);
         }
 
         @Override
         public boolean isExtensionAvailable(String cameraId, CameraMetadataNative chars) {
-            return mImageExtender.isExtensionAvailable(cameraId,
-                    new CameraCharacteristics(chars));
+            CameraCharacteristics c = new CameraCharacteristics(chars);
+            mCameraManager.registerDeviceStateListener(c);
+            return mImageExtender.isExtensionAvailable(cameraId, c);
         }
 
         @Override
diff --git a/packages/services/PacProcessor/src/com/android/pacprocessor/PacService.java b/packages/services/PacProcessor/src/com/android/pacprocessor/PacService.java
index 46bda06..27d4ea7 100644
--- a/packages/services/PacProcessor/src/com/android/pacprocessor/PacService.java
+++ b/packages/services/PacProcessor/src/com/android/pacprocessor/PacService.java
@@ -21,6 +21,7 @@
 import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.UserManager;
 import android.util.Log;
 import android.webkit.PacProcessor;
 
@@ -33,16 +34,44 @@
 public class PacService extends Service {
     private static final String TAG = "PacService";
 
-    private Object mLock = new Object();
+    private final Object mLock = new Object();
 
+    // Webkit PacProcessor cannot be instantiated before the user is unlocked, so this field is
+    // initialized lazily.
     @GuardedBy("mLock")
-    private final PacProcessor mPacProcessor = PacProcessor.getInstance();
+    private PacProcessor mPacProcessor;
+
+    // Stores PAC script when setPacFile is called before mPacProcessor is available. In case the
+    // script was already fed to the PacProcessor, it should be null.
+    @GuardedBy("mLock")
+    private String mPendingScript;
 
     private ProxyServiceStub mStub = new ProxyServiceStub();
 
     @Override
     public void onCreate() {
         super.onCreate();
+
+        synchronized (mLock) {
+            checkPacProcessorLocked();
+        }
+    }
+
+    /**
+     * Initializes PacProcessor if it hasn't been initialized yet and if the system user is
+     * unlocked, e.g. after the user has entered their PIN after a reboot.
+     * Returns whether PacProcessor is available.
+     */
+    private boolean checkPacProcessorLocked() {
+        if (mPacProcessor != null) {
+            return true;
+        }
+        UserManager um = getSystemService(UserManager.class);
+        if (um.isUserUnlocked()) {
+            mPacProcessor = PacProcessor.getInstance();
+            return true;
+        }
+        return false;
     }
 
     @Override
@@ -74,7 +103,20 @@
                 }
 
                 synchronized (mLock) {
-                    return mPacProcessor.findProxyForUrl(url);
+                    if (checkPacProcessorLocked()) {
+                        // Apply pending script in case it was set before processor was ready.
+                        if (mPendingScript != null) {
+                            if (!mPacProcessor.setProxyScript(mPendingScript)) {
+                                Log.e(TAG, "Unable to parse proxy script.");
+                            }
+                            mPendingScript = null;
+                        }
+                        return mPacProcessor.findProxyForUrl(url);
+                    } else {
+                        Log.e(TAG, "PacProcessor isn't ready during early boot,"
+                                + " request will be direct");
+                        return null;
+                    }
                 }
             } catch (MalformedURLException e) {
                 throw new IllegalArgumentException("Invalid URL was passed");
@@ -88,8 +130,13 @@
                 throw new SecurityException();
             }
             synchronized (mLock) {
-                if (!mPacProcessor.setProxyScript(script)) {
-                    Log.e(TAG, "Unable to parse proxy script.");
+                if (checkPacProcessorLocked()) {
+                    if (!mPacProcessor.setProxyScript(script)) {
+                        Log.e(TAG, "Unable to parse proxy script.");
+                    }
+                } else {
+                    Log.d(TAG, "PAC processor isn't ready, saving script for later.");
+                    mPendingScript = script;
                 }
             }
         }
diff --git a/proto/src/criticalevents/OWNERS b/proto/src/criticalevents/OWNERS
new file mode 100644
index 0000000..07c0e70
--- /dev/null
+++ b/proto/src/criticalevents/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/criticalevents/OWNERS
diff --git a/services/accessibility/java/com/android/server/accessibility/PolicyWarningUIController.java b/services/accessibility/java/com/android/server/accessibility/PolicyWarningUIController.java
index 9801407..5080ca2 100644
--- a/services/accessibility/java/com/android/server/accessibility/PolicyWarningUIController.java
+++ b/services/accessibility/java/com/android/server/accessibility/PolicyWarningUIController.java
@@ -95,7 +95,7 @@
         filter.addAction(ACTION_A11Y_SETTINGS);
         filter.addAction(ACTION_DISMISS_NOTIFICATION);
         mContext.registerReceiver(mNotificationController, filter,
-                Manifest.permission.MANAGE_ACCESSIBILITY, mMainHandler);
+                Manifest.permission.MANAGE_ACCESSIBILITY, mMainHandler, Context.RECEIVER_EXPORTED);
 
     }
 
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index 012c5b1..5e2449d 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -209,7 +209,8 @@
 
         final IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
-        context.registerReceiver(mBroadcastReceiver, filter, null, FgThread.getHandler());
+        context.registerReceiver(mBroadcastReceiver, filter, null, FgThread.getHandler(),
+                Context.RECEIVER_NOT_EXPORTED);
 
         mAugmentedAutofillResolver = new FrameworkResourcesServiceNameResolver(getContext(),
                 com.android.internal.R.string.config_defaultAugmentedAutofillService);
diff --git a/services/companion/OWNERS b/services/companion/OWNERS
new file mode 100644
index 0000000..cb4cc56
--- /dev/null
+++ b/services/companion/OWNERS
@@ -0,0 +1,4 @@
+evanxinchen@google.com
+ewol@google.com
+guojing@google.com
+svetoslavganov@google.com
\ No newline at end of file
diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java
index 850c3ff..9e4e1a1 100644
--- a/services/core/java/android/content/pm/PackageManagerInternal.java
+++ b/services/core/java/android/content/pm/PackageManagerInternal.java
@@ -1251,4 +1251,9 @@
      * @return the number of freed bytes or -1 if there was an error in the process.
      */
     public abstract long deleteOatArtifactsOfPackage(String packageName);
+
+    /**
+     * Reconcile all app data for the given user.
+     */
+    public abstract void reconcileAppsData(int userId, int flags, boolean migrateAppsData);
 }
diff --git a/services/core/java/com/android/server/SensorPrivacyService.java b/services/core/java/com/android/server/SensorPrivacyService.java
index 57ce342..7a4d11c 100644
--- a/services/core/java/com/android/server/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/SensorPrivacyService.java
@@ -39,6 +39,7 @@
 import static android.hardware.SensorPrivacyManager.Sources.QS_TILE;
 import static android.hardware.SensorPrivacyManager.Sources.SETTINGS;
 import static android.hardware.SensorPrivacyManager.Sources.SHELL;
+import static android.os.UserHandle.USER_NULL;
 import static android.os.UserHandle.USER_SYSTEM;
 import static android.service.SensorPrivacyIndividualEnabledSensorProto.UNKNOWN;
 
@@ -195,7 +196,7 @@
     private EmergencyCallHelper mEmergencyCallHelper;
     private KeyguardManager mKeyguardManager;
 
-    private int mCurrentUser = -1;
+    private int mCurrentUser = USER_NULL;
 
     public SensorPrivacyService(Context context) {
         super(context);
@@ -228,9 +229,9 @@
 
     @Override
     public void onUserStarting(TargetUser user) {
-        if (mCurrentUser == -1) {
+        if (mCurrentUser == USER_NULL) {
             mCurrentUser = user.getUserIdentifier();
-            mSensorPrivacyServiceImpl.userSwitching(-1, user.getUserIdentifier());
+            mSensorPrivacyServiceImpl.userSwitching(USER_NULL, user.getUserIdentifier());
         }
     }
 
@@ -347,7 +348,7 @@
                             intent.getIntExtra(EXTRA_SENSOR, UNKNOWN), false);
                 }
             }, new IntentFilter(ACTION_DISABLE_INDIVIDUAL_SENSOR_PRIVACY),
-                    MANAGE_SENSOR_PRIVACY, null);
+                    MANAGE_SENSOR_PRIVACY, null, Context.RECEIVER_EXPORTED);
             mUserManagerInternal.addUserRestrictionsListener(this);
         }
 
@@ -1294,13 +1295,13 @@
                 micState = isIndividualSensorPrivacyEnabledLocked(to, MICROPHONE);
                 camState = isIndividualSensorPrivacyEnabledLocked(to, CAMERA);
             }
-            if (prevMicState != micState) {
+            if (from == USER_NULL || prevMicState != micState) {
                 mHandler.onUserGlobalSensorPrivacyChanged(MICROPHONE, micState);
                 setGlobalRestriction(MICROPHONE, micState);
             }
-            if (prevCamState != camState) {
+            if (from == USER_NULL || prevCamState != camState) {
                 mHandler.onUserGlobalSensorPrivacyChanged(CAMERA, camState);
-                setGlobalRestriction(CAMERA, micState);
+                setGlobalRestriction(CAMERA, camState);
             }
         }
 
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 63301ac..fab58e8 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -385,6 +385,10 @@
     private int[] mAllowedNetworkTypeReason;
     private long[] mAllowedNetworkTypeValue;
 
+    private static final List<LinkCapacityEstimate> INVALID_LCE_LIST =
+            new ArrayList<LinkCapacityEstimate>(Arrays.asList(new LinkCapacityEstimate(
+            LinkCapacityEstimate.LCE_TYPE_COMBINED,
+            LinkCapacityEstimate.INVALID, LinkCapacityEstimate.INVALID)));
     private List<List<LinkCapacityEstimate>> mLinkCapacityEstimateLists;
 
     /**
@@ -719,7 +723,7 @@
             mPhysicalChannelConfigs.add(i, new ArrayList<>());
             mAllowedNetworkTypeReason[i] = -1;
             mAllowedNetworkTypeValue[i] = -1;
-            mLinkCapacityEstimateLists.add(i, new ArrayList<>());
+            mLinkCapacityEstimateLists.add(i, INVALID_LCE_LIST);
         }
     }
 
@@ -819,7 +823,7 @@
             mPhysicalChannelConfigs.add(i, new ArrayList<>());
             mAllowedNetworkTypeReason[i] = -1;
             mAllowedNetworkTypeValue[i] = -1;
-            mLinkCapacityEstimateLists.add(i, new ArrayList<>());
+            mLinkCapacityEstimateLists.add(i, INVALID_LCE_LIST);
         }
 
         mAppOps = mContext.getSystemService(AppOpsManager.class);
diff --git a/services/core/java/com/android/server/VpnManagerService.java b/services/core/java/com/android/server/VpnManagerService.java
index a03425c..b48e21e 100644
--- a/services/core/java/com/android/server/VpnManagerService.java
+++ b/services/core/java/com/android/server/VpnManagerService.java
@@ -681,7 +681,7 @@
         intentFilter = new IntentFilter();
         intentFilter.addAction(LockdownVpnTracker.ACTION_LOCKDOWN_RESET);
         mUserAllContext.registerReceiver(
-                mIntentReceiver, intentFilter, NETWORK_STACK, mHandler);
+                mIntentReceiver, intentFilter, NETWORK_STACK, mHandler, Context.RECEIVER_EXPORTED);
     }
 
     private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index 46efa3c..e7b078a 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -143,6 +143,8 @@
     );
 
     public static final String[] AIDL_INTERFACE_PREFIXES_OF_INTEREST = new String[] {
+            "android.hardware.biometrics.face.IFace/",
+            "android.hardware.biometrics.fingerprint.IFingerprint/",
             "android.hardware.light.ILights/",
             "android.hardware.power.stats.IPowerStats/",
     };
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 2b30943..1997897 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -5000,12 +5000,15 @@
                 // Since we kicked off all its pending restarts, there could be some open slots
                 // in the pending restarts list, schedule a check on it. We are posting to the same
                 // handler, so by the time of the check, those immediate restarts should be done.
-                mAm.mHandler.post(() ->
+                mAm.mHandler.post(() -> {
+                    final long now = SystemClock.uptimeMillis();
+                    synchronized (mAm) {
                         rescheduleServiceRestartIfPossibleLocked(
                                 getExtraRestartTimeInBetweenLocked(),
                                 mAm.mConstants.SERVICE_MIN_RESTART_TIME_BETWEEN,
-                                "other", SystemClock.uptimeMillis())
-                );
+                                "other", now);
+                    }
+                });
             }
         }
         return didSomething;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 6a63254..b194930 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -188,6 +188,7 @@
 import android.app.WaitResult;
 import android.app.backup.BackupManager.OperationType;
 import android.app.backup.IBackupManager;
+import android.app.compat.CompatChanges;
 import android.app.job.JobParameters;
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageEvents.Event;
@@ -195,7 +196,6 @@
 import android.app.usage.UsageStatsManagerInternal;
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetManagerInternal;
-import android.compat.Compatibility;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.content.AttributionSource;
@@ -5091,14 +5091,19 @@
     @Override
     public PendingIntentInfo getInfoForIntentSender(IIntentSender sender) {
         if (sender instanceof PendingIntentRecord) {
-            PendingIntentRecord res = (PendingIntentRecord) sender;
+            final PendingIntentRecord res = (PendingIntentRecord) sender;
+            final String packageName = res.key.packageName;
+            final int uid = res.uid;
+            final boolean shouldFilter = getPackageManagerInternal().filterAppAccess(
+                    packageName, Binder.getCallingUid(), UserHandle.getUserId(uid));
             return new PendingIntentInfo(
-                    res.key.packageName,
-                    res.uid,
+                    shouldFilter ? null : packageName,
+                    shouldFilter ? INVALID_UID : uid,
                     (res.key.flags & PendingIntent.FLAG_IMMUTABLE) != 0,
                     res.key.type);
         } else {
-            return new PendingIntentInfo(null, -1, false, ActivityManager.INTENT_SENDER_UNKNOWN);
+            return new PendingIntentInfo(null, INVALID_UID, false,
+                    ActivityManager.INTENT_SENDER_UNKNOWN);
         }
     }
 
@@ -12512,11 +12517,6 @@
         ProcessRecord callerApp = null;
         final boolean visibleToInstantApps
                 = (flags & Context.RECEIVER_VISIBLE_TO_INSTANT_APPS) != 0;
-        // Dynamic receivers are exported by default for versions prior to T
-        final boolean exported =
-                ((flags & Context.RECEIVER_EXPORTED) != 0
-                        || (!Compatibility.isChangeEnabled(
-                                DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED)));
 
         int callingUid;
         int callingPid;
@@ -12589,8 +12589,8 @@
             // If the caller is registering for a sticky broadcast with a null receiver, we won't
             // require a flag
             if (!onlyProtectedBroadcasts && receiver != null && (
-                    Compatibility.isChangeEnabled(
-                            DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED)
+                    CompatChanges.isChangeEnabled(
+                            DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED, callingUid)
                             && (flags & (Context.RECEIVER_EXPORTED | Context.RECEIVER_NOT_EXPORTED))
                             == 0)) {
                 Slog.e(TAG,
@@ -12605,6 +12605,12 @@
             }
         }
 
+        // Dynamic receivers are exported by default for versions prior to T
+        final boolean exported =
+                ((flags & Context.RECEIVER_EXPORTED) != 0
+                        || (!CompatChanges.isChangeEnabled(
+                        DYNAMIC_RECEIVER_EXPLICIT_EXPORT_REQUIRED, callingUid)));
+
         ArrayList<Intent> allSticky = null;
         if (stickyIntents != null) {
             final ContentResolver resolver = mContext.getContentResolver();
diff --git a/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java
index 3c6b682..6fe9f8e 100644
--- a/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java
+++ b/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java
@@ -117,6 +117,9 @@
     private int mScreenState;
 
     @GuardedBy("this")
+    private int[] mPerDisplayScreenStates = null;
+
+    @GuardedBy("this")
     private boolean mUseLatestStates = true;
 
     @GuardedBy("this")
@@ -294,8 +297,8 @@
     }
 
     @Override
-    public Future<?> scheduleSyncDueToScreenStateChange(
-            int flags, boolean onBattery, boolean onBatteryScreenOff, int screenState) {
+    public Future<?> scheduleSyncDueToScreenStateChange(int flags, boolean onBattery,
+            boolean onBatteryScreenOff, int screenState, int[] perDisplayScreenStates) {
         synchronized (BatteryExternalStatsWorker.this) {
             if (mCurrentFuture == null || (mUpdateFlags & UPDATE_CPU) == 0) {
                 mOnBattery = onBattery;
@@ -304,6 +307,7 @@
             }
             // always update screen state
             mScreenState = screenState;
+            mPerDisplayScreenStates = perDisplayScreenStates;
             return scheduleSyncLocked("screen-state", flags);
         }
     }
@@ -446,6 +450,7 @@
             final boolean onBattery;
             final boolean onBatteryScreenOff;
             final int screenState;
+            final int[] displayScreenStates;
             final boolean useLatestStates;
             synchronized (BatteryExternalStatsWorker.this) {
                 updateFlags = mUpdateFlags;
@@ -454,6 +459,7 @@
                 onBattery = mOnBattery;
                 onBatteryScreenOff = mOnBatteryScreenOff;
                 screenState = mScreenState;
+                displayScreenStates = mPerDisplayScreenStates;
                 useLatestStates = mUseLatestStates;
                 mUpdateFlags = 0;
                 mCurrentReason = null;
@@ -475,7 +481,8 @@
                     }
                     try {
                         updateExternalStatsLocked(reason, updateFlags, onBattery,
-                                onBatteryScreenOff, screenState, useLatestStates);
+                                onBatteryScreenOff, screenState, displayScreenStates,
+                                useLatestStates);
                     } finally {
                         if (DEBUG) {
                             Slog.d(TAG, "end updateExternalStatsSync");
@@ -520,7 +527,8 @@
 
     @GuardedBy("mWorkerLock")
     private void updateExternalStatsLocked(final String reason, int updateFlags, boolean onBattery,
-            boolean onBatteryScreenOff, int screenState, boolean useLatestStates) {
+            boolean onBatteryScreenOff, int screenState, int[] displayScreenStates,
+            boolean useLatestStates) {
         // We will request data from external processes asynchronously, and wait on a timeout.
         SynchronousResultReceiver wifiReceiver = null;
         SynchronousResultReceiver bluetoothReceiver = null;
@@ -673,11 +681,14 @@
 
             // Inform mStats about each applicable measured energy (unless addressed elsewhere).
             if (measuredEnergyDeltas != null) {
-                final long displayChargeUC = measuredEnergyDeltas.displayChargeUC;
-                if (displayChargeUC != MeasuredEnergySnapshot.UNAVAILABLE) {
+                final long[] displayChargeUC = measuredEnergyDeltas.displayChargeUC;
+                if (displayChargeUC != null && displayChargeUC.length > 0) {
+                    // TODO (b/194107383): pass all display ordinals to mStats with
+                    //  displayScreenStates
+                    final long primaryDisplayChargeUC = displayChargeUC[0];
                     // If updating, pass in what BatteryExternalStatsWorker thinks screenState is.
-                    mStats.updateDisplayMeasuredEnergyStatsLocked(displayChargeUC, screenState,
-                            elapsedRealtime);
+                    mStats.updateDisplayMeasuredEnergyStatsLocked(primaryDisplayChargeUC,
+                            screenState, elapsedRealtime);
                 }
 
                 final long gnssChargeUC = measuredEnergyDeltas.gnssChargeUC;
@@ -962,6 +973,7 @@
                 switch (consumer.type) {
                     case EnergyConsumerType.OTHER:
                     case EnergyConsumerType.CPU_CLUSTER:
+                    case EnergyConsumerType.DISPLAY:
                         break;
                     default:
                         Slog.w(TAG, "EnergyConsumer '" + consumer.name + "' has unexpected ordinal "
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 60530a3..03a4d84 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -1215,7 +1215,7 @@
             mHandler.post(() -> {
                 if (DBG) Slog.d(TAG, "begin noteScreenState");
                 synchronized (mStats) {
-                    mStats.noteScreenStateLocked(state, elapsedRealtime, uptime, currentTime);
+                    mStats.noteScreenStateLocked(0, state, elapsedRealtime, uptime, currentTime);
                 }
                 if (DBG) Slog.d(TAG, "end noteScreenState");
             });
@@ -1230,7 +1230,7 @@
             final long uptime = SystemClock.uptimeMillis();
             mHandler.post(() -> {
                 synchronized (mStats) {
-                    mStats.noteScreenBrightnessLocked(brightness, elapsedRealtime, uptime);
+                    mStats.noteScreenBrightnessLocked(0, brightness, elapsedRealtime, uptime);
                 }
             });
         }
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index ed70d2b..8638c7d 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -1588,17 +1588,23 @@
                     perm = PackageManager.PERMISSION_DENIED;
                 }
 
-                if (perm == PackageManager.PERMISSION_GRANTED) {
-                    skip = true;
-                    break;
-                }
-
                 int appOp = AppOpsManager.permissionToOpCode(excludedPermission);
                 if (appOp != AppOpsManager.OP_NONE) {
-                    if (mService.getAppOpsManager().checkOpNoThrow(appOp,
+                    // When there is an app op associated with the permission,
+                    // skip when both the permission and the app op are
+                    // granted.
+                    if ((perm == PackageManager.PERMISSION_GRANTED) && (
+                                mService.getAppOpsManager().checkOpNoThrow(appOp,
                                 info.activityInfo.applicationInfo.uid,
                                 info.activityInfo.packageName)
-                            == AppOpsManager.MODE_ALLOWED) {
+                            == AppOpsManager.MODE_ALLOWED)) {
+                        skip = true;
+                        break;
+                    }
+                } else {
+                    // When there is no app op associated with the permission,
+                    // skip when permission is granted.
+                    if (perm == PackageManager.PERMISSION_GRANTED) {
                         skip = true;
                         break;
                     }
diff --git a/services/core/java/com/android/server/am/MeasuredEnergySnapshot.java b/services/core/java/com/android/server/am/MeasuredEnergySnapshot.java
index a9fca4f..0359aa5 100644
--- a/services/core/java/com/android/server/am/MeasuredEnergySnapshot.java
+++ b/services/core/java/com/android/server/am/MeasuredEnergySnapshot.java
@@ -49,6 +49,9 @@
     /** Number of ordinals for {@link EnergyConsumerType#CPU_CLUSTER}. */
     private final int mNumCpuClusterOrdinals;
 
+    /** Number of ordinals for {@link EnergyConsumerType#DISPLAY}. */
+    private final int mNumDisplayOrdinals;
+
     /** Number of ordinals for {@link EnergyConsumerType#OTHER}. */
     private final int mNumOtherOrdinals;
 
@@ -95,6 +98,7 @@
 
         mNumCpuClusterOrdinals = calculateNumOrdinals(EnergyConsumerType.CPU_CLUSTER,
                 idToConsumerMap);
+        mNumDisplayOrdinals = calculateNumOrdinals(EnergyConsumerType.DISPLAY, idToConsumerMap);
         mNumOtherOrdinals = calculateNumOrdinals(EnergyConsumerType.OTHER, idToConsumerMap);
         mAttributionSnapshots = new SparseArray<>(mNumOtherOrdinals);
     }
@@ -108,7 +112,7 @@
         public long[] cpuClusterChargeUC = null;
 
         /** The chargeUC for {@link EnergyConsumerType#DISPLAY}. */
-        public long displayChargeUC = UNAVAILABLE;
+        public long[] displayChargeUC = null;
 
         /** The chargeUC for {@link EnergyConsumerType#GNSS}. */
         public long gnssChargeUC = UNAVAILABLE;
@@ -212,7 +216,10 @@
                     break;
 
                 case EnergyConsumerType.DISPLAY:
-                    output.displayChargeUC = deltaChargeUC;
+                    if (output.displayChargeUC == null) {
+                        output.displayChargeUC = new long[mNumDisplayOrdinals];
+                    }
+                    output.displayChargeUC[ordinal]  = deltaChargeUC;
                     break;
 
                 case EnergyConsumerType.GNSS:
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 84a3060..427e90a 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1714,10 +1714,12 @@
         MESSAGES_MUTE_MUSIC = new HashSet<>();
         MESSAGES_MUTE_MUSIC.add(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED);
         MESSAGES_MUTE_MUSIC.add(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED);
+        MESSAGES_MUTE_MUSIC.add(MSG_IL_SET_LE_AUDIO_OUT_CONNECTION_STATE);
         MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONFIG_CHANGE);
         MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_ACTIVE_DEVICE_CHANGE);
         MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION);
         MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION);
+        MESSAGES_MUTE_MUSIC.add(MSG_L_LE_AUDIO_DEVICE_OUT_CONNECTION_CHANGE_EXT);
         MESSAGES_MUTE_MUSIC.add(MSG_IIL_SET_FORCE_BT_A2DP_USE);
         MESSAGES_MUTE_MUSIC.add(MSG_REPORT_NEW_ROUTES_A2DP);
     }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 289396e..e8b0e08 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -97,6 +97,7 @@
 import android.media.ISpatializerCallback;
 import android.media.ISpatializerHeadToSoundStagePoseCallback;
 import android.media.ISpatializerHeadTrackingModeCallback;
+import android.media.ISpatializerOutputCallback;
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.IVolumeController;
 import android.media.MediaMetrics;
@@ -1122,7 +1123,8 @@
         intentFilter.addAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
         intentFilter.addAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
 
-        mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, intentFilter, null, null);
+        mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, intentFilter, null, null,
+                Context.RECEIVER_EXPORTED);
 
     }
 
@@ -8508,6 +8510,26 @@
         mSpatializerHelper.getEffectParameter(key, value);
     }
 
+    /** @see Spatializer#getOutput */
+    public int getSpatializerOutput() {
+        enforceModifyDefaultAudioEffectsPermission();
+        return mSpatializerHelper.getOutput();
+    }
+
+    /** @see Spatializer#setOnSpatializerOutputChangedListener */
+    public void registerSpatializerOutputCallback(ISpatializerOutputCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.registerSpatializerOutputCallback(cb);
+    }
+
+    /** @see Spatializer#clearOnSpatializerOutputChangedListener */
+    public void unregisterSpatializerOutputCallback(ISpatializerOutputCallback cb) {
+        enforceModifyDefaultAudioEffectsPermission();
+        Objects.requireNonNull(cb);
+        mSpatializerHelper.unregisterSpatializerOutputCallback(cb);
+    }
+
     /**
      * post a message to schedule init/release of head tracking sensors
      * @param init initialization if true, release if false
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index b2fa86b..7cd027c 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -31,6 +31,7 @@
 import android.media.ISpatializerHeadToSoundStagePoseCallback;
 import android.media.ISpatializerHeadTrackingCallback;
 import android.media.ISpatializerHeadTrackingModeCallback;
+import android.media.ISpatializerOutputCallback;
 import android.media.SpatializationLevel;
 import android.media.Spatializer;
 import android.media.SpatializerHeadTrackingMode;
@@ -76,6 +77,7 @@
     private int mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
     private int mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
     private int mDesiredHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
+    private int mSpatOutput = 0;
     private @Nullable ISpatializer mSpat;
     private @Nullable SpatializerCallback mSpatCallback;
     private @Nullable SpatializerHeadTrackingCallback mSpatHeadTrackingCallback;
@@ -213,6 +215,18 @@
                 postInitSensors(true);
             }
         }
+
+        public void onOutputChanged(int output) {
+            logd("SpatializerCallback.onOutputChanged output:" + output);
+            int oldOutput;
+            synchronized (SpatializerHelper.this) {
+                oldOutput = mSpatOutput;
+                mSpatOutput = output;
+            }
+            if (oldOutput != output) {
+                dispatchOutputUpdate(output);
+            }
+        }
     };
 
     // spatializer head tracking callback from native
@@ -782,6 +796,60 @@
     }
 
     //------------------------------------------------------
+    // output
+
+    /** @see Spatializer#getOutput */
+    synchronized int getOutput() {
+        switch (mState) {
+            case STATE_UNINITIALIZED:
+            case STATE_NOT_SUPPORTED:
+                throw (new IllegalStateException(
+                        "Can't get output without a spatializer"));
+            case STATE_ENABLED_UNAVAILABLE:
+            case STATE_DISABLED_UNAVAILABLE:
+            case STATE_DISABLED_AVAILABLE:
+            case STATE_ENABLED_AVAILABLE:
+                if (mSpat == null) {
+                    throw (new IllegalStateException(
+                            "null Spatializer for getOutput"));
+                }
+                break;
+        }
+        // mSpat != null
+        try {
+            return mSpat.getOutput();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in getOutput", e);
+            return 0;
+        }
+    }
+
+    final RemoteCallbackList<ISpatializerOutputCallback> mOutputCallbacks =
+            new RemoteCallbackList<ISpatializerOutputCallback>();
+
+    synchronized void registerSpatializerOutputCallback(
+            @NonNull ISpatializerOutputCallback callback) {
+        mOutputCallbacks.register(callback);
+    }
+
+    synchronized void unregisterSpatializerOutputCallback(
+            @NonNull ISpatializerOutputCallback callback) {
+        mOutputCallbacks.unregister(callback);
+    }
+
+    private void dispatchOutputUpdate(int output) {
+        final int nbCallbacks = mOutputCallbacks.beginBroadcast();
+        for (int i = 0; i < nbCallbacks; i++) {
+            try {
+                mOutputCallbacks.getBroadcastItem(i).dispatchSpatializerOutputChanged(output);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in dispatchOutputUpdate", e);
+            }
+        }
+        mOutputCallbacks.finishBroadcast();
+    }
+
+    //------------------------------------------------------
     // sensors
     private void initSensors(boolean init) {
         if (mSensorManager == null) {
@@ -825,9 +893,18 @@
     }
 
     synchronized void onInitSensors(boolean init) {
-        final int[] modes = getSupportedHeadTrackingModes();
-        if (modes.length == 0) {
-            Log.i(TAG, "not initializing sensors, no headtracking supported");
+        final String action = init ? "initializing" : "releasing";
+        if (mSpat == null) {
+            Log.e(TAG, "not " + action + " sensors, null spatializer");
+            return;
+        }
+        try {
+            if (!mSpat.isHeadTrackingSupported()) {
+                Log.e(TAG, "not " + action + " sensors, spatializer doesn't support headtracking");
+                return;
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "not " + action + " sensors, error querying headtracking", e);
             return;
         }
         initSensors(init);
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
index dc5dace9..a0befea 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/LockoutFrameworkImpl.java
@@ -79,7 +79,7 @@
         mLockoutReceiver = new LockoutReceiver();
 
         context.registerReceiver(mLockoutReceiver, new IntentFilter(ACTION_LOCKOUT_RESET),
-                RESET_FINGERPRINT_LOCKOUT, null /* handler */);
+                RESET_FINGERPRINT_LOCKOUT, null /* handler */, Context.RECEIVER_EXPORTED);
     }
 
     // Attempt counter should only be cleared when Keyguard goes away or when
diff --git a/services/core/java/com/android/server/communal/CommunalManagerService.java b/services/core/java/com/android/server/communal/CommunalManagerService.java
new file mode 100644
index 0000000..dc2b418
--- /dev/null
+++ b/services/core/java/com/android/server/communal/CommunalManagerService.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.communal;
+
+import static android.app.ActivityManager.INTENT_SENDER_ACTIVITY;
+
+import static com.android.server.wm.ActivityInterceptorCallback.COMMUNAL_MODE_ORDERED_ID;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.app.KeyguardManager;
+import android.app.PendingIntent;
+import android.app.communal.ICommunalManager;
+import android.content.Context;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.ActivityInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.LaunchAfterAuthenticationActivity;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.wm.ActivityInterceptorCallback;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * System service for handling Communal Mode state.
+ */
+public final class CommunalManagerService extends SystemService {
+    private static final String DELIMITER = ",";
+    private final Context mContext;
+    private final ActivityTaskManagerInternal mAtmInternal;
+    private final KeyguardManager mKeyguardManager;
+    private final AtomicBoolean mCommunalViewIsShowing = new AtomicBoolean(false);
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final Set<String> mEnabledApps = new HashSet<>();
+    private final SettingsObserver mSettingsObserver;
+
+    private final ActivityInterceptorCallback mActivityInterceptorCallback =
+            new ActivityInterceptorCallback() {
+                @Nullable
+                @Override
+                public Intent intercept(ActivityInterceptorInfo info) {
+                    if (isActivityAllowed(info.aInfo)) {
+                        return null;
+                    }
+
+                    final IIntentSender target = mAtmInternal.getIntentSender(
+                            INTENT_SENDER_ACTIVITY,
+                            info.callingPackage,
+                            info.callingFeatureId,
+                            info.callingUid,
+                            info.userId,
+                            /* token= */null,
+                            /* resultWho= */ null,
+                            /* requestCode= */ 0,
+                            new Intent[]{info.intent},
+                            new String[]{info.resolvedType},
+                            PendingIntent.FLAG_IMMUTABLE,
+                            /* bOptions= */ null);
+
+                    return LaunchAfterAuthenticationActivity.createLaunchAfterAuthenticationIntent(
+                            new IntentSender(target));
+
+                }
+            };
+
+    public CommunalManagerService(Context context) {
+        super(context);
+        mContext = context;
+        mSettingsObserver = new SettingsObserver();
+        mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+        mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(Context.COMMUNAL_MANAGER_SERVICE, new BinderService());
+        mAtmInternal.registerActivityStartInterceptor(COMMUNAL_MODE_ORDERED_ID,
+                mActivityInterceptorCallback);
+
+
+        updateSelectedApps();
+        mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor(
+                Settings.Secure.COMMUNAL_MODE_PACKAGES), false, mSettingsObserver,
+                UserHandle.USER_SYSTEM);
+    }
+
+    @VisibleForTesting
+    void updateSelectedApps() {
+        final String encodedApps = Settings.Secure.getStringForUser(
+                mContext.getContentResolver(),
+                Settings.Secure.COMMUNAL_MODE_PACKAGES,
+                UserHandle.USER_SYSTEM);
+
+        mEnabledApps.clear();
+
+        if (!TextUtils.isEmpty(encodedApps)) {
+            mEnabledApps.addAll(Arrays.asList(encodedApps.split(DELIMITER)));
+        }
+    }
+
+    private boolean isActivityAllowed(ActivityInfo activityInfo) {
+        if (!mCommunalViewIsShowing.get() || !mKeyguardManager.isKeyguardLocked()) return true;
+
+        // If the activity doesn't have showWhenLocked enabled, disallow the activity.
+        final boolean showWhenLocked =
+                (activityInfo.flags & ActivityInfo.FLAG_SHOW_WHEN_LOCKED) != 0;
+        if (!showWhenLocked) {
+            return false;
+        }
+
+        // Check the cached user preferences to see if the user has allowed this app.
+        return mEnabledApps.contains(activityInfo.applicationInfo.packageName);
+    }
+
+    private final class SettingsObserver extends ContentObserver {
+        SettingsObserver() {
+            super(mHandler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            mContext.getMainExecutor().execute(CommunalManagerService.this::updateSelectedApps);
+        }
+    }
+
+    private final class BinderService extends ICommunalManager.Stub {
+        /**
+         * Sets whether or not we are in communal mode.
+         */
+        @RequiresPermission(Manifest.permission.WRITE_COMMUNAL_STATE)
+        @Override
+        public void setCommunalViewShowing(boolean isShowing) {
+            mContext.enforceCallingPermission(Manifest.permission.WRITE_COMMUNAL_STATE,
+                    Manifest.permission.WRITE_COMMUNAL_STATE
+                            + "permission required to modify communal state.");
+            mCommunalViewIsShowing.set(isShowing);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/communal/TEST_MAPPING b/services/core/java/com/android/server/communal/TEST_MAPPING
new file mode 100644
index 0000000..026e9bb
--- /dev/null
+++ b/services/core/java/com/android/server/communal/TEST_MAPPING
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "FrameworksMockingServicesTests",
+      "options": [
+        {
+          "include-filter": "com.android.server.communal"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/criticalevents/OWNERS b/services/core/java/com/android/server/criticalevents/OWNERS
new file mode 100644
index 0000000..9c3136c
--- /dev/null
+++ b/services/core/java/com/android/server/criticalevents/OWNERS
@@ -0,0 +1,2 @@
+benmiles@google.com
+gaillard@google.com
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 045ee8a..5178f35 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1632,13 +1632,13 @@
             mDisplayModeDirector.getAppRequestObserver().setAppRequest(
                     displayId, requestedModeId, requestedMinRefreshRate, requestedMaxRefreshRate);
 
-            if (display.getDisplayInfoLocked().minimalPostProcessingSupported) {
-                boolean mppRequest = mMinimalPostProcessingAllowed && preferMinimalPostProcessing;
+            // TODO(b/202378408) set minimal post-processing only if it's supported once we have a
+            // separate API for disabling on-device processing.
+            boolean mppRequest = mMinimalPostProcessingAllowed && preferMinimalPostProcessing;
 
-                if (display.getRequestedMinimalPostProcessingLocked() != mppRequest) {
-                    display.setRequestedMinimalPostProcessingLocked(mppRequest);
-                    shouldScheduleTraversal = true;
-                }
+            if (display.getRequestedMinimalPostProcessingLocked() != mppRequest) {
+                display.setRequestedMinimalPostProcessingLocked(mppRequest);
+                shouldScheduleTraversal = true;
             }
 
             if (shouldScheduleTraversal) {
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index d6c28e2..87b9e6a 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -17,6 +17,7 @@
 package com.android.server.display;
 
 import static android.hardware.display.DisplayManagerInternal.REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE;
+import static android.os.PowerManager.BRIGHTNESS_INVALID;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -40,6 +41,7 @@
 import android.os.IThermalService;
 import android.os.Looper;
 import android.os.Message;
+import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
@@ -61,6 +63,7 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.os.BackgroundThread;
 import com.android.server.LocalServices;
 import com.android.server.display.utils.AmbientFilter;
@@ -153,7 +156,7 @@
         mAppRequestObserver = new AppRequestObserver();
         mSettingsObserver = new SettingsObserver(context, handler);
         mDisplayObserver = new DisplayObserver(context, handler);
-        mBrightnessObserver = new BrightnessObserver(context, handler);
+        mBrightnessObserver = new BrightnessObserver(context, handler, injector);
         mUdfpsObserver = new UdfpsObserver();
         final BallotBox ballotBox = (displayId, priority, vote) -> {
             synchronized (mLock) {
@@ -1425,8 +1428,6 @@
         @Override
         public void onDisplayChanged(int displayId) {
             updateDisplayModes(displayId);
-            // TODO: Break the coupling between DisplayObserver and BrightnessObserver.
-            mBrightnessObserver.onDisplayChanged(displayId);
         }
 
         private void updateDisplayModes(int displayId) {
@@ -1463,7 +1464,7 @@
      * {@link R.array#config_ambientThresholdsOfPeakRefreshRate}.
      */
     @VisibleForTesting
-    public class BrightnessObserver extends ContentObserver {
+    public class BrightnessObserver implements DisplayManager.DisplayListener {
         private final static int LIGHT_SENSOR_RATE_MS = 250;
         private int[] mLowDisplayBrightnessThresholds;
         private int[] mLowAmbientBrightnessThresholds;
@@ -1486,6 +1487,8 @@
         private int mBrightness = -1;
 
         private final Context mContext;
+        private final Injector mInjector;
+        private final Handler mHandler;
 
         // Enable light sensor only when mShouldObserveAmbientLowChange is true or
         // mShouldObserveAmbientHighChange is true, screen is on, peak refresh rate
@@ -1498,9 +1501,11 @@
         private int mRefreshRateInLowZone;
         private int mRefreshRateInHighZone;
 
-        BrightnessObserver(Context context, Handler handler) {
-            super(handler);
+        BrightnessObserver(Context context, Handler handler, Injector injector) {
             mContext = context;
+            mHandler = handler;
+            mInjector = injector;
+
             mLowDisplayBrightnessThresholds = context.getResources().getIntArray(
                     R.array.config_brightnessThresholdsOfPeakRefreshRate);
             mLowAmbientBrightnessThresholds = context.getResources().getIntArray(
@@ -1567,8 +1572,7 @@
         public void observe(SensorManager sensorManager) {
             mSensorManager = sensorManager;
             final ContentResolver cr = mContext.getContentResolver();
-            mBrightness = Settings.System.getIntForUser(cr,
-                    Settings.System.SCREEN_BRIGHTNESS, -1 /*default*/, cr.getUserId());
+            mBrightness = getBrightness(Display.DEFAULT_DISPLAY);
 
             // DeviceConfig is accessible after system ready.
             int[] lowDisplayBrightnessThresholds =
@@ -1601,6 +1605,10 @@
 
             restartObserver();
             mDeviceConfigDisplaySettings.startListening();
+
+            mInjector.registerDisplayListener(this, mHandler,
+                    DisplayManager.EVENT_FLAG_DISPLAY_CHANGED |
+                    DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS);
         }
 
         public void setLoggingEnabled(boolean loggingEnabled) {
@@ -1716,28 +1724,30 @@
             }
         }
 
+        @Override
+        public void onDisplayAdded(int displayId) {}
+
+        @Override
+        public void onDisplayRemoved(int displayId) {}
+
+        @Override
         public void onDisplayChanged(int displayId) {
             if (displayId == Display.DEFAULT_DISPLAY) {
                 updateDefaultDisplayState();
-            }
-        }
 
-        @Override
-        public void onChange(boolean selfChange, Uri uri, int userId) {
-            synchronized (mLock) {
-                final ContentResolver cr = mContext.getContentResolver();
-                int brightness = Settings.System.getIntForUser(cr,
-                        Settings.System.SCREEN_BRIGHTNESS, -1 /*default*/, cr.getUserId());
-                if (brightness != mBrightness) {
-                    mBrightness = brightness;
-                    onBrightnessChangedLocked();
+                // We don't support multiple display blocking zones yet, so only handle
+                // brightness changes for the default display for now.
+                int brightness = getBrightness(displayId);
+                synchronized (mLock) {
+                    if (brightness != mBrightness) {
+                        mBrightness = brightness;
+                        onBrightnessChangedLocked();
+                    }
                 }
             }
         }
 
         private void restartObserver() {
-            final ContentResolver cr = mContext.getContentResolver();
-
             if (mRefreshRateInLowZone > 0) {
                 mShouldObserveDisplayLowChange = hasValidThreshold(
                         mLowDisplayBrightnessThresholds);
@@ -1758,15 +1768,6 @@
                 mShouldObserveAmbientHighChange = false;
             }
 
-            if (mShouldObserveDisplayLowChange || mShouldObserveDisplayHighChange) {
-                // Content Service does not check if an listener has already been registered.
-                // To ensure only one listener is registered, force an unregistration first.
-                mInjector.unregisterBrightnessObserver(cr, this);
-                mInjector.registerBrightnessObserver(cr, this);
-            } else {
-                mInjector.unregisterBrightnessObserver(cr, this);
-            }
-
             if (mShouldObserveAmbientLowChange || mShouldObserveAmbientHighChange) {
                 Resources resources = mContext.getResources();
                 String lightSensorType = resources.getString(
@@ -1966,6 +1967,15 @@
             return mDefaultDisplayState == Display.STATE_ON;
         }
 
+        private int getBrightness(int displayId) {
+            final BrightnessInfo info = mInjector.getBrightnessInfo(displayId);
+            if (info != null) {
+                return BrightnessSynchronizer.brightnessFloatToInt(info.adjustedBrightness);
+            }
+
+            return BRIGHTNESS_INVALID;
+        }
+
         private final class LightSensorEventListener implements SensorEventListener {
             final private static int INJECT_EVENTS_INTERVAL_MS = LIGHT_SENSOR_RATE_MS;
             private float mLastSensorData;
@@ -2283,6 +2293,7 @@
         private final BallotBox mBallotBox;
         private final Handler mHandler;
         private final SparseIntArray mHbmMode = new SparseIntArray();
+        private final SparseBooleanArray mHbmActive = new SparseBooleanArray();
         private final Injector mInjector;
         private final DeviceConfigDisplaySettings mDeviceConfigDisplaySettings;
         private int mRefreshRateInHbmSunlight;
@@ -2351,6 +2362,7 @@
         public void onDisplayRemoved(int displayId) {
             mBallotBox.vote(displayId, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE, null);
             mHbmMode.delete(displayId);
+            mHbmActive.delete(displayId);
         }
 
         @Override
@@ -2360,12 +2372,17 @@
                 // Display no longer there. Assume we'll get an onDisplayRemoved very soon.
                 return;
             }
+
             final int hbmMode = info.highBrightnessMode;
-            if (hbmMode == mHbmMode.get(displayId)) {
+            final boolean isHbmActive = hbmMode != BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF &&
+                info.adjustedBrightness > info.highBrightnessTransitionPoint;
+            if (hbmMode == mHbmMode.get(displayId) &&
+                isHbmActive == mHbmActive.get(displayId)) {
                 // no change, ignore.
                 return;
             }
             mHbmMode.put(displayId, hbmMode);
+            mHbmActive.put(displayId, isHbmActive);
             recalculateVotesForDisplay(displayId);
         }
 
@@ -2379,28 +2396,36 @@
         }
 
         private void recalculateVotesForDisplay(int displayId) {
-            final int hbmMode = mHbmMode.get(displayId, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF);
             Vote vote = null;
-            if (hbmMode == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT) {
-                // Device resource properties take priority over DisplayDeviceConfig
-                if (mRefreshRateInHbmSunlight > 0) {
-                    vote = Vote.forRefreshRates(mRefreshRateInHbmSunlight,
-                            mRefreshRateInHbmSunlight);
-                } else {
-                    final List<RefreshRateLimitation> limits =
-                        mDisplayManagerInternal.getRefreshRateLimitations(displayId);
-                    for (int i = 0; limits != null && i < limits.size(); i++) {
-                        final RefreshRateLimitation limitation = limits.get(i);
-                        if (limitation.type == REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE) {
-                            vote = Vote.forRefreshRates(limitation.range.min, limitation.range.max);
-                            break;
+            if (mHbmActive.get(displayId, false)) {
+                final int hbmMode =
+                    mHbmMode.get(displayId, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF);
+                if (hbmMode == BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT) {
+                    // Device resource properties take priority over DisplayDeviceConfig
+                    if (mRefreshRateInHbmSunlight > 0) {
+                        vote = Vote.forRefreshRates(mRefreshRateInHbmSunlight,
+                                mRefreshRateInHbmSunlight);
+                    } else {
+                        final List<RefreshRateLimitation> limits =
+                            mDisplayManagerInternal.getRefreshRateLimitations(displayId);
+                        for (int i = 0; limits != null && i < limits.size(); i++) {
+                            final RefreshRateLimitation limitation = limits.get(i);
+                            if (limitation.type == REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE) {
+                                vote = Vote.forRefreshRates(limitation.range.min,
+                                        limitation.range.max);
+                                break;
+                            }
                         }
                     }
+                } else if (hbmMode == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR &&
+                        mRefreshRateInHbmHdr > 0) {
+                    // HBM for HDR vote isn't supported through DisplayDeviceConfig yet, so look for
+                    // a vote from Device properties
+                    vote = Vote.forRefreshRates(mRefreshRateInHbmHdr, mRefreshRateInHbmHdr);
+                } else {
+                    Slog.w(TAG, "Unexpected HBM mode " + hbmMode + " for display ID " + displayId);
                 }
-            }
-            if (hbmMode == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR
-                    && mRefreshRateInHbmHdr > 0) {
-                vote = Vote.forRefreshRates(mRefreshRateInHbmHdr, mRefreshRateInHbmHdr);
+
             }
             mBallotBox.vote(displayId, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE, vote);
         }
@@ -2408,6 +2433,7 @@
         void dumpLocked(PrintWriter pw) {
             pw.println("   HbmObserver");
             pw.println("     mHbmMode: " + mHbmMode);
+            pw.println("     mHbmActive: " + mHbmActive);
             pw.println("     mRefreshRateInHbmSunlight: " + mRefreshRateInHbmSunlight);
             pw.println("     mRefreshRateInHbmHdr: " + mRefreshRateInHbmHdr);
         }
@@ -2630,19 +2656,11 @@
     }
 
     interface Injector {
-        // TODO: brightnessfloat: change this to the float setting
-        Uri DISPLAY_BRIGHTNESS_URI = Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS);
         Uri PEAK_REFRESH_RATE_URI = Settings.System.getUriFor(Settings.System.PEAK_REFRESH_RATE);
 
         @NonNull
         DeviceConfigInterface getDeviceConfig();
 
-        void registerBrightnessObserver(@NonNull ContentResolver cr,
-                @NonNull ContentObserver observer);
-
-        void unregisterBrightnessObserver(@NonNull ContentResolver cr,
-                @NonNull ContentObserver observer);
-
         void registerPeakRefreshRateObserver(@NonNull ContentResolver cr,
                 @NonNull ContentObserver observer);
 
@@ -2672,19 +2690,6 @@
         }
 
         @Override
-        public void registerBrightnessObserver(@NonNull ContentResolver cr,
-                @NonNull ContentObserver observer) {
-            cr.registerContentObserver(DISPLAY_BRIGHTNESS_URI, false /*notifyDescendants*/,
-                    observer, UserHandle.USER_SYSTEM);
-        }
-
-        @Override
-        public void unregisterBrightnessObserver(@NonNull ContentResolver cr,
-                @NonNull ContentObserver observer) {
-            cr.unregisterContentObserver(observer);
-        }
-
-        @Override
         public void registerPeakRefreshRateObserver(@NonNull ContentResolver cr,
                 @NonNull ContentObserver observer) {
             cr.registerContentObserver(PEAK_REFRESH_RATE_URI, false /*notifyDescendants*/,
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 1063481..63d32c8 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -125,6 +125,7 @@
     private static final int MSG_IGNORE_PROXIMITY = 8;
     private static final int MSG_STOP = 9;
     private static final int MSG_UPDATE_BRIGHTNESS = 10;
+    private static final int MSG_UPDATE_RBC = 11;
 
     private static final int PROXIMITY_UNKNOWN = -1;
     private static final int PROXIMITY_NEGATIVE = 0;
@@ -422,13 +423,13 @@
     // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary adjustment set.
     private float mTemporaryAutoBrightnessAdjustment;
 
-    // Whether a reduce bright colors (rbc) change has been initiated by the user. We want to
-    // retain the current backlight level when rbc is toggled, since rbc additionally makes the
-    // screen appear dimmer using screen colors rather than backlight levels, and therefore we
-    // don't actually want to compensate for this by then in/decreasing the backlight when
-    // toggling this feature.
+    // Whether reduce bright colors (rbc) has been turned on, or a change in strength has been
+    // requested. We want to retain the current backlight level when rbc is toggled, since rbc
+    // additionally makes the screen appear dimmer using screen colors rather than backlight levels,
+    // and therefore we don't actually want to compensate for this by then in/decreasing the
+    // backlight when toggling this feature.
     // This should be false during system start up.
-    private boolean mPendingUserRbcChange;
+    private boolean mPendingRbcOnOrChanged = false;
 
     // Animators.
     private ObjectAnimator mColorFadeOnAnimator;
@@ -564,23 +565,35 @@
                 @Override
                 public void onReduceBrightColorsActivationChanged(boolean activated,
                         boolean userInitiated) {
-                    applyReduceBrightColorsSplineAdjustment(userInitiated);
+                    applyReduceBrightColorsSplineAdjustment(
+                            /* rbcStrengthChanged= */ false, activated);
+
                 }
 
                 @Override
                 public void onReduceBrightColorsStrengthChanged(int strength) {
-                    applyReduceBrightColorsSplineAdjustment(/*userInitiated*/ false);
+                    applyReduceBrightColorsSplineAdjustment(
+                            /* rbcStrengthChanged= */ true, /* justActivated= */ false);
                 }
             });
             if (active) {
-                applyReduceBrightColorsSplineAdjustment(/*userInitiated*/ false);
+                applyReduceBrightColorsSplineAdjustment(
+                        /* rbcStrengthChanged= */ false,  /* justActivated= */ false);
             }
         } else {
             mCdsi = null;
         }
     }
 
-    private void applyReduceBrightColorsSplineAdjustment(boolean userInitiated) {
+    private void applyReduceBrightColorsSplineAdjustment(
+            boolean rbcStrengthChanged, boolean justActivated) {
+        final int strengthChanged = rbcStrengthChanged ? 1 : 0;
+        final int activated = justActivated ? 1 : 0;
+        mHandler.obtainMessage(MSG_UPDATE_RBC, strengthChanged, activated).sendToTarget();
+        sendUpdatePowerState();
+    }
+
+    private void handleRbcChanged(boolean strengthChanged, boolean justActivated) {
         if (mBrightnessMapper == null) {
             Log.w(TAG, "No brightness mapping available to recalculate splines");
             return;
@@ -591,8 +604,13 @@
             adjustedNits[i] = mCdsi.getReduceBrightColorsAdjustedBrightnessNits(mNitsRange[i]);
         }
         mBrightnessMapper.recalculateSplines(mCdsi.isReduceBrightColorsActivated(), adjustedNits);
-        mPendingUserRbcChange = userInitiated;
-        sendUpdatePowerState();
+
+        mPendingRbcOnOrChanged = strengthChanged || justActivated;
+
+        // Reset model if strength changed OR rbc is turned off
+        if (strengthChanged || !justActivated && mAutomaticBrightnessController != null) {
+            mAutomaticBrightnessController.resetShortTermModel();
+        }
     }
 
     /**
@@ -926,7 +944,8 @@
 
     private void reloadReduceBrightColours() {
         if (mCdsi != null && mCdsi.isReduceBrightColorsActivated()) {
-            applyReduceBrightColorsSplineAdjustment(/*userInitiated*/ false);
+            applyReduceBrightColorsSplineAdjustment(
+                    /* rbcStrengthChanged= */ false, /* justActivated= */ false);
         }
     }
 
@@ -1259,10 +1278,6 @@
             putScreenBrightnessSetting(brightnessState, /* updateCurrent */ true);
         }
 
-        // We save the brightness info *after* the brightness setting has been changed so that
-        // the brightness info reflects the latest value.
-        saveBrightnessInfo(getScreenBrightnessSetting());
-
         // Apply dimming by at least some minimum amount when user activity
         // timeout is about to expire.
         if (mPowerRequest.policy == DisplayPowerRequest.POLICY_DIM) {
@@ -1393,6 +1408,11 @@
                         hadUserBrightnessPoint);
             }
 
+            // We save the brightness info *after* the brightness setting has been changed and
+            // adjustments made so that the brightness info reflects the latest value.
+            saveBrightnessInfo(getScreenBrightnessSetting(), animateValue);
+        } else {
+            saveBrightnessInfo(getScreenBrightnessSetting());
         }
 
         // Log any changes to what is currently driving the brightness setting.
@@ -1509,18 +1529,27 @@
         synchronized (mCachedBrightnessInfo) {
             return new BrightnessInfo(
                     mCachedBrightnessInfo.brightness,
+                    mCachedBrightnessInfo.adjustedBrightness,
                     mCachedBrightnessInfo.brightnessMin,
                     mCachedBrightnessInfo.brightnessMax,
-                    mCachedBrightnessInfo.hbmMode);
+                    mCachedBrightnessInfo.hbmMode,
+                    mCachedBrightnessInfo.highBrightnessTransitionPoint);
         }
     }
 
     private void saveBrightnessInfo(float brightness) {
+        saveBrightnessInfo(brightness, brightness);
+    }
+
+    private void saveBrightnessInfo(float brightness, float adjustedBrightness) {
         synchronized (mCachedBrightnessInfo) {
             mCachedBrightnessInfo.brightness = brightness;
+            mCachedBrightnessInfo.adjustedBrightness = adjustedBrightness;
             mCachedBrightnessInfo.brightnessMin = mHbmController.getCurrentBrightnessMin();
             mCachedBrightnessInfo.brightnessMax = mHbmController.getCurrentBrightnessMax();
             mCachedBrightnessInfo.hbmMode = mHbmController.getHighBrightnessMode();
+            mCachedBrightnessInfo.highBrightnessTransitionPoint =
+                mHbmController.getTransitionPoint();
         }
     }
 
@@ -2062,21 +2091,24 @@
         return true;
     }
 
+    // We want to return true if the user has set the screen brightness.
+    // If they have just turned RBC on (and therefore added that interaction to the curve),
+    // or changed the brightness another way, then we should return true.
     private boolean updateUserSetScreenBrightness() {
-        final boolean brightnessSplineChanged = mPendingUserRbcChange;
-        if (mPendingUserRbcChange && !Float.isNaN(mCurrentScreenBrightnessSetting)) {
+        final boolean treatAsIfUserChanged = mPendingRbcOnOrChanged;
+        if (treatAsIfUserChanged && !Float.isNaN(mCurrentScreenBrightnessSetting)) {
             mLastUserSetScreenBrightness = mCurrentScreenBrightnessSetting;
         }
-        mPendingUserRbcChange = false;
+        mPendingRbcOnOrChanged = false;
 
         if ((Float.isNaN(mPendingScreenBrightnessSetting)
                 || mPendingScreenBrightnessSetting < 0.0f)) {
-            return brightnessSplineChanged;
+            return treatAsIfUserChanged;
         }
         if (mCurrentScreenBrightnessSetting == mPendingScreenBrightnessSetting) {
             mPendingScreenBrightnessSetting = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             mTemporaryScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            return brightnessSplineChanged;
+            return treatAsIfUserChanged;
         }
         setCurrentScreenBrightness(mPendingScreenBrightnessSetting);
         mLastUserSetScreenBrightness = mPendingScreenBrightnessSetting;
@@ -2195,6 +2227,18 @@
         pw.println("  mSkipScreenOnBrightnessRamp=" + mSkipScreenOnBrightnessRamp);
         pw.println("  mColorFadeFadesConfig=" + mColorFadeFadesConfig);
         pw.println("  mColorFadeEnabled=" + mColorFadeEnabled);
+        synchronized (mCachedBrightnessInfo) {
+            pw.println("  mCachedBrightnessInfo.brightness=" + mCachedBrightnessInfo.brightness);
+            pw.println("  mCachedBrightnessInfo.adjustedBrightness=" +
+                    mCachedBrightnessInfo.adjustedBrightness);
+            pw.println("  mCachedBrightnessInfo.brightnessMin=" +
+                    mCachedBrightnessInfo.brightnessMin);
+            pw.println("  mCachedBrightnessInfo.brightnessMax=" +
+                    mCachedBrightnessInfo.brightnessMax);
+            pw.println("  mCachedBrightnessInfo.hbmMode=" + mCachedBrightnessInfo.hbmMode);
+            pw.println("  mCachedBrightnessInfo.highBrightnessTransitionPoint=" +
+                    mCachedBrightnessInfo.highBrightnessTransitionPoint);
+        }
         pw.println("  mDisplayBlanksAfterDozeConfig=" + mDisplayBlanksAfterDozeConfig);
         pw.println("  mBrightnessBucketsInDozeConfig=" + mBrightnessBucketsInDozeConfig);
 
@@ -2406,6 +2450,12 @@
                     }
                     handleSettingsChange(false /*userSwitch*/);
                     break;
+
+                case MSG_UPDATE_RBC:
+                    final int strengthChanged = msg.arg1;
+                    final int justActivated = msg.arg2;
+                    handleRbcChanged(strengthChanged == 1, justActivated == 1);
+                    break;
             }
         }
     }
@@ -2606,8 +2656,10 @@
 
     static class CachedBrightnessInfo {
         public float brightness;
+        public float adjustedBrightness;
         public float brightnessMin;
         public float brightnessMax;
         public int hbmMode;
+        public float highBrightnessTransitionPoint;
     }
 }
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java
index 2791f6a..1e1cfeb 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java
@@ -59,6 +59,9 @@
 
     private static final float HDR_PERCENT_OF_SCREEN_REQUIRED = 0.50f;
 
+    @VisibleForTesting
+    static final float HBM_TRANSITION_POINT_INVALID = Float.POSITIVE_INFINITY;
+
     private final float mBrightnessMin;
     private final float mBrightnessMax;
     private final Handler mHandler;
@@ -214,6 +217,14 @@
         return mHbmMode;
     }
 
+    float getTransitionPoint() {
+        if (deviceSupportsHbm()) {
+            return mHbmData.transitionPoint;
+        } else {
+            return HBM_TRANSITION_POINT_INVALID;
+        }
+    }
+
     void stop() {
         registerHdrListener(null /*displayToken*/);
         mSkinThermalStatusObserver.stopObserving();
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index dbe17b7..b6d13e0 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -993,12 +993,12 @@
 
             mGameContentTypeRequested = on;
 
-            if (!mGameContentTypeSupported) {
-                Slog.d(TAG, "Unable to set game content type because the connected "
-                        + "display does not support game content type.");
-                return;
-            }
-
+            // Even if game content type is not supported on the connected display we
+            // propagate the requested state down to the HAL. This is because some devices
+            // with external displays, such as Android TV set-top boxes, use this signal
+            // to disable/enable on-device processing.
+            // TODO(b/202378408) set game content type only if it's supported once we have a
+            // separate API for disabling on-device processing.
             mSurfaceControlProxy.setGameContentType(getDisplayTokenLocked(), on);
         }
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecConfig.java b/services/core/java/com/android/server/hdmi/HdmiCecConfig.java
index 5de89c9..cf8cc38 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecConfig.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecConfig.java
@@ -522,19 +522,19 @@
             case HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION:
                 return STORAGE_SHARED_PREFS;
             case HdmiControlManager.CEC_SETTING_NAME_ROUTING_CONTROL:
-                return STORAGE_GLOBAL_SETTINGS;
+                return STORAGE_SHARED_PREFS;
             case HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE:
-                return STORAGE_GLOBAL_SETTINGS;
+                return STORAGE_SHARED_PREFS;
             case HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE:
-                return STORAGE_GLOBAL_SETTINGS;
+                return STORAGE_SHARED_PREFS;
             case HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST:
                 return STORAGE_SHARED_PREFS;
             case HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL:
-                return STORAGE_GLOBAL_SETTINGS;
+                return STORAGE_SHARED_PREFS;
             case HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING:
                 return STORAGE_SHARED_PREFS;
             case HdmiControlManager.CEC_SETTING_NAME_TV_WAKE_ON_ONE_TOUCH_PLAY:
-                return STORAGE_GLOBAL_SETTINGS;
+                return STORAGE_SHARED_PREFS;
             case HdmiControlManager.CEC_SETTING_NAME_TV_SEND_STANDBY_ON_SLEEP:
                 return STORAGE_GLOBAL_SETTINGS;
             case HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_TV:
@@ -563,19 +563,19 @@
             case HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION:
                 return setting.getName();
             case HdmiControlManager.CEC_SETTING_NAME_ROUTING_CONTROL:
-                return Global.HDMI_CEC_SWITCH_ENABLED;
+                return setting.getName();
             case HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE:
-                return Global.HDMI_CONTROL_SEND_STANDBY_ON_SLEEP;
+                return setting.getName();
             case HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE:
-                return Global.HDMI_CONTROL_VOLUME_CONTROL_ENABLED;
+                return setting.getName();
             case HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST:
                 return setting.getName();
             case HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL:
-                return Global.HDMI_SYSTEM_AUDIO_CONTROL_ENABLED;
+                return setting.getName();
             case HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_MODE_MUTING:
                 return setting.getName();
             case HdmiControlManager.CEC_SETTING_NAME_TV_WAKE_ON_ONE_TOUCH_PLAY:
-                return Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED;
+                return setting.getName();
             case HdmiControlManager.CEC_SETTING_NAME_TV_SEND_STANDBY_ON_SLEEP:
                 return Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED;
             case HdmiControlManager.CEC_SETTING_NAME_RC_PROFILE_TV:
@@ -634,15 +634,6 @@
             case Global.HDMI_CONTROL_ENABLED:
                 notifySettingChanged(HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED);
                 break;
-            case Global.HDMI_CONTROL_SEND_STANDBY_ON_SLEEP:
-                notifySettingChanged(HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE);
-                break;
-            case Global.HDMI_CONTROL_VOLUME_CONTROL_ENABLED:
-                notifySettingChanged(HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE);
-                break;
-            case Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED:
-                notifySettingChanged(HdmiControlManager.CEC_SETTING_NAME_TV_WAKE_ON_ONE_TOUCH_PLAY);
-                break;
             case Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED:
                 notifySettingChanged(HdmiControlManager.CEC_SETTING_NAME_TV_SEND_STANDBY_ON_SLEEP);
                 break;
@@ -687,9 +678,6 @@
         ContentResolver resolver = mContext.getContentResolver();
         String[] settings = new String[] {
                 Global.HDMI_CONTROL_ENABLED,
-                Global.HDMI_CONTROL_SEND_STANDBY_ON_SLEEP,
-                Global.HDMI_CONTROL_VOLUME_CONTROL_ENABLED,
-                Global.HDMI_CONTROL_AUTO_WAKEUP_ENABLED,
                 Global.HDMI_CONTROL_AUTO_DEVICE_OFF_ENABLED,
         };
         for (String setting: settings) {
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 9be9505..15d2a05 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -73,7 +73,6 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
-import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.VibrationEffect;
 import android.os.vibrator.StepSegment;
@@ -175,9 +174,6 @@
     /** TODO(b/169067926): Remove this. */
     private static final boolean UNTRUSTED_TOUCHES_TOAST = false;
 
-    public static final boolean ENABLE_PER_WINDOW_INPUT_ROTATION =
-            SystemProperties.getBoolean("persist.debug.per_window_input_rotation", false);
-
     // Pointer to native input manager service object.
     private final long mPtr;
 
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 075b74d..d14ef16 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -270,6 +270,24 @@
             | Context.BIND_IMPORTANT_BACKGROUND;
 
     /**
+     * Binding flags for establishing connection to the {@link InputMethodService} when
+     * config_killableInputMethods is enabled.
+     */
+    private static final int IME_CONNECTION_LOW_PRIORITY_BIND_FLAGS =
+            Context.BIND_AUTO_CREATE
+            | Context.BIND_REDUCTION_FLAGS;
+
+    /**
+     * Binding flags for establishing connection to the {@link InputMethodService}.
+     *
+     * <p>
+     * This defaults to {@link #IME_CONNECTION_BIND_FLAGS} unless config_killableInputMethods is
+     * enabled, in which case this takes the value of
+     * {@link #IME_CONNECTION_LOW_PRIORITY_BIND_FLAGS}.
+     */
+    private final int mImeConnectionBindFlags;
+
+    /**
      * Binding flags used only while the {@link InputMethodService} is showing window.
      */
     private static final int IME_VISIBLE_BIND_FLAGS =
@@ -1651,6 +1669,16 @@
         mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
                 mSettings, context);
         mMenuController = new InputMethodMenuController(this);
+
+        // If configured, use low priority flags to make the IME killable by the lowmemorykiller
+        final boolean lowerIMEPriority = mRes.getBoolean(
+                com.android.internal.R.bool.config_killableInputMethods);
+
+        if (lowerIMEPriority) {
+            mImeConnectionBindFlags = IME_CONNECTION_LOW_PRIORITY_BIND_FLAGS;
+        } else {
+            mImeConnectionBindFlags = IME_CONNECTION_BIND_FLAGS;
+        }
     }
 
     @GuardedBy("mMethodMap")
@@ -1798,7 +1826,8 @@
                 final IntentFilter broadcastFilterForAllUsers = new IntentFilter();
                 broadcastFilterForAllUsers.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
                 mContext.registerReceiverAsUser(new ImmsBroadcastReceiverForAllUsers(),
-                        UserHandle.ALL, broadcastFilterForAllUsers, null, null);
+                        UserHandle.ALL, broadcastFilterForAllUsers, null, null,
+                        Context.RECEIVER_NOT_EXPORTED);
 
                 final String defaultImiId = mSettings.getSelectedInputMethod();
                 final boolean imeSelectedOnBoot = !TextUtils.isEmpty(defaultImiId);
@@ -2354,6 +2383,8 @@
 
         if (displayIdToShowIme == INVALID_DISPLAY) {
             mImeHiddenByDisplayPolicy = true;
+            hideCurrentInputLocked(mCurFocusedWindow, 0, null,
+                    SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE);
             return InputBindResult.NO_IME;
         }
         mImeHiddenByDisplayPolicy = false;
@@ -2434,7 +2465,7 @@
                 mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS),
                 PendingIntent.FLAG_IMMUTABLE));
 
-        if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
+        if (bindCurrentInputMethodServiceLocked(mCurIntent, this, mImeConnectionBindFlags)) {
             mLastBindTime = SystemClock.uptimeMillis();
             mHaveConnection = true;
             mCurId = info.getId();
@@ -3155,7 +3186,7 @@
                     SystemClock.uptimeMillis()-mLastBindTime,1);
             Slog.w(TAG, "Force disconnect/connect to the IME in showCurrentInputLocked()");
             mContext.unbindService(this);
-            bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS);
+            bindCurrentInputMethodServiceLocked(mCurIntent, this, mImeConnectionBindFlags);
         } else {
             if (DEBUG) {
                 Slog.d(TAG, "Can't show input: connection = " + mHaveConnection + ", time = "
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
new file mode 100644
index 0000000..0045499
--- /dev/null
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locales;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.ActivityManagerInternal;
+import android.app.ILocaleManager;
+import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.os.Binder;
+import android.os.LocaleList;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.DumpUtils;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * The implementation of ILocaleManager.aidl.
+ *
+ * <p>This service is API entry point for storing app-specific UI locales
+ */
+public class LocaleManagerService extends SystemService {
+    private static final String TAG = "LocaleManagerService";
+    private final Context mContext;
+    private final LocaleManagerService.LocaleManagerBinderService mBinderService;
+    private ActivityTaskManagerInternal mActivityTaskManagerInternal;
+    private ActivityManagerInternal mActivityManagerInternal;
+    private PackageManagerInternal mPackageManagerInternal;
+    public static final boolean DEBUG = false;
+
+    public LocaleManagerService(Context context) {
+        super(context);
+        mContext = context;
+        mBinderService = new LocaleManagerBinderService();
+        mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+        mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+    }
+
+    @VisibleForTesting
+    LocaleManagerService(Context context, ActivityTaskManagerInternal activityTaskManagerInternal,
+            ActivityManagerInternal activityManagerInternal,
+            PackageManagerInternal packageManagerInternal) {
+        super(context);
+        mContext = context;
+        mBinderService = new LocaleManagerBinderService();
+        mActivityTaskManagerInternal = activityTaskManagerInternal;
+        mActivityManagerInternal = activityManagerInternal;
+        mPackageManagerInternal = packageManagerInternal;
+    }
+
+    @Override
+    public void onStart() {
+        publishBinderService(Context.LOCALE_SERVICE, mBinderService);
+    }
+
+    private final class LocaleManagerBinderService extends ILocaleManager.Stub {
+        @Override
+        public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId,
+                @NonNull LocaleList locales) throws RemoteException {
+            LocaleManagerService.this.setApplicationLocales(appPackageName, userId, locales);
+        }
+
+        @Override
+        @NonNull
+        public LocaleList getApplicationLocales(@NonNull String appPackageName,
+                @UserIdInt int userId) throws RemoteException {
+            return LocaleManagerService.this.getApplicationLocales(appPackageName, userId);
+        }
+
+        @Override
+        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            LocaleManagerService.this.dump(fd, pw, args);
+        }
+
+        @Override
+        public void onShellCommand(FileDescriptor in, FileDescriptor out,
+                FileDescriptor err, String[] args, ShellCallback callback,
+                ResultReceiver resultReceiver) {
+            (new LocaleManagerShellCommand(mBinderService))
+                    .exec(this, in, out, err, args, callback, resultReceiver);
+        }
+    }
+
+    /**
+     * Sets the current UI locales for a specified app.
+     */
+    public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId,
+            @NonNull LocaleList locales) throws RemoteException, IllegalArgumentException {
+        requireNonNull(appPackageName);
+        requireNonNull(locales);
+
+        //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user.
+        userId = mActivityManagerInternal.handleIncomingUser(
+                Binder.getCallingPid(), Binder.getCallingUid(), userId,
+                false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
+                "setApplicationLocales", appPackageName);
+
+        // This function handles two types of set operations:
+        // 1.) A normal, non-privileged app setting its own locale.
+        // 2.) A privileged system service setting locales of another package.
+        // The least privileged case is a normal app performing a set, so check that first and
+        // set locales if the package name is owned by the app. Next, check if the caller has the
+        // necessary permission and set locales.
+        boolean isCallerOwner = isPackageOwnedByCaller(appPackageName, userId);
+        if (!isCallerOwner) {
+            enforceChangeConfigurationPermission();
+        }
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            setApplicationLocalesUnchecked(appPackageName, userId, locales);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+
+    private void setApplicationLocalesUnchecked(@NonNull String appPackageName,
+            @UserIdInt int userId, @NonNull LocaleList locales) {
+        if (DEBUG) {
+            Slog.d(TAG, "setApplicationLocales: setting locales for package " + appPackageName
+                    + " and user " + userId);
+        }
+        final ActivityTaskManagerInternal.PackageConfigurationUpdater updater =
+                mActivityTaskManagerInternal.createPackageConfigurationUpdater(appPackageName,
+                        userId);
+        updater.setLocales(locales).commit();
+    }
+
+
+    /**
+     * Checks if the package is owned by the calling app or not for the given user id.
+     *
+     * @throws IllegalArgumentException if package not found for given userid
+     */
+    private boolean isPackageOwnedByCaller(String appPackageName, int userId) {
+        final int uid = mPackageManagerInternal
+                .getPackageUid(appPackageName, /* flags */ 0, userId);
+        if (uid < 0) {
+            Slog.w(TAG, "Unknown package " + appPackageName + " for user " + userId);
+            throw new IllegalArgumentException("Unknown package: " + appPackageName
+                    + " for user " + userId);
+        }
+        //Once valid package found, ignore the userId part for validating package ownership
+        //as apps with INTERACT_ACROSS_USERS permission could be changing locale for different user.
+        return UserHandle.isSameApp(Binder.getCallingUid(), uid);
+    }
+
+    private void enforceChangeConfigurationPermission() {
+        mContext.enforceCallingPermission(
+                android.Manifest.permission.CHANGE_CONFIGURATION, "setApplicationLocales");
+    }
+
+    /**
+     * Returns the current UI locales for the specified app.
+     */
+    @NonNull
+    public LocaleList getApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId)
+            throws RemoteException, IllegalArgumentException {
+        requireNonNull(appPackageName);
+
+        //Allow apps with INTERACT_ACROSS_USERS permission to query locales for different user.
+        userId = mActivityManagerInternal.handleIncomingUser(
+                Binder.getCallingPid(), Binder.getCallingUid(), userId,
+                false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
+                "getApplicationLocales", appPackageName);
+
+        // This function handles two types of query operations:
+        // 1.) A normal, non-privileged app querying its own locale.
+        // 2.) A privileged system service querying locales of another package.
+        // The least privileged case is a normal app performing a query, so check that first and
+        // get locales if the package name is owned by the app. Next, check if the caller has the
+        // necessary permission and get locales.
+        if (!isPackageOwnedByCaller(appPackageName, userId)) {
+            enforceReadAppSpecificLocalesPermission();
+        }
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return getApplicationLocalesUnchecked(appPackageName, userId);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    private LocaleList getApplicationLocalesUnchecked(@NonNull String appPackageName,
+            @UserIdInt int userId) {
+        if (DEBUG) {
+            Slog.d(TAG, "getApplicationLocales: fetching locales for package " + appPackageName
+                    + " and user " + userId);
+        }
+
+        final ActivityTaskManagerInternal.PackageConfig appConfig =
+                mActivityTaskManagerInternal.getApplicationConfig(appPackageName, userId);
+        if (appConfig == null) {
+            if (DEBUG) {
+                Slog.d(TAG, "getApplicationLocales: application config not found for "
+                        + appPackageName + " and user id " + userId);
+            }
+            return LocaleList.getEmptyLocaleList();
+        }
+        LocaleList locales = appConfig.mLocales;
+        return locales != null ? locales : LocaleList.getEmptyLocaleList();
+    }
+
+    private void enforceReadAppSpecificLocalesPermission() {
+        mContext.enforceCallingPermission(
+                android.Manifest.permission.READ_APP_SPECIFIC_LOCALES,
+                "getApplicationLocales");
+    }
+
+    /**
+     * Dumps useful info related to service.
+     */
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+        // TODO(b/201766221): Implement when there is state.
+    }
+}
diff --git a/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java b/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
new file mode 100644
index 0000000..769ea17
--- /dev/null
+++ b/services/core/java/com/android/server/locales/LocaleManagerShellCommand.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locales;
+
+import android.app.ActivityManager;
+import android.app.ILocaleManager;
+import android.os.LocaleList;
+import android.os.RemoteException;
+import android.os.ShellCommand;
+import android.os.UserHandle;
+
+import java.io.PrintWriter;
+
+/**
+ * Shell commands for {@link LocaleManagerService}
+ */
+public class LocaleManagerShellCommand extends ShellCommand {
+    private final ILocaleManager mBinderService;
+
+    LocaleManagerShellCommand(ILocaleManager localeManager) {
+        mBinderService = localeManager;
+    }
+    @Override
+    public int onCommand(String cmd) {
+        if (cmd == null) {
+            return handleDefaultCommands(cmd);
+        }
+        switch (cmd) {
+            case "set-app-locales":
+                return runSetAppLocales();
+            case "get-app-locales":
+                return runGetAppLocales();
+            default: {
+                return handleDefaultCommands(cmd);
+            }
+        }
+    }
+
+    @Override
+    public void onHelp() {
+        PrintWriter pw = getOutPrintWriter();
+        pw.println("Locale manager (locale) shell commands:");
+        pw.println("  help");
+        pw.println("      Print this help text.");
+        pw.println("  set-app-locales <PACKAGE_NAME> [--user <USER_ID>] [--locales <LOCALE_INFO>]");
+        pw.println("      Set the locales for the specified app.");
+        pw.println("      --user <USER_ID>: apply for the given user, "
+                + "the current user is used when unspecified.");
+        pw.println("      --locales <LOCALE_INFO>: The language tags of locale to be included "
+                + "as a single String separated by commas");
+        pw.println("                 Empty locale list is used when unspecified.");
+        pw.println("                 eg. en,en-US,hi ");
+        pw.println("  get-app-locales <PACKAGE_NAME> [--user <USER_ID>]");
+        pw.println("      Get the locales for the specified app.");
+        pw.println("      --user <USER_ID>: get for the given user, "
+                + "the current user is used when unspecified.");
+    }
+
+    private int runSetAppLocales() {
+        final PrintWriter err = getErrPrintWriter();
+        String packageName = getNextArg();
+
+        if (packageName != null) {
+            int userId = ActivityManager.getCurrentUser();
+            LocaleList locales = LocaleList.getEmptyLocaleList();
+            do {
+                String option = getNextOption();
+                if (option == null) {
+                    break;
+                }
+                switch (option) {
+                    case "--user": {
+                        userId = UserHandle.parseUserArg(getNextArgRequired());
+                        break;
+                    }
+                    case "--locales": {
+                        locales = parseLocales();
+                        break;
+                    }
+                    default: {
+                        throw new IllegalArgumentException("Unknown option: " + option);
+                    }
+                }
+            } while (true);
+
+            try {
+                mBinderService.setApplicationLocales(packageName, userId, locales);
+            } catch (RemoteException e) {
+                getOutPrintWriter().println("Remote Exception: " + e);
+            } catch (IllegalArgumentException e) {
+                getOutPrintWriter().println("Unknown package " + packageName
+                        + " for userId " + userId);
+            }
+        } else {
+            err.println("Error: no package specified");
+            return -1;
+        }
+        return 0;
+    }
+
+    private int runGetAppLocales() {
+        final PrintWriter err = getErrPrintWriter();
+        String packageName = getNextArg();
+
+        if (packageName != null) {
+            int userId = ActivityManager.getCurrentUser();
+            do {
+                String option = getNextOption();
+                if (option == null) {
+                    break;
+                }
+                if ("--user".equals(option)) {
+                    userId = UserHandle.parseUserArg(getNextArgRequired());
+                    break;
+                } else {
+                    throw new IllegalArgumentException("Unknown option: " + option);
+                }
+            } while (true);
+            try {
+                LocaleList locales = mBinderService.getApplicationLocales(packageName, userId);
+                getOutPrintWriter().println("Locales for " + packageName
+                        + " for user " + userId + " are " + locales);
+            } catch (RemoteException e) {
+                getOutPrintWriter().println("Remote Exception: " + e);
+            } catch (IllegalArgumentException e) {
+                getOutPrintWriter().println("Unknown package " + packageName
+                        + " for userId " + userId);
+            }
+        } else {
+            err.println("Error: no package specified");
+            return -1;
+        }
+        return 0;
+    }
+
+    private LocaleList parseLocales() {
+        if (getRemainingArgsCount() <= 0) {
+            return LocaleList.getEmptyLocaleList();
+        }
+        String[] args = peekRemainingArgs();
+        String inputLocales = args[0];
+        LocaleList locales = LocaleList.forLanguageTags(inputLocales);
+        return locales;
+    }
+}
diff --git a/services/core/java/com/android/server/locales/OWNERS b/services/core/java/com/android/server/locales/OWNERS
new file mode 100644
index 0000000..be284a7
--- /dev/null
+++ b/services/core/java/com/android/server/locales/OWNERS
@@ -0,0 +1,3 @@
+roosa@google.com
+pratyushmore@google.com
+goldmanj@google.com
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
index 442abc9..b676f28 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
@@ -207,6 +207,12 @@
      */
     private AtomicBoolean mIsPendingIntentCancelled = new AtomicBoolean(false);
 
+    /**
+     * True if a permissions query has been issued and is being processed. Used to prevent too many
+     * queries from being issued by a single client at once.
+     */
+    private AtomicBoolean mIsPermQueryIssued = new AtomicBoolean(false);
+
     /*
      * Map containing all nanoapps this client has a messaging channel with and whether it is
      * allowed to communicate over that channel. A channel is defined to have been opened if the
@@ -233,11 +239,11 @@
     private final IContextHubTransactionCallback mQueryPermsCallback =
             new IContextHubTransactionCallback.Stub() {
             @Override
-            public void onTransactionComplete(int result) {
-            }
+            public void onTransactionComplete(int result) {}
 
             @Override
             public void onQueryResponse(int result, List<NanoAppState> nanoAppStateList) {
+                mIsPermQueryIssued.set(false);
                 if (result != ContextHubTransaction.RESULT_SUCCESS && nanoAppStateList != null) {
                     Log.e(TAG, "Permissions query failed, but still received nanoapp state");
                 } else if (nanoAppStateList != null) {
@@ -650,9 +656,11 @@
      * communicated with in the past.
      */
     private void checkNanoappPermsAsync() {
-        ContextHubServiceTransaction transaction = mTransactionManager.createQueryTransaction(
-                mAttachedContextHubInfo.getId(), mQueryPermsCallback, mPackage);
-        mTransactionManager.addTransaction(transaction);
+        if (!mIsPermQueryIssued.getAndSet(true)) {
+            ContextHubServiceTransaction transaction = mTransactionManager.createQueryTransaction(
+                    mAttachedContextHubInfo.getId(), mQueryPermsCallback, mPackage);
+            mTransactionManager.addTransaction(transaction);
+        }
     }
 
     private int updateNanoAppAuthState(
diff --git a/services/core/java/com/android/server/location/injector/LocationPermissionsHelper.java b/services/core/java/com/android/server/location/injector/LocationPermissionsHelper.java
index a47c48f..2df2101 100644
--- a/services/core/java/com/android/server/location/injector/LocationPermissionsHelper.java
+++ b/services/core/java/com/android/server/location/injector/LocationPermissionsHelper.java
@@ -100,7 +100,7 @@
             return false;
         }
 
-        return mAppOps.checkOpNoThrow(permissionLevel, identity);
+        return mAppOps.checkOpNoThrow(LocationPermissions.asAppOp(permissionLevel), identity);
     }
 
     protected abstract boolean hasPermission(String permission, CallerIdentity callerIdentity);
diff --git a/services/core/java/com/android/server/location/provider/StationaryThrottlingLocationProvider.java b/services/core/java/com/android/server/location/provider/StationaryThrottlingLocationProvider.java
index ad87c45..22a675a 100644
--- a/services/core/java/com/android/server/location/provider/StationaryThrottlingLocationProvider.java
+++ b/services/core/java/com/android/server/location/provider/StationaryThrottlingLocationProvider.java
@@ -105,15 +105,20 @@
 
         synchronized (mLock) {
             mDeviceIdleHelper.addListener(this);
-            onDeviceIdleChanged(mDeviceIdleHelper.isDeviceIdle());
+            mDeviceIdle = mDeviceIdleHelper.isDeviceIdle();
+            mDeviceStationaryHelper.addListener(this);
+            mDeviceStationary = false;
+            mDeviceStationaryRealtimeMs = Long.MIN_VALUE;
+
+            onThrottlingChangedLocked(false);
         }
     }
 
     @Override
     protected void onStop() {
         synchronized (mLock) {
+            mDeviceStationaryHelper.removeListener(this);
             mDeviceIdleHelper.removeListener(this);
-            onDeviceIdleChanged(false);
 
             mIncomingRequest = ProviderRequest.EMPTY_REQUEST;
             mOutgoingRequest = ProviderRequest.EMPTY_REQUEST;
@@ -146,26 +151,13 @@
             }
 
             mDeviceIdle = deviceIdle;
-
-            if (deviceIdle) {
-                // device stationary helper will deliver an immediate listener update
-                mDeviceStationaryHelper.addListener(this);
-            } else {
-                mDeviceStationaryHelper.removeListener(this);
-                mDeviceStationary = false;
-                mDeviceStationaryRealtimeMs = Long.MIN_VALUE;
-            }
+            onThrottlingChangedLocked(false);
         }
     }
 
     @Override
     public void onDeviceStationaryChanged(boolean deviceStationary) {
         synchronized (mLock) {
-            if (!mDeviceIdle) {
-                // stationary detection is only registered while idle - ignore late notifications
-                return;
-            }
-
             if (mDeviceStationary == deviceStationary) {
                 return;
             }
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 84be7f5..9e677e0 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -408,8 +408,6 @@
     private static final String ATTR_NETWORK_TYPES = "networkTypes";
     private static final String ATTR_XML_UTILS_NAME = "name";
 
-    private static final String ACTION_ALLOW_BACKGROUND =
-            "com.android.server.net.action.ALLOW_BACKGROUND";
     private static final String ACTION_SNOOZE_WARNING =
             "com.android.server.net.action.SNOOZE_WARNING";
     private static final String ACTION_SNOOZE_RAPID =
@@ -974,10 +972,6 @@
             mContext.registerReceiver(
                     mStatsReceiver, statsFilter, READ_NETWORK_USAGE_HISTORY, mHandler);
 
-            // listen for restrict background changes from notifications
-            final IntentFilter allowFilter = new IntentFilter(ACTION_ALLOW_BACKGROUND);
-            mContext.registerReceiver(mAllowReceiver, allowFilter, MANAGE_NETWORK_POLICY, mHandler);
-
             // Listen for snooze from notifications
             mContext.registerReceiver(mSnoozeReceiver,
                     new IntentFilter(ACTION_SNOOZE_WARNING), MANAGE_NETWORK_POLICY, mHandler);
@@ -1196,20 +1190,6 @@
 
     /**
      * Receiver that watches for {@link Notification} control of
-     * {@link #mRestrictBackground}.
-     */
-    final private BroadcastReceiver mAllowReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            // on background handler thread, and verified MANAGE_NETWORK_POLICY
-            // permission above.
-
-            setRestrictBackground(false);
-        }
-    };
-
-    /**
-     * Receiver that watches for {@link Notification} control of
      * {@link NetworkPolicy#lastWarningSnooze}.
      */
     final private BroadcastReceiver mSnoozeReceiver = new BroadcastReceiver() {
@@ -4073,7 +4053,7 @@
         if (hasRestrictedModeAccess(uid)) {
             uidBlockedState.allowedReasons |= ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS;
         } else {
-            uidBlockedState.allowedReasons &= ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS;
+            uidBlockedState.allowedReasons &= ~ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS;
         }
         uidBlockedState.updateEffectiveBlockedReasons();
         if (oldEffectiveBlockedReasons != uidBlockedState.effectiveBlockedReasons) {
@@ -5521,10 +5501,6 @@
         }
     }
 
-    private static Intent buildAllowBackgroundDataIntent() {
-        return new Intent(ACTION_ALLOW_BACKGROUND);
-    }
-
     private static Intent buildSnoozeWarningIntent(NetworkTemplate template, String targetPackage) {
         final Intent intent = new Intent(ACTION_SNOOZE_WARNING);
         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index b7744c7e..2b39234 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -36,6 +36,7 @@
 import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
 import static android.app.NotificationManager.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 import static android.app.NotificationManager.EXTRA_AUTOMATIC_ZEN_RULE_STATUS;
+import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
 import static android.app.NotificationManager.IMPORTANCE_LOW;
 import static android.app.NotificationManager.IMPORTANCE_MIN;
 import static android.app.NotificationManager.IMPORTANCE_NONE;
@@ -291,6 +292,7 @@
 import com.android.server.notification.toast.TextToastRecord;
 import com.android.server.notification.toast.ToastRecord;
 import com.android.server.pm.PackageManagerService;
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
 import com.android.server.statusbar.StatusBarManagerInternal;
 import com.android.server.uri.UriGrantsManagerInternal;
 import com.android.server.utils.quota.MultiRateLimiter;
@@ -489,6 +491,7 @@
     private UserManager mUm;
     private IPlatformCompat mPlatformCompat;
     private ShortcutHelper mShortcutHelper;
+    private PermissionHelper mPermissionHelper;
 
     final IBinder mForegroundToken = new Binder();
     private WorkerHandler mHandler;
@@ -606,6 +609,7 @@
 
     private int mWarnRemoteViewsSizeBytes;
     private int mStripRemoteViewsSizeBytes;
+    final boolean mEnableAppSettingMigration;
 
     private MetricsLogger mMetricsLogger;
     private TriPredicate<String, Integer, String> mAllowedManagedServicePackages;
@@ -2003,6 +2007,12 @@
         mNotificationRecordLogger = notificationRecordLogger;
         mNotificationInstanceIdSequence = notificationInstanceIdSequence;
         Notification.processAllowlistToken = ALLOWLIST_TOKEN;
+        // TODO (b/194833441): remove when OS is ready for migration. This flag is checked once
+        // rather than having a settings observer because some of the behaviors (e.g. readXml) only
+        // happen on reboot
+        mEnableAppSettingMigration = Settings.Secure.getIntForUser(
+                getContext().getContentResolver(),
+                Settings.Secure.NOTIFICATION_PERMISSION_ENABLED, 0, USER_SYSTEM) == 1;
     }
 
     // TODO - replace these methods with new fields in the VisibleForTesting constructor
@@ -2169,7 +2179,7 @@
             UriGrantsManagerInternal ugmInternal, AppOpsManager appOps, UserManager userManager,
             NotificationHistoryManager historyManager, StatsManager statsManager,
             TelephonyManager telephonyManager, ActivityManagerInternal ami,
-            MultiRateLimiter toastRateLimiter) {
+            MultiRateLimiter toastRateLimiter, PermissionHelper permissionHelper) {
         mHandler = handler;
         Resources resources = getContext().getResources();
         mMaxPackageEnqueueRate = Settings.Global.getFloat(getContext().getContentResolver(),
@@ -2275,6 +2285,7 @@
         mGroupHelper = groupHelper;
         mVibratorHelper = new VibratorHelper(getContext());
         mHistoryManager = historyManager;
+        mPermissionHelper = permissionHelper;
 
         // This is a ManagedServices object that keeps track of the listeners.
         mListeners = notificationListeners;
@@ -2480,7 +2491,9 @@
                         Context.STATS_MANAGER),
                 getContext().getSystemService(TelephonyManager.class),
                 LocalServices.getService(ActivityManagerInternal.class),
-                createToastRateLimiter());
+                createToastRateLimiter(), new PermissionHelper(LocalServices.getService(
+                        PermissionManagerServiceInternal.class), AppGlobals.getPackageManager(),
+                        AppGlobals.getPermissionManager(), mEnableAppSettingMigration));
 
         publishBinderService(Context.NOTIFICATION_SERVICE, mService, /* allowIsolated= */ false,
                 DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL);
@@ -3411,17 +3424,24 @@
         @Override
         public void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled) {
             enforceSystemOrSystemUI("setNotificationsEnabledForPackage");
-
-            synchronized (mNotificationLock) {
-                boolean wasEnabled = mPreferencesHelper.getImportance(pkg, uid)
-                        != NotificationManager.IMPORTANCE_NONE;
-
+            if (mEnableAppSettingMigration) {
+                boolean wasEnabled = mPermissionHelper.hasPermission(uid);
                 if (wasEnabled == enabled) {
                     return;
                 }
-            }
+                mPermissionHelper.setNotificationPermission(pkg, uid, enabled, true);
+            } else {
+                synchronized (mNotificationLock) {
+                    boolean wasEnabled = mPreferencesHelper.getImportance(pkg, uid)
+                            != NotificationManager.IMPORTANCE_NONE;
 
-            mPreferencesHelper.setEnabled(pkg, uid, enabled);
+                    if (wasEnabled == enabled) {
+                        return;
+                    }
+                }
+
+                mPreferencesHelper.setEnabled(pkg, uid, enabled);
+            }
             mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_BAN_APP_NOTES)
                     .setType(MetricsEvent.TYPE_ACTION)
                     .setPackageName(pkg)
@@ -3489,7 +3509,11 @@
                         "canNotifyAsPackage for uid " + uid);
             }
 
-            return mPreferencesHelper.getImportance(pkg, uid) != IMPORTANCE_NONE;
+            if (mEnableAppSettingMigration) {
+                return mPermissionHelper.hasPermission(uid);
+            } else {
+                return mPreferencesHelper.getImportance(pkg, uid) != IMPORTANCE_NONE;
+            }
         }
 
         /**
@@ -3581,7 +3605,15 @@
         @Override
         public int getPackageImportance(String pkg) {
             checkCallerIsSystemOrSameApp(pkg);
-            return mPreferencesHelper.getImportance(pkg, Binder.getCallingUid());
+            if (mEnableAppSettingMigration) {
+                if (mPermissionHelper.hasPermission(Binder.getCallingUid())) {
+                    return IMPORTANCE_DEFAULT;
+                } else {
+                    return IMPORTANCE_NONE;
+                }
+            } else {
+                return mPreferencesHelper.getImportance(pkg, Binder.getCallingUid());
+            }
         }
 
         @Override
@@ -5022,7 +5054,6 @@
 
         @Override
         public boolean matchesCallFilter(Bundle extras) {
-            enforceSystemOrSystemUI("INotificationManager.matchesCallFilter");
             return mZenModeHelper.matchesCallFilter(
                     Binder.getCallingUserHandle(),
                     extras,
@@ -5032,6 +5063,12 @@
         }
 
         @Override
+        public void cleanUpCallersAfter(long timeThreshold) {
+            enforceSystemOrSystemUI("INotificationManager.cleanUpCallersAfter");
+            mZenModeHelper.cleanUpCallersAfter(timeThreshold);
+        }
+
+        @Override
         public boolean isSystemConditionProviderEnabled(String path) {
             enforceSystemOrSystemUI("INotificationManager.isSystemConditionProviderEnabled");
             return mConditionProviders.isSystemProviderEnabled(path);
@@ -6248,11 +6285,17 @@
                     + ", notificationUid=" + notificationUid
                     + ", notification=" + notification;
             Slog.e(TAG, noChannelStr);
-            boolean appNotificationsOff = mPreferencesHelper.getImportance(pkg, notificationUid)
-                    == NotificationManager.IMPORTANCE_NONE;
+            boolean appNotificationsOff;
+            if (mEnableAppSettingMigration) {
+                appNotificationsOff = !mPermissionHelper.hasPermission(notificationUid);
+            } else {
+                appNotificationsOff = mPreferencesHelper.getImportance(pkg, notificationUid)
+                        == NotificationManager.IMPORTANCE_NONE;
+            }
 
             if (!appNotificationsOff) {
-                doChannelWarningToast("Developer warning for package \"" + pkg + "\"\n" +
+                doChannelWarningToast(notificationUid,
+                        "Developer warning for package \"" + pkg + "\"\n" +
                         "Failed to post notification on channel \"" + channelId + "\"\n" +
                         "See log for more details");
             }
@@ -6498,7 +6541,7 @@
                 }
             };
 
-    private void doChannelWarningToast(CharSequence toastText) {
+    protected void doChannelWarningToast(int forUid, CharSequence toastText) {
         Binder.withCleanCallingIdentity(() -> {
             final int defaultWarningEnabled = Build.IS_DEBUGGABLE ? 1 : 0;
             final boolean warningEnabled = Settings.Global.getInt(getContext().getContentResolver(),
@@ -10871,7 +10914,7 @@
         }
     }
 
-
+    // TODO (b/194833441): remove when we've fully migrated to a permission
     class RoleObserver implements OnRoleHoldersChangedListener {
         // Role name : user id : list of approved packages
         private ArrayMap<String, ArrayMap<Integer, ArraySet<String>>> mNonBlockableDefaultApps;
diff --git a/services/core/java/com/android/server/notification/PermissionHelper.java b/services/core/java/com/android/server/notification/PermissionHelper.java
new file mode 100644
index 0000000..0a34724
--- /dev/null
+++ b/services/core/java/com/android/server/notification/PermissionHelper.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET;
+import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.permission.PermissionManager.PERMISSION_GRANTED;
+
+import android.Manifest;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.ParceledListSlice;
+import android.os.RemoteException;
+import android.permission.IPermissionManager;
+import android.util.Slog;
+
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * NotificationManagerService helper for querying/setting the app-level notification permission
+ */
+public final class PermissionHelper {
+    private static final String TAG = "PermissionHelper";
+
+    private static String NOTIFICATION_PERMISSION = Manifest.permission.POST_NOTIFICATIONS;
+
+    private final PermissionManagerServiceInternal mPmi;
+    private final IPackageManager mPackageManager;
+    private final IPermissionManager mPermManager;
+    // TODO (b/194833441): Remove when the migration is enabled
+    private final boolean mMigrationEnabled;
+
+    public PermissionHelper(PermissionManagerServiceInternal pmi, IPackageManager packageManager,
+            IPermissionManager permManager, boolean migrationEnabled) {
+        mPmi = pmi;
+        mPackageManager = packageManager;
+        mPermManager = permManager;
+        mMigrationEnabled = migrationEnabled;
+    }
+
+    /**
+     * Returns whether the given uid holds the notification permission. Must not be called
+     * with a lock held.
+     */
+    public boolean hasPermission(int uid) {
+        assertFlag();
+        return mPmi.checkUidPermission(uid, NOTIFICATION_PERMISSION) == PERMISSION_GRANTED;
+    }
+
+    /**
+     * Returns all of the apps that have requested the notification permission in a given user.
+     * Must not be called with a lock held. Format: uid, packageName
+     */
+    public Map<Integer, String> getAppsRequestingPermission(int userId) {
+        assertFlag();
+        Map<Integer, String> requested = new HashMap<>();
+        List<PackageInfo> pkgs = getInstalledPackages(userId);
+        for (PackageInfo pi : pkgs) {
+            // when data was stored in PreferencesHelper, we only had data for apps that
+            // had ever registered an intent to send a notification. To match that behavior,
+            // filter the app list to apps that have requested the notification permission.
+            if (pi.requestedPermissions == null) {
+                continue;
+            }
+            for (String perm : pi.requestedPermissions) {
+                if (NOTIFICATION_PERMISSION.equals(perm)) {
+                    requested.put(pi.applicationInfo.uid, pi.packageName);
+                    break;
+                }
+            }
+        }
+        return requested;
+    }
+
+    private List<PackageInfo> getInstalledPackages(int userId) {
+        ParceledListSlice<PackageInfo> parceledList = null;
+        try {
+            parceledList = mPackageManager.getInstalledPackages(GET_PERMISSIONS, userId);
+        } catch (RemoteException e) {
+            Slog.d(TAG, "Could not reach system server", e);
+        }
+        if (parceledList == null) {
+            return Collections.emptyList();
+        }
+        return parceledList.getList();
+    }
+
+    /**
+     * Returns a list of apps that hold the notification permission. Must not be called
+     * with a lock held. Format: uid, packageName.
+     */
+    public Map<Integer, String> getAppsGrantedPermission(int userId) {
+        assertFlag();
+        Map<Integer, String> granted = new HashMap<>();
+        ParceledListSlice<PackageInfo> parceledList = null;
+        try {
+            parceledList = mPackageManager.getPackagesHoldingPermissions(
+                    new String[] {NOTIFICATION_PERMISSION}, 0, userId);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Could not reach system server", e);
+        }
+        if (parceledList == null) {
+            return granted;
+        }
+        for (PackageInfo pi : parceledList.getList()) {
+            granted.put(pi.applicationInfo.uid, pi.packageName);
+        }
+        return granted;
+    }
+
+    /**
+     * Grants or revokes the notification permission for a given package/user. UserSet should
+     * only be true if this method is being called to migrate existing user choice, because it
+     * can prevent the user from seeing the in app permission dialog. Must not be called
+     * with a lock held.
+     */
+    public void setNotificationPermission(String packageName, int userId, boolean grant,
+            boolean userSet) {
+        assertFlag();
+        try {
+            if (grant) {
+                mPermManager.grantRuntimePermission(packageName, NOTIFICATION_PERMISSION, userId);
+            } else {
+                mPermManager.revokeRuntimePermission(packageName, NOTIFICATION_PERMISSION, userId,
+                        TAG);
+            }
+            if (userSet) {
+                mPermManager.updatePermissionFlags(packageName, NOTIFICATION_PERMISSION,
+                        FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, userId);
+            }
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Could not reach system server", e);
+        }
+    }
+
+    private void assertFlag() {
+        if (!mMigrationEnabled) {
+            throw new IllegalStateException("Method called without checking flag value");
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/notification/ZenLog.java b/services/core/java/com/android/server/notification/ZenLog.java
index 1050835..7d7f3a9 100644
--- a/services/core/java/com/android/server/notification/ZenLog.java
+++ b/services/core/java/com/android/server/notification/ZenLog.java
@@ -65,6 +65,7 @@
     private static final int TYPE_LISTENER_HINTS_CHANGED = 15;
     private static final int TYPE_SET_NOTIFICATION_POLICY = 16;
     private static final int TYPE_SET_CONSOLIDATED_ZEN_POLICY = 17;
+    private static final int TYPE_MATCHES_CALL_FILTER = 18;
 
     private static int sNext;
     private static int sSize;
@@ -166,6 +167,13 @@
             + hintsToString(newHints) + ",listeners=" + listenerCount);
     }
 
+    /*
+     * Trace calls to matchesCallFilter with the result of the call and the reason for the result.
+     */
+    public static void traceMatchesCallFilter(boolean result, String reason) {
+        append(TYPE_MATCHES_CALL_FILTER, "result=" + result + ", reason=" + reason);
+    }
+
     private static String subscribeResult(IConditionProvider provider, RemoteException e) {
         return provider == null ? "no provider" : e != null ? e.getMessage() : "ok";
     }
@@ -189,6 +197,7 @@
             case TYPE_LISTENER_HINTS_CHANGED: return "listener_hints_changed";
             case TYPE_SET_NOTIFICATION_POLICY: return "set_notification_policy";
             case TYPE_SET_CONSOLIDATED_ZEN_POLICY: return "set_consolidated_policy";
+            case TYPE_MATCHES_CALL_FILTER: return "matches_call_filter";
             default: return "unknown";
         }
     }
diff --git a/services/core/java/com/android/server/notification/ZenModeFiltering.java b/services/core/java/com/android/server/notification/ZenModeFiltering.java
index 4d19855..b186f61 100644
--- a/services/core/java/com/android/server/notification/ZenModeFiltering.java
+++ b/services/core/java/com/android/server/notification/ZenModeFiltering.java
@@ -89,20 +89,34 @@
     public static boolean matchesCallFilter(Context context, int zen, NotificationManager.Policy
             consolidatedPolicy, UserHandle userHandle, Bundle extras,
             ValidateNotificationPeople validator, int contactsTimeoutMs, float timeoutAffinity) {
-        if (zen == Global.ZEN_MODE_NO_INTERRUPTIONS) return false; // nothing gets through
-        if (zen == Global.ZEN_MODE_ALARMS) return false; // not an alarm
+        if (zen == Global.ZEN_MODE_NO_INTERRUPTIONS) {
+            ZenLog.traceMatchesCallFilter(false, "no interruptions");
+            return false; // nothing gets through
+        }
+        if (zen == Global.ZEN_MODE_ALARMS) {
+            ZenLog.traceMatchesCallFilter(false, "alarms only");
+            return false; // not an alarm
+        }
         if (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) {
             if (consolidatedPolicy.allowRepeatCallers()
                     && REPEAT_CALLERS.isRepeat(context, extras)) {
+                ZenLog.traceMatchesCallFilter(true, "repeat caller");
                 return true;
             }
-            if (!consolidatedPolicy.allowCalls()) return false; // no other calls get through
+            if (!consolidatedPolicy.allowCalls()) {
+                ZenLog.traceMatchesCallFilter(false, "calls not allowed");
+                return false; // no other calls get through
+            }
             if (validator != null) {
                 final float contactAffinity = validator.getContactAffinity(userHandle, extras,
                         contactsTimeoutMs, timeoutAffinity);
-                return audienceMatches(consolidatedPolicy.allowCallsFrom(), contactAffinity);
+                boolean match =
+                        audienceMatches(consolidatedPolicy.allowCallsFrom(), contactAffinity);
+                ZenLog.traceMatchesCallFilter(match, "contact affinity " + contactAffinity);
+                return match;
             }
         }
+        ZenLog.traceMatchesCallFilter(true, "no restrictions");
         return true;
     }
 
@@ -311,6 +325,10 @@
         }
     }
 
+    protected void cleanUpCallersAfter(long timeThreshold) {
+        REPEAT_CALLERS.cleanUpCallsAfter(timeThreshold);
+    }
+
     private static class RepeatCallers {
         // Person : time
         private final ArrayMap<String, Long> mCalls = new ArrayMap<>();
@@ -346,6 +364,17 @@
             }
         }
 
+        // Clean up all calls that occurred after the given time.
+        // Used only for tests, to clean up after testing.
+        private synchronized void cleanUpCallsAfter(long timeThreshold) {
+            for (int i = mCalls.size() - 1; i >= 0; i--) {
+                final long time = mCalls.valueAt(i);
+                if (time > timeThreshold) {
+                    mCalls.removeAt(i);
+                }
+            }
+        }
+
         private void setThresholdMinutes(Context context) {
             if (mThresholdMinutes <= 0) {
                 mThresholdMinutes = context.getResources().getInteger(com.android.internal.R.integer
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 16a0b7e..93f1b47 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -188,6 +188,10 @@
         mFiltering.recordCall(record);
     }
 
+    protected void cleanUpCallersAfter(long timeThreshold) {
+        mFiltering.cleanUpCallersAfter(timeThreshold);
+    }
+
     public boolean shouldIntercept(NotificationRecord record) {
         synchronized (mConfig) {
             return mFiltering.shouldIntercept(mZenMode, mConsolidatedPolicy, record);
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index 7889ff2..37cb8a9 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -1035,17 +1035,6 @@
             }
         }
 
-        private void checkDowngrade(PackageInfo existingApexPkg, PackageInfo newApexPkg)
-                throws PackageManagerException {
-            final long currentVersionCode = existingApexPkg.applicationInfo.longVersionCode;
-            final long newVersionCode = newApexPkg.applicationInfo.longVersionCode;
-            if (currentVersionCode > newVersionCode) {
-                throw new PackageManagerException(PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE,
-                          "Downgrade of APEX package " + newApexPkg.packageName
-                                  + " is not allowed");
-            }
-        }
-
         @Override
         void installPackage(File apexFile, PackageParser2 packageParser)
                 throws PackageManagerException {
@@ -1069,7 +1058,6 @@
                             "It is forbidden to install new APEX packages");
                 }
                 checkApexSignature(existingApexPkg, newApexPkg);
-                checkDowngrade(existingApexPkg, newApexPkg);
                 ApexInfo apexInfo = waitForApexService().installAndActivatePackage(
                         apexFile.getAbsolutePath());
                 final ParsedPackage parsedPackage2 = packageParser.parsePackage(
diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java
new file mode 100644
index 0000000..8387482
--- /dev/null
+++ b/services/core/java/com/android/server/pm/AppDataHelper.java
@@ -0,0 +1,588 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
+
+import static com.android.server.pm.PackageManagerService.TAG;
+import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.PackageManager;
+import android.content.pm.SELinuxUtil;
+import android.content.pm.UserInfo;
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageManagerInternal;
+import android.os.storage.VolumeInfo;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.util.TimingsTraceLog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.server.SystemServerInitThreadPool;
+import com.android.server.pm.dex.ArtManagerService;
+import com.android.server.pm.parsing.pkg.AndroidPackage;
+import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
+
+import dalvik.system.VMRuntime;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+
+/**
+ * Prepares app data for users
+ */
+final class AppDataHelper {
+    private static final boolean DEBUG_APP_DATA = false;
+
+    private final PackageManagerService mPm;
+    private final Installer mInstaller;
+    private final ArtManagerService mArtManagerService;
+
+    // TODO(b/198166813): remove PMS dependency
+    AppDataHelper(PackageManagerService pm) {
+        mPm = pm;
+        mInstaller = mPm.mInjector.getInstaller();
+        mArtManagerService = mPm.mInjector.getArtManagerService();
+    }
+
+    /**
+     * Prepare app data for the given app just after it was installed or
+     * upgraded. This method carefully only touches users that it's installed
+     * for, and it forces a restorecon to handle any seinfo changes.
+     * <p>
+     * Verifies that directories exist and that ownership and labeling is
+     * correct for all installed apps. If there is an ownership mismatch, it
+     * will try recovering system apps by wiping data; third-party app data is
+     * left intact.
+     * <p>
+     * <em>Note: To avoid a deadlock, do not call this method with {@code mLock} lock held</em>
+     */
+    public void prepareAppDataAfterInstallLIF(AndroidPackage pkg) {
+        final PackageSetting ps;
+        synchronized (mPm.mLock) {
+            ps = mPm.mSettings.getPackageLPr(pkg.getPackageName());
+            mPm.mSettings.writeKernelMappingLPr(ps);
+        }
+
+        Installer.Batch batch = new Installer.Batch();
+        UserManagerInternal umInternal = mPm.mInjector.getUserManagerInternal();
+        StorageManagerInternal smInternal = mPm.mInjector.getLocalService(
+                StorageManagerInternal.class);
+        for (UserInfo user : umInternal.getUsers(false /*excludeDying*/)) {
+            final int flags;
+            if (StorageManager.isUserKeyUnlocked(user.id)
+                    && smInternal.isCeStoragePrepared(user.id)) {
+                flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
+            } else if (umInternal.isUserRunning(user.id)) {
+                flags = StorageManager.FLAG_STORAGE_DE;
+            } else {
+                continue;
+            }
+
+            // TODO@ashfall check ScanResult.mNeedsNewAppId, and if true instead
+            // of creating app data, migrate / change ownership of existing
+            // data.
+
+            if (ps.getInstalled(user.id)) {
+                // TODO: when user data is locked, mark that we're still dirty
+                prepareAppData(batch, pkg, user.id, flags).thenRun(() -> {
+                    // Note: this code block is executed with the Installer lock
+                    // already held, since it's invoked as a side-effect of
+                    // executeBatchLI()
+                    if (umInternal.isUserUnlockingOrUnlocked(user.id)) {
+                        // Prepare app data on external storage; currently this is used to
+                        // setup any OBB dirs that were created by the installer correctly.
+                        int uid = UserHandle.getUid(user.id, UserHandle.getAppId(pkg.getUid()));
+                        smInternal.prepareAppDataAfterInstall(pkg.getPackageName(), uid);
+                    }
+                });
+            }
+        }
+        executeBatchLI(batch);
+    }
+
+    private void executeBatchLI(@NonNull Installer.Batch batch) {
+        try {
+            batch.execute(mInstaller);
+        } catch (Installer.InstallerException e) {
+            Slog.w(TAG, "Failed to execute pending operations", e);
+        }
+    }
+
+    /**
+     * Prepare app data for the given app.
+     * <p>
+     * Verifies that directories exist and that ownership and labeling is
+     * correct for all installed apps. If there is an ownership mismatch, this
+     * will try recovering system apps by wiping data; third-party app data is
+     * left intact.
+     */
+    private @NonNull CompletableFuture<?> prepareAppData(@NonNull Installer.Batch batch,
+            @Nullable AndroidPackage pkg, int userId, int flags) {
+        if (pkg == null) {
+            Slog.wtf(TAG, "Package was null!", new Throwable());
+            return CompletableFuture.completedFuture(null);
+        }
+        return prepareAppDataLeaf(batch, pkg, userId, flags);
+    }
+
+    private void prepareAppDataAndMigrate(@NonNull Installer.Batch batch,
+            @NonNull AndroidPackage pkg, int userId, int flags, boolean maybeMigrateAppData) {
+        prepareAppData(batch, pkg, userId, flags).thenRun(() -> {
+            // Note: this code block is executed with the Installer lock
+            // already held, since it's invoked as a side-effect of
+            // executeBatchLI()
+            if (maybeMigrateAppData && maybeMigrateAppDataLIF(pkg, userId)) {
+                // We may have just shuffled around app data directories, so
+                // prepare them one more time
+                final Installer.Batch batchInner = new Installer.Batch();
+                prepareAppData(batchInner, pkg, userId, flags);
+                executeBatchLI(batchInner);
+            }
+        });
+    }
+
+    private @NonNull CompletableFuture<?> prepareAppDataLeaf(@NonNull Installer.Batch batch,
+            @NonNull AndroidPackage pkg, int userId, int flags) {
+        if (DEBUG_APP_DATA) {
+            Slog.v(TAG, "prepareAppData for " + pkg.getPackageName() + " u" + userId + " 0x"
+                    + Integer.toHexString(flags));
+        }
+
+        final PackageSetting ps;
+        final String seInfoUser;
+        synchronized (mPm.mLock) {
+            ps = mPm.mSettings.getPackageLPr(pkg.getPackageName());
+            seInfoUser = SELinuxUtil.getSeinfoUser(ps.readUserState(userId));
+        }
+        final String volumeUuid = pkg.getVolumeUuid();
+        final String packageName = pkg.getPackageName();
+
+        final int appId = UserHandle.getAppId(pkg.getUid());
+
+        String pkgSeInfo = AndroidPackageUtils.getSeInfo(pkg, ps);
+
+        Preconditions.checkNotNull(pkgSeInfo);
+
+        final String seInfo = pkgSeInfo + seInfoUser;
+        final int targetSdkVersion = pkg.getTargetSdkVersion();
+
+        return batch.createAppData(volumeUuid, packageName, userId, flags, appId, seInfo,
+                targetSdkVersion).whenComplete((ceDataInode, e) -> {
+                    // Note: this code block is executed with the Installer lock
+                    // already held, since it's invoked as a side-effect of
+                    // executeBatchLI()
+                    if (e != null) {
+                        logCriticalInfo(Log.WARN, "Failed to create app data for " + packageName
+                                + ", but trying to recover: " + e);
+                        destroyAppDataLeafLIF(pkg, userId, flags);
+                        try {
+                            ceDataInode = mInstaller.createAppData(volumeUuid, packageName, userId,
+                                    flags, appId, seInfo, pkg.getTargetSdkVersion());
+                            logCriticalInfo(Log.DEBUG, "Recovery succeeded!");
+                        } catch (Installer.InstallerException e2) {
+                            logCriticalInfo(Log.DEBUG, "Recovery failed!");
+                        }
+                    }
+
+                    // Prepare the application profiles only for upgrades and
+                    // first boot (so that we don't repeat the same operation at
+                    // each boot).
+                    //
+                    // We only have to cover the upgrade and first boot here
+                    // because for app installs we prepare the profiles before
+                    // invoking dexopt (in installPackageLI).
+                    //
+                    // We also have to cover non system users because we do not
+                    // call the usual install package methods for them.
+                    //
+                    // NOTE: in order to speed up first boot time we only create
+                    // the current profile and do not update the content of the
+                    // reference profile. A system image should already be
+                    // configured with the right profile keys and the profiles
+                    // for the speed-profile prebuilds should already be copied.
+                    // That's done in #performDexOptUpgrade.
+                    //
+                    // TODO(calin, mathieuc): We should use .dm files for
+                    // prebuilds profiles instead of manually copying them in
+                    // #performDexOptUpgrade. When we do that we should have a
+                    // more granular check here and only update the existing
+                    // profiles.
+                    if (mPm.mIsUpgrade || mPm.mFirstBoot || (userId != UserHandle.USER_SYSTEM)) {
+                        mArtManagerService.prepareAppProfiles(pkg, userId,
+                                /* updateReferenceProfileContent= */ false);
+                    }
+
+                    if ((flags & StorageManager.FLAG_STORAGE_CE) != 0 && ceDataInode != -1) {
+                        // TODO: mark this structure as dirty so we persist it!
+                        synchronized (mPm.mLock) {
+                            if (ps != null) {
+                                ps.setCeDataInode(ceDataInode, userId);
+                            }
+                        }
+                    }
+
+                    prepareAppDataContentsLeafLIF(pkg, ps, userId, flags);
+                });
+    }
+
+    public void prepareAppDataContentsLIF(AndroidPackage pkg, @Nullable PackageSetting pkgSetting,
+            int userId, int flags) {
+        if (pkg == null) {
+            Slog.wtf(TAG, "Package was null!", new Throwable());
+            return;
+        }
+        prepareAppDataContentsLeafLIF(pkg, pkgSetting, userId, flags);
+    }
+
+    private void prepareAppDataContentsLeafLIF(AndroidPackage pkg,
+            @Nullable PackageSetting pkgSetting, int userId, int flags) {
+        final String volumeUuid = pkg.getVolumeUuid();
+        final String packageName = pkg.getPackageName();
+
+        if ((flags & StorageManager.FLAG_STORAGE_CE) != 0) {
+            // Create a native library symlink only if we have native libraries
+            // and if the native libraries are 32 bit libraries. We do not provide
+            // this symlink for 64 bit libraries.
+            String primaryCpuAbi = AndroidPackageUtils.getPrimaryCpuAbi(pkg, pkgSetting);
+            if (primaryCpuAbi != null && !VMRuntime.is64BitAbi(primaryCpuAbi)) {
+                final String nativeLibPath = pkg.getNativeLibraryDir();
+                try {
+                    mInstaller.linkNativeLibraryDirectory(volumeUuid, packageName,
+                            nativeLibPath, userId);
+                } catch (Installer.InstallerException e) {
+                    Slog.e(TAG, "Failed to link native for " + packageName + ": " + e);
+                }
+            }
+        }
+    }
+
+    /**
+     * For system apps on non-FBE devices, this method migrates any existing
+     * CE/DE data to match the {@code defaultToDeviceProtectedStorage} flag
+     * requested by the app.
+     */
+    private boolean maybeMigrateAppDataLIF(AndroidPackage pkg, int userId) {
+        if (pkg.isSystem() && !StorageManager.isFileEncryptedNativeOrEmulated()
+                && PackageManager.APPLY_DEFAULT_TO_DEVICE_PROTECTED_STORAGE) {
+            final int storageTarget = pkg.isDefaultToDeviceProtectedStorage()
+                    ? StorageManager.FLAG_STORAGE_DE : StorageManager.FLAG_STORAGE_CE;
+            try {
+                mInstaller.migrateAppData(pkg.getVolumeUuid(), pkg.getPackageName(), userId,
+                        storageTarget);
+            } catch (Installer.InstallerException e) {
+                logCriticalInfo(Log.WARN,
+                        "Failed to migrate " + pkg.getPackageName() + ": " + e.getMessage());
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Reconcile all app data for the given user.
+     * <p>
+     * Verifies that directories exist and that ownership and labeling is
+     * correct for all installed apps on all mounted volumes.
+     */
+    @NonNull
+    public void reconcileAppsData(int userId, int flags, boolean migrateAppsData) {
+        final StorageManager storage = mPm.mInjector.getSystemService(StorageManager.class);
+        for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
+            final String volumeUuid = vol.getFsUuid();
+            synchronized (mPm.mInstallLock) {
+                reconcileAppsDataLI(volumeUuid, userId, flags, migrateAppsData);
+            }
+        }
+    }
+
+    @GuardedBy("mPm.mInstallLock")
+    void reconcileAppsDataLI(String volumeUuid, int userId, int flags,
+            boolean migrateAppData) {
+        reconcileAppsDataLI(volumeUuid, userId, flags, migrateAppData, false /* onlyCoreApps */);
+    }
+
+    /**
+     * Reconcile all app data on given mounted volume.
+     * <p>
+     * Destroys app data that isn't expected, either due to uninstallation or
+     * reinstallation on another volume.
+     * <p>
+     * Verifies that directories exist and that ownership and labeling is
+     * correct for all installed apps.
+     *
+     * @return list of skipped non-core packages (if {@code onlyCoreApps} is true)
+     */
+    @GuardedBy("mPm.mInstallLock")
+    private List<String> reconcileAppsDataLI(String volumeUuid, int userId, int flags,
+            boolean migrateAppData, boolean onlyCoreApps) {
+        Slog.v(TAG, "reconcileAppsData for " + volumeUuid + " u" + userId + " 0x"
+                + Integer.toHexString(flags) + " migrateAppData=" + migrateAppData);
+        List<String> result = onlyCoreApps ? new ArrayList<>() : null;
+
+        final File ceDir = Environment.getDataUserCeDirectory(volumeUuid, userId);
+        final File deDir = Environment.getDataUserDeDirectory(volumeUuid, userId);
+
+        // First look for stale data that doesn't belong, and check if things
+        // have changed since we did our last restorecon
+        if ((flags & StorageManager.FLAG_STORAGE_CE) != 0) {
+            if (StorageManager.isFileEncryptedNativeOrEmulated()
+                    && !StorageManager.isUserKeyUnlocked(userId)) {
+                throw new RuntimeException(
+                        "Yikes, someone asked us to reconcile CE storage while " + userId
+                                + " was still locked; this would have caused massive data loss!");
+            }
+
+            final File[] files = FileUtils.listFilesOrEmpty(ceDir);
+            for (File file : files) {
+                final String packageName = file.getName();
+                try {
+                    assertPackageKnownAndInstalled(volumeUuid, packageName, userId);
+                } catch (PackageManagerException e) {
+                    logCriticalInfo(Log.WARN, "Destroying " + file + " due to: " + e);
+                    try {
+                        mInstaller.destroyAppData(volumeUuid, packageName, userId,
+                                StorageManager.FLAG_STORAGE_CE, 0);
+                    } catch (Installer.InstallerException e2) {
+                        logCriticalInfo(Log.WARN, "Failed to destroy: " + e2);
+                    }
+                }
+            }
+        }
+        if ((flags & StorageManager.FLAG_STORAGE_DE) != 0) {
+            final File[] files = FileUtils.listFilesOrEmpty(deDir);
+            for (File file : files) {
+                final String packageName = file.getName();
+                try {
+                    assertPackageKnownAndInstalled(volumeUuid, packageName, userId);
+                } catch (PackageManagerException e) {
+                    logCriticalInfo(Log.WARN, "Destroying " + file + " due to: " + e);
+                    try {
+                        mInstaller.destroyAppData(volumeUuid, packageName, userId,
+                                StorageManager.FLAG_STORAGE_DE, 0);
+                    } catch (Installer.InstallerException e2) {
+                        logCriticalInfo(Log.WARN, "Failed to destroy: " + e2);
+                    }
+                }
+            }
+        }
+
+        // Ensure that data directories are ready to roll for all packages
+        // installed for this volume and user
+        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "prepareAppDataAndMigrate");
+        Installer.Batch batch = new Installer.Batch();
+        final List<PackageSetting> packages;
+        synchronized (mPm.mLock) {
+            packages = mPm.mSettings.getVolumePackagesLPr(volumeUuid);
+        }
+        int preparedCount = 0;
+        for (PackageSetting ps : packages) {
+            final String packageName = ps.getPackageName();
+            if (ps.getPkg() == null) {
+                Slog.w(TAG, "Odd, missing scanned package " + packageName);
+                // TODO: might be due to legacy ASEC apps; we should circle back
+                // and reconcile again once they're scanned
+                continue;
+            }
+            // Skip non-core apps if requested
+            if (onlyCoreApps && !ps.getPkg().isCoreApp()) {
+                result.add(packageName);
+                continue;
+            }
+
+            if (ps.getInstalled(userId)) {
+                prepareAppDataAndMigrate(batch, ps.getPkg(), userId, flags, migrateAppData);
+                preparedCount++;
+            }
+        }
+        executeBatchLI(batch);
+        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+
+        Slog.v(TAG, "reconcileAppsData finished " + preparedCount + " packages");
+        return result;
+    }
+
+    private void assertPackageKnownAndInstalled(String volumeUuid, String packageName, int userId)
+            throws PackageManagerException {
+        synchronized (mPm.mLock) {
+            // Normalize package name to handle renamed packages
+            packageName = normalizePackageNameLPr(packageName);
+
+            final PackageSetting ps = mPm.mSettings.getPackageLPr(packageName);
+            if (ps == null) {
+                throw new PackageManagerException("Package " + packageName + " is unknown");
+            } else if (!TextUtils.equals(volumeUuid, ps.getVolumeUuid())) {
+                throw new PackageManagerException(
+                        "Package " + packageName + " found on unknown volume " + volumeUuid
+                                + "; expected volume " + ps.getVolumeUuid());
+            } else if (!ps.getInstalled(userId)) {
+                throw new PackageManagerException(
+                        "Package " + packageName + " not installed for user " + userId);
+            }
+        }
+    }
+
+    @GuardedBy("mPm.mLock")
+    private String normalizePackageNameLPr(String packageName) {
+        String normalizedPackageName = mPm.mSettings.getRenamedPackageLPr(packageName);
+        return normalizedPackageName != null ? normalizedPackageName : packageName;
+    }
+
+    /**
+     * Prepare storage for system user really early during boot,
+     * since core system apps like SettingsProvider and SystemUI
+     * can't wait for user to start
+     */
+    public Future<?> fixAppsDataOnBoot() {
+        final int storageFlags;
+        if (StorageManager.isFileEncryptedNativeOrEmulated()) {
+            storageFlags = StorageManager.FLAG_STORAGE_DE;
+        } else {
+            storageFlags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
+        }
+        List<String> deferPackages = reconcileAppsDataLI(StorageManager.UUID_PRIVATE_INTERNAL,
+                UserHandle.USER_SYSTEM, storageFlags, true /* migrateAppData */,
+                true /* onlyCoreApps */);
+        Future<?> prepareAppDataFuture = SystemServerInitThreadPool.submit(() -> {
+            TimingsTraceLog traceLog = new TimingsTraceLog("SystemServerTimingAsync",
+                    Trace.TRACE_TAG_PACKAGE_MANAGER);
+            traceLog.traceBegin("AppDataFixup");
+            try {
+                mInstaller.fixupAppData(StorageManager.UUID_PRIVATE_INTERNAL,
+                        StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
+            } catch (Installer.InstallerException e) {
+                Slog.w(TAG, "Trouble fixing GIDs", e);
+            }
+            traceLog.traceEnd();
+
+            traceLog.traceBegin("AppDataPrepare");
+            if (deferPackages == null || deferPackages.isEmpty()) {
+                return;
+            }
+            int count = 0;
+            final Installer.Batch batch = new Installer.Batch();
+            for (String pkgName : deferPackages) {
+                AndroidPackage pkg = null;
+                synchronized (mPm.mLock) {
+                    PackageSetting ps = mPm.mSettings.getPackageLPr(pkgName);
+                    if (ps != null && ps.getInstalled(UserHandle.USER_SYSTEM)) {
+                        pkg = ps.getPkg();
+                    }
+                }
+                if (pkg != null) {
+                    prepareAppDataAndMigrate(batch, pkg, UserHandle.USER_SYSTEM, storageFlags,
+                            true /* maybeMigrateAppData */);
+                    count++;
+                }
+            }
+            synchronized (mPm.mInstallLock) {
+                executeBatchLI(batch);
+            }
+            traceLog.traceEnd();
+            Slog.i(TAG, "Deferred reconcileAppsData finished " + count + " packages");
+        }, "prepareAppData");
+        return prepareAppDataFuture;
+    }
+
+    void clearAppDataLIF(AndroidPackage pkg, int userId, int flags) {
+        if (pkg == null) {
+            return;
+        }
+        clearAppDataLeafLIF(pkg, userId, flags);
+
+        if ((flags & Installer.FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES) == 0) {
+            clearAppProfilesLIF(pkg);
+        }
+    }
+
+    private void clearAppDataLeafLIF(AndroidPackage pkg, int userId, int flags) {
+        final PackageSetting ps;
+        synchronized (mPm.mLock) {
+            ps = mPm.mSettings.getPackageLPr(pkg.getPackageName());
+        }
+        for (int realUserId : mPm.resolveUserIds(userId)) {
+            final long ceDataInode = (ps != null) ? ps.getCeDataInode(realUserId) : 0;
+            try {
+                mInstaller.clearAppData(pkg.getVolumeUuid(), pkg.getPackageName(), realUserId,
+                        flags, ceDataInode);
+            } catch (Installer.InstallerException e) {
+                Slog.w(TAG, String.valueOf(e));
+            }
+        }
+    }
+
+    void clearAppProfilesLIF(AndroidPackage pkg) {
+        if (pkg == null) {
+            Slog.wtf(TAG, "Package was null!", new Throwable());
+            return;
+        }
+        mArtManagerService.clearAppProfiles(pkg);
+    }
+
+    public void destroyAppDataLIF(AndroidPackage pkg, int userId, int flags) {
+        if (pkg == null) {
+            Slog.wtf(TAG, "Package was null!", new Throwable());
+            return;
+        }
+        destroyAppDataLeafLIF(pkg, userId, flags);
+    }
+
+    public void destroyAppDataLeafLIF(AndroidPackage pkg, int userId, int flags) {
+        final PackageSetting ps;
+        synchronized (mPm.mLock) {
+            ps = mPm.mSettings.getPackageLPr(pkg.getPackageName());
+        }
+        for (int realUserId : mPm.resolveUserIds(userId)) {
+            final long ceDataInode = (ps != null) ? ps.getCeDataInode(realUserId) : 0;
+            try {
+                mInstaller.destroyAppData(pkg.getVolumeUuid(), pkg.getPackageName(), realUserId,
+                        flags, ceDataInode);
+            } catch (Installer.InstallerException e) {
+                Slog.w(TAG, String.valueOf(e));
+            }
+            mPm.getDexManager().notifyPackageDataDestroyed(pkg.getPackageName(), userId);
+        }
+    }
+
+    public void destroyAppProfilesLIF(AndroidPackage pkg) {
+        if (pkg == null) {
+            Slog.wtf(TAG, "Package was null!", new Throwable());
+            return;
+        }
+        destroyAppProfilesLeafLIF(pkg);
+    }
+
+    private void destroyAppProfilesLeafLIF(AndroidPackage pkg) {
+        try {
+            mInstaller.destroyAppProfiles(pkg.getPackageName());
+        } catch (Installer.InstallerException e) {
+            Slog.w(TAG, String.valueOf(e));
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 5b692b0..7dd02cb 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -50,7 +50,6 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
-import android.os.incremental.IncrementalManager;
 import android.util.EventLog;
 import android.util.Log;
 import android.util.Slog;
@@ -78,33 +77,32 @@
     private static final boolean DEBUG_SD_INSTALL = false;
 
     private final PackageManagerService mPm;
-    private final IncrementalManager mIncrementalManager;
-    private final Installer mInstaller;
     private final UserManagerInternal mUserManagerInternal;
     private final PermissionManagerServiceInternal mPermissionManager;
     private final RemovePackageHelper mRemovePackageHelper;
     private final InitAndSystemPackageHelper mInitAndSystemPackageHelper;
+    private final AppDataHelper mAppDataHelper;
 
     // TODO(b/198166813): remove PMS dependency
     DeletePackageHelper(PackageManagerService pm, RemovePackageHelper removePackageHelper,
-            InitAndSystemPackageHelper initAndSystemPackageHelper) {
+            InitAndSystemPackageHelper initAndSystemPackageHelper,
+            AppDataHelper appDataHelper) {
         mPm = pm;
-        mIncrementalManager = mPm.mInjector.getIncrementalManager();
-        mInstaller = mPm.mInjector.getInstaller();
         mUserManagerInternal = mPm.mInjector.getUserManagerInternal();
         mPermissionManager = mPm.mInjector.getPermissionManagerServiceInternal();
         mRemovePackageHelper = removePackageHelper;
         mInitAndSystemPackageHelper = initAndSystemPackageHelper;
+        mAppDataHelper = appDataHelper;
     }
 
     DeletePackageHelper(PackageManagerService pm) {
         mPm = pm;
-        mIncrementalManager = mPm.mInjector.getIncrementalManager();
-        mInstaller = mPm.mInjector.getInstaller();
+        mAppDataHelper = new AppDataHelper(mPm);
         mUserManagerInternal = mPm.mInjector.getUserManagerInternal();
         mPermissionManager = mPm.mInjector.getPermissionManagerServiceInternal();
-        mRemovePackageHelper = new RemovePackageHelper(mPm);
-        mInitAndSystemPackageHelper = new InitAndSystemPackageHelper(mPm, mRemovePackageHelper);
+        mRemovePackageHelper = new RemovePackageHelper(mPm, mAppDataHelper);
+        mInitAndSystemPackageHelper = new InitAndSystemPackageHelper(mPm, mRemovePackageHelper,
+                mAppDataHelper);
     }
 
     /**
@@ -457,13 +455,11 @@
             pkg = mPm.mPackages.get(ps.getPackageName());
         }
 
-        mRemovePackageHelper.destroyAppProfilesLIF(pkg);
+        mAppDataHelper.destroyAppProfilesLIF(pkg);
 
         final SharedUserSetting sus = ps.getSharedUser();
-        List<AndroidPackage> sharedUserPkgs = sus != null ? sus.getPackages() : null;
-        if (sharedUserPkgs == null) {
-            sharedUserPkgs = Collections.emptyList();
-        }
+        final List<AndroidPackage> sharedUserPkgs =
+                sus != null ? sus.getPackages() : Collections.emptyList();
         final int[] userIds = (userId == UserHandle.USER_ALL) ? mUserManagerInternal.getUserIds()
                 : new int[] {userId};
         for (int nextUserId : userIds) {
@@ -472,7 +468,7 @@
                         + nextUserId);
             }
             if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) {
-                mRemovePackageHelper.destroyAppDataLIF(pkg, nextUserId,
+                mAppDataHelper.destroyAppDataLIF(pkg, nextUserId,
                         FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL);
             }
             PackageManagerService.removeKeystoreDataIfNeeded(mUserManagerInternal, nextUserId,
diff --git a/services/core/java/com/android/server/pm/InitAndSystemPackageHelper.java b/services/core/java/com/android/server/pm/InitAndSystemPackageHelper.java
index 8c52630..9ba69f8 100644
--- a/services/core/java/com/android/server/pm/InitAndSystemPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InitAndSystemPackageHelper.java
@@ -88,11 +88,14 @@
 final class InitAndSystemPackageHelper {
     private final PackageManagerService mPm;
     private final RemovePackageHelper mRemovePackageHelper;
+    private final AppDataHelper mAppDataHelper;
 
     // TODO(b/198166813): remove PMS dependency
-    InitAndSystemPackageHelper(PackageManagerService pm, RemovePackageHelper removePackageHelper) {
+    InitAndSystemPackageHelper(PackageManagerService pm, RemovePackageHelper removePackageHelper,
+            AppDataHelper appDataHelper) {
         mPm = pm;
         mRemovePackageHelper = removePackageHelper;
+        mAppDataHelper = appDataHelper;
     }
 
     /**
@@ -571,7 +574,7 @@
                          mPm.freezePackage(stubPkg.getPackageName(), "setEnabledSetting")) {
                 pkg = installStubPackageLI(stubPkg, parseFlags, 0 /*scanFlags*/);
                 synchronized (mPm.mLock) {
-                    mPm.prepareAppDataAfterInstallLIF(pkg);
+                    mAppDataHelper.prepareAppDataAfterInstallLIF(pkg);
                     try {
                         mPm.updateSharedLibrariesLocked(pkg, stubPkgSetting, null, null,
                                 Collections.unmodifiableMap(mPm.mPackages));
@@ -615,8 +618,9 @@
                 }
                 return false;
             }
-            mPm.clearAppDataLIF(pkg, UserHandle.USER_ALL, FLAG_STORAGE_DE | FLAG_STORAGE_CE
-                    | FLAG_STORAGE_EXTERNAL | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
+            mAppDataHelper.clearAppDataLIF(pkg, UserHandle.USER_ALL,
+                    FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL
+                            | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
             mPm.getDexManager().notifyPackageUpdated(pkg.getPackageName(),
                     pkg.getBaseApkPath(), pkg.getSplitCodePaths());
         }
@@ -839,7 +843,7 @@
             Slog.e(TAG, "updateAllSharedLibrariesLPw failed: " + e.getMessage());
         }
 
-        mPm.prepareAppDataAfterInstallLIF(pkg);
+        mAppDataHelper.prepareAppDataAfterInstallLIF(pkg);
 
         // writer
         synchronized (mPm.mLock) {
diff --git a/services/core/java/com/android/server/pm/InstallParams.java b/services/core/java/com/android/server/pm/InstallParams.java
index 2187416..5a18d40 100644
--- a/services/core/java/com/android/server/pm/InstallParams.java
+++ b/services/core/java/com/android/server/pm/InstallParams.java
@@ -1840,6 +1840,7 @@
      */
     private void executePostCommitSteps(CommitRequest commitRequest) {
         final ArraySet<IncrementalStorage> incrementalStorages = new ArraySet<>();
+        final AppDataHelper appDataHelper = new AppDataHelper(mPm);
         for (ReconciledPackage reconciledPkg : commitRequest.mReconciledPackages.values()) {
             final boolean instantApp = ((reconciledPkg.mScanResult.mRequest.mScanFlags
                     & SCAN_AS_INSTANT_APP) != 0);
@@ -1856,10 +1857,11 @@
                 }
                 incrementalStorages.add(storage);
             }
-            mPm.prepareAppDataAfterInstallLIF(pkg);
+            appDataHelper.prepareAppDataAfterInstallLIF(pkg);
             if (reconciledPkg.mPrepareResult.mClearCodeCache) {
-                mPm.clearAppDataLIF(pkg, UserHandle.USER_ALL, FLAG_STORAGE_DE | FLAG_STORAGE_CE
-                        | FLAG_STORAGE_EXTERNAL | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
+                appDataHelper.clearAppDataLIF(pkg, UserHandle.USER_ALL,
+                        FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL
+                                | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
             }
             if (reconciledPkg.mPrepareResult.mReplace) {
                 mPm.getDexManager().notifyPackageUpdated(pkg.getPackageName(),
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 449cfe2..d89af91 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -688,11 +688,6 @@
             if (params.isMultiPackage) {
                 throw new IllegalArgumentException("A multi-session can't be set as APEX.");
             }
-            if (!params.isStaged
-                    && (params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) {
-                throw new IllegalArgumentException(
-                    "Non-staged APEX session doesn't support INSTALL_ENABLE_ROLLBACK");
-            }
             if (isCalledBySystemOrShell(callingUid) || mBypassNextAllowedApexUpdateCheck) {
                 params.installFlags |= PackageManager.INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK;
             } else {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index df54030..2bec8be 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -143,7 +143,6 @@
 import android.content.pm.ProcessInfo;
 import android.content.pm.ProviderInfo;
 import android.content.pm.ResolveInfo;
-import android.content.pm.SELinuxUtil;
 import android.content.pm.ServiceInfo;
 import android.content.pm.SharedLibraryInfo;
 import android.content.pm.Signature;
@@ -199,7 +198,6 @@
 import android.os.storage.IStorageManager;
 import android.os.storage.StorageManager;
 import android.os.storage.StorageManagerInternal;
-import android.os.storage.VolumeInfo;
 import android.os.storage.VolumeRecord;
 import android.permission.PermissionManager;
 import android.provider.ContactsContract;
@@ -224,7 +222,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
-import android.util.TimingsTraceLog;
 import android.util.TypedXmlPullParser;
 import android.util.TypedXmlSerializer;
 import android.util.Xml;
@@ -256,7 +253,6 @@
 import com.android.server.PackageWatchdog;
 import com.android.server.ServiceThread;
 import com.android.server.SystemConfig;
-import com.android.server.SystemServerInitThreadPool;
 import com.android.server.Watchdog;
 import com.android.server.apphibernation.AppHibernationManagerInternal;
 import com.android.server.apphibernation.AppHibernationService;
@@ -339,7 +335,6 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
@@ -417,7 +412,6 @@
 
     static final boolean DEBUG_ABI_SELECTION = false;
     public static final boolean DEBUG_INSTANT = Build.IS_DEBUGGABLE;
-    private static final boolean DEBUG_APP_DATA = false;
 
     /** REMOVE. According to Svet, this was only used to reset permissions during development. */
     static final boolean CLEAR_RUNTIME_PERMISSIONS_ON_UPGRADE = false;
@@ -1059,6 +1053,7 @@
     private final RemovePackageHelper mRemovePackageHelper;
     private final DeletePackageHelper mDeletePackageHelper;
     private final InitAndSystemPackageHelper mInitAndSystemPackageHelper;
+    private final AppDataHelper mAppDataHelper;
 
     /**
      * Invalidate the package info cache, which includes updating the cached computer.
@@ -1575,7 +1570,7 @@
                     Slog.i(TAG, "Updating seInfo for package " + packageName + " from: "
                             + oldSeInfo + " to: " + newSeInfo);
                     ps.getPkgState().setOverrideSeInfo(newSeInfo);
-                    m.prepareAppDataAfterInstallLIF(pkg);
+                    m.mAppDataHelper.prepareAppDataAfterInstallLIF(pkg);
                 }
             }
         };
@@ -1747,10 +1742,12 @@
         mDomainVerificationConnection = new DomainVerificationConnection(this);
 
         mBroadcastHelper = new BroadcastHelper(mInjector);
-        mRemovePackageHelper = new RemovePackageHelper(this);
-        mInitAndSystemPackageHelper = new InitAndSystemPackageHelper(this, mRemovePackageHelper);
+        mAppDataHelper = new AppDataHelper(this);
+        mRemovePackageHelper = new RemovePackageHelper(this, mAppDataHelper);
+        mInitAndSystemPackageHelper = new InitAndSystemPackageHelper(this, mRemovePackageHelper,
+                mAppDataHelper);
         mDeletePackageHelper = new DeletePackageHelper(this, mRemovePackageHelper,
-                mInitAndSystemPackageHelper);
+                mInitAndSystemPackageHelper, mAppDataHelper);
 
         invalidatePackageInfoCache();
     }
@@ -1896,10 +1893,12 @@
         mDomainVerificationManager.setConnection(mDomainVerificationConnection);
 
         mBroadcastHelper = new BroadcastHelper(mInjector);
-        mRemovePackageHelper = new RemovePackageHelper(this);
-        mInitAndSystemPackageHelper = new InitAndSystemPackageHelper(this, mRemovePackageHelper);
+        mAppDataHelper = new AppDataHelper(this);
+        mRemovePackageHelper = new RemovePackageHelper(this, mAppDataHelper);
+        mInitAndSystemPackageHelper = new InitAndSystemPackageHelper(this, mRemovePackageHelper,
+                mAppDataHelper);
         mDeletePackageHelper = new DeletePackageHelper(this, mRemovePackageHelper,
-                mInitAndSystemPackageHelper);
+                mInitAndSystemPackageHelper, mAppDataHelper);
 
         synchronized (mLock) {
             // Create the computer as soon as the state objects have been installed.  The
@@ -2136,56 +2135,7 @@
                 }
             }
 
-            // Prepare storage for system user really early during boot,
-            // since core system apps like SettingsProvider and SystemUI
-            // can't wait for user to start
-            final int storageFlags;
-            if (StorageManager.isFileEncryptedNativeOrEmulated()) {
-                storageFlags = StorageManager.FLAG_STORAGE_DE;
-            } else {
-                storageFlags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
-            }
-            List<String> deferPackages = reconcileAppsDataLI(StorageManager.UUID_PRIVATE_INTERNAL,
-                    UserHandle.USER_SYSTEM, storageFlags, true /* migrateAppData */,
-                    true /* onlyCoreApps */);
-            mPrepareAppDataFuture = SystemServerInitThreadPool.submit(() -> {
-                TimingsTraceLog traceLog = new TimingsTraceLog("SystemServerTimingAsync",
-                        Trace.TRACE_TAG_PACKAGE_MANAGER);
-                traceLog.traceBegin("AppDataFixup");
-                try {
-                    mInstaller.fixupAppData(StorageManager.UUID_PRIVATE_INTERNAL,
-                            StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE);
-                } catch (InstallerException e) {
-                    Slog.w(TAG, "Trouble fixing GIDs", e);
-                }
-                traceLog.traceEnd();
-
-                traceLog.traceBegin("AppDataPrepare");
-                if (deferPackages == null || deferPackages.isEmpty()) {
-                    return;
-                }
-                int count = 0;
-                final Installer.Batch batch = new Installer.Batch();
-                for (String pkgName : deferPackages) {
-                    AndroidPackage pkg = null;
-                    synchronized (mLock) {
-                        PackageSetting ps = mSettings.getPackageLPr(pkgName);
-                        if (ps != null && ps.getInstalled(UserHandle.USER_SYSTEM)) {
-                            pkg = ps.getPkg();
-                        }
-                    }
-                    if (pkg != null) {
-                        prepareAppDataAndMigrate(batch, pkg, UserHandle.USER_SYSTEM, storageFlags,
-                                true /* maybeMigrateAppData */);
-                        count++;
-                    }
-                }
-                synchronized (mInstallLock) {
-                    executeBatchLI(batch);
-                }
-                traceLog.traceEnd();
-                Slog.i(TAG, "Deferred reconcileAppsData finished " + count + " packages");
-            }, "prepareAppData");
+            mPrepareAppDataFuture = mAppDataHelper.fixAppsDataOnBoot();
 
             // If this is first boot after an OTA, and a normal boot, then
             // we need to clear code cache directories.
@@ -2198,7 +2148,7 @@
                     final PackageSetting ps = packageSettings.valueAt(i);
                     if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, ps.getVolumeUuid())) {
                         // No apps are running this early, so no need to freeze
-                        clearAppDataLIF(ps.getPkg(), UserHandle.USER_ALL,
+                        mAppDataHelper.clearAppDataLIF(ps.getPkg(), UserHandle.USER_ALL,
                                 FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL
                                         | Installer.FLAG_CLEAR_CODE_CACHE_ONLY
                                         | Installer.FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES);
@@ -3051,11 +3001,6 @@
                 filterCallingUid, userId);
     }
 
-    @GuardedBy("mLock")
-    private String normalizePackageNameLPr(String packageName) {
-        String normalizedPackageName = mSettings.getRenamedPackageLPr(packageName);
-        return normalizedPackageName != null ? normalizedPackageName : packageName;
-    }
 
     @Override
     public void deletePreloadsFileCache() {
@@ -6534,41 +6479,6 @@
         return (userId == UserHandle.USER_ALL) ? mUserManager.getUserIds() : new int[] { userId };
     }
 
-    void clearAppDataLIF(AndroidPackage pkg, int userId, int flags) {
-        if (pkg == null) {
-            return;
-        }
-        clearAppDataLeafLIF(pkg, userId, flags);
-
-        if ((flags & Installer.FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES) == 0) {
-            clearAppProfilesLIF(pkg);
-        }
-    }
-
-    private void clearAppDataLeafLIF(AndroidPackage pkg, int userId, int flags) {
-        final PackageSetting ps;
-        synchronized (mLock) {
-            ps = mSettings.getPackageLPr(pkg.getPackageName());
-        }
-        for (int realUserId : resolveUserIds(userId)) {
-            final long ceDataInode = (ps != null) ? ps.getCeDataInode(realUserId) : 0;
-            try {
-                mInstaller.clearAppData(pkg.getVolumeUuid(), pkg.getPackageName(), realUserId,
-                        flags, ceDataInode);
-            } catch (InstallerException e) {
-                Slog.w(TAG, String.valueOf(e));
-            }
-        }
-    }
-
-    void clearAppProfilesLIF(AndroidPackage pkg) {
-        if (pkg == null) {
-            Slog.wtf(TAG, "Package was null!", new Throwable());
-            return;
-        }
-        mArtManagerService.clearAppProfiles(pkg);
-    }
-
     @GuardedBy("mLock")
     private void applyDefiningSharedLibraryUpdateLocked(
             AndroidPackage pkg, SharedLibraryInfo libInfo,
@@ -7834,7 +7744,7 @@
                 if (pkgSetting.getPkg() != null) {
                     synchronized (mInstallLock) {
                         // We don't need to freeze for a brand new install
-                        prepareAppDataAfterInstallLIF(pkgSetting.getPkg());
+                        mAppDataHelper.prepareAppDataAfterInstallLIF(pkgSetting.getPkg());
                     }
                 }
                 sendPackageAddedForUser(packageName, pkgSetting, userId, DataLoaderType.NONE);
@@ -9435,7 +9345,7 @@
 
         try (PackageFreezer freezer = freezePackage(packageName, "clearApplicationProfileData")) {
             synchronized (mInstallLock) {
-                clearAppProfilesLIF(pkg);
+                mAppDataHelper.clearAppProfilesLIF(pkg);
             }
         }
     }
@@ -9527,7 +9437,7 @@
         }
         mPermissionManager.resetRuntimePermissions(pkg, userId);
 
-        clearAppDataLIF(pkg, userId,
+        mAppDataHelper.clearAppDataLIF(pkg, userId,
                 FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL);
 
         final int appId = UserHandle.getAppId(pkg.getUid());
@@ -9543,7 +9453,7 @@
         } else {
             flags = 0;
         }
-        prepareAppDataContentsLIF(pkg, ps, userId, flags);
+        mAppDataHelper.prepareAppDataContentsLIF(pkg, ps, userId, flags);
 
         return true;
     }
@@ -9627,8 +9537,10 @@
                     final int flags = FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL;
                     // We're only clearing cache files, so we don't care if the
                     // app is unfrozen and still able to run
-                    clearAppDataLIF(pkg, userId, flags | Installer.FLAG_CLEAR_CACHE_ONLY);
-                    clearAppDataLIF(pkg, userId, flags | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
+                    mAppDataHelper.clearAppDataLIF(pkg, userId,
+                            flags | Installer.FLAG_CLEAR_CACHE_ONLY);
+                    mAppDataHelper.clearAppDataLIF(pkg, userId,
+                            flags | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
                 }
             }
             if (observer != null) {
@@ -12325,382 +12237,6 @@
         }
     }
 
-    private void assertPackageKnownAndInstalled(String volumeUuid, String packageName, int userId)
-            throws PackageManagerException {
-        synchronized (mLock) {
-            // Normalize package name to handle renamed packages
-            packageName = normalizePackageNameLPr(packageName);
-
-            final PackageSetting ps = mSettings.getPackageLPr(packageName);
-            if (ps == null) {
-                throw new PackageManagerException("Package " + packageName + " is unknown");
-            } else if (!TextUtils.equals(volumeUuid, ps.getVolumeUuid())) {
-                throw new PackageManagerException(
-                        "Package " + packageName + " found on unknown volume " + volumeUuid
-                                + "; expected volume " + ps.getVolumeUuid());
-            } else if (!ps.getInstalled(userId)) {
-                throw new PackageManagerException(
-                        "Package " + packageName + " not installed for user " + userId);
-            }
-        }
-    }
-
-    private void executeBatchLI(@NonNull Installer.Batch batch) {
-        try {
-            batch.execute(mInstaller);
-        } catch (InstallerException e) {
-            Slog.w(TAG, "Failed to execute pending operations", e);
-        }
-    }
-
-    /**
-     * Reconcile all app data for the given user.
-     * <p>
-     * Verifies that directories exist and that ownership and labeling is
-     * correct for all installed apps on all mounted volumes.
-     */
-    @NonNull
-    void reconcileAppsData(int userId, int flags, boolean migrateAppsData) {
-        final StorageManager storage = mInjector.getSystemService(StorageManager.class);
-        for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
-            final String volumeUuid = vol.getFsUuid();
-            synchronized (mInstallLock) {
-                reconcileAppsDataLI(volumeUuid, userId, flags, migrateAppsData);
-            }
-        }
-    }
-
-    @GuardedBy("mInstallLock")
-    void reconcileAppsDataLI(String volumeUuid, int userId, int flags,
-            boolean migrateAppData) {
-        reconcileAppsDataLI(volumeUuid, userId, flags, migrateAppData, false /* onlyCoreApps */);
-    }
-
-    /**
-     * Reconcile all app data on given mounted volume.
-     * <p>
-     * Destroys app data that isn't expected, either due to uninstallation or
-     * reinstallation on another volume.
-     * <p>
-     * Verifies that directories exist and that ownership and labeling is
-     * correct for all installed apps.
-     * @return list of skipped non-core packages (if {@code onlyCoreApps} is true)
-     */
-    @GuardedBy("mInstallLock")
-    private List<String> reconcileAppsDataLI(String volumeUuid, int userId, int flags,
-            boolean migrateAppData, boolean onlyCoreApps) {
-        Slog.v(TAG, "reconcileAppsData for " + volumeUuid + " u" + userId + " 0x"
-                + Integer.toHexString(flags) + " migrateAppData=" + migrateAppData);
-        List<String> result = onlyCoreApps ? new ArrayList<>() : null;
-
-        final File ceDir = Environment.getDataUserCeDirectory(volumeUuid, userId);
-        final File deDir = Environment.getDataUserDeDirectory(volumeUuid, userId);
-
-        // First look for stale data that doesn't belong, and check if things
-        // have changed since we did our last restorecon
-        if ((flags & StorageManager.FLAG_STORAGE_CE) != 0) {
-            if (StorageManager.isFileEncryptedNativeOrEmulated()
-                    && !StorageManager.isUserKeyUnlocked(userId)) {
-                throw new RuntimeException(
-                        "Yikes, someone asked us to reconcile CE storage while " + userId
-                                + " was still locked; this would have caused massive data loss!");
-            }
-
-            final File[] files = FileUtils.listFilesOrEmpty(ceDir);
-            for (File file : files) {
-                final String packageName = file.getName();
-                try {
-                    assertPackageKnownAndInstalled(volumeUuid, packageName, userId);
-                } catch (PackageManagerException e) {
-                    logCriticalInfo(Log.WARN, "Destroying " + file + " due to: " + e);
-                    try {
-                        mInstaller.destroyAppData(volumeUuid, packageName, userId,
-                                StorageManager.FLAG_STORAGE_CE, 0);
-                    } catch (InstallerException e2) {
-                        logCriticalInfo(Log.WARN, "Failed to destroy: " + e2);
-                    }
-                }
-            }
-        }
-        if ((flags & StorageManager.FLAG_STORAGE_DE) != 0) {
-            final File[] files = FileUtils.listFilesOrEmpty(deDir);
-            for (File file : files) {
-                final String packageName = file.getName();
-                try {
-                    assertPackageKnownAndInstalled(volumeUuid, packageName, userId);
-                } catch (PackageManagerException e) {
-                    logCriticalInfo(Log.WARN, "Destroying " + file + " due to: " + e);
-                    try {
-                        mInstaller.destroyAppData(volumeUuid, packageName, userId,
-                                StorageManager.FLAG_STORAGE_DE, 0);
-                    } catch (InstallerException e2) {
-                        logCriticalInfo(Log.WARN, "Failed to destroy: " + e2);
-                    }
-                }
-            }
-        }
-
-        // Ensure that data directories are ready to roll for all packages
-        // installed for this volume and user
-        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "prepareAppDataAndMigrate");
-        Installer.Batch batch = new Installer.Batch();
-        final List<PackageSetting> packages;
-        synchronized (mLock) {
-            packages = mSettings.getVolumePackagesLPr(volumeUuid);
-        }
-        int preparedCount = 0;
-        for (PackageSetting ps : packages) {
-            final String packageName = ps.getPackageName();
-            if (ps.getPkg() == null) {
-                Slog.w(TAG, "Odd, missing scanned package " + packageName);
-                // TODO: might be due to legacy ASEC apps; we should circle back
-                // and reconcile again once they're scanned
-                continue;
-            }
-            // Skip non-core apps if requested
-            if (onlyCoreApps && !ps.getPkg().isCoreApp()) {
-                result.add(packageName);
-                continue;
-            }
-
-            if (ps.getInstalled(userId)) {
-                prepareAppDataAndMigrate(batch, ps.getPkg(), userId, flags, migrateAppData);
-                preparedCount++;
-            }
-        }
-        executeBatchLI(batch);
-        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
-
-        Slog.v(TAG, "reconcileAppsData finished " + preparedCount + " packages");
-        return result;
-    }
-
-    /**
-     * Prepare app data for the given app just after it was installed or
-     * upgraded. This method carefully only touches users that it's installed
-     * for, and it forces a restorecon to handle any seinfo changes.
-     * <p>
-     * Verifies that directories exist and that ownership and labeling is
-     * correct for all installed apps. If there is an ownership mismatch, it
-     * will try recovering system apps by wiping data; third-party app data is
-     * left intact.
-     * <p>
-     * <em>Note: To avoid a deadlock, do not call this method with {@code mLock} lock held</em>
-     */
-    void prepareAppDataAfterInstallLIF(AndroidPackage pkg) {
-        final PackageSetting ps;
-        synchronized (mLock) {
-            ps = mSettings.getPackageLPr(pkg.getPackageName());
-            mSettings.writeKernelMappingLPr(ps);
-        }
-
-        Installer.Batch batch = new Installer.Batch();
-        UserManagerInternal umInternal = mInjector.getUserManagerInternal();
-        StorageManagerInternal smInternal = mInjector.getLocalService(StorageManagerInternal.class);
-        for (UserInfo user : mUserManager.getUsers(false /*excludeDying*/)) {
-            final int flags;
-            if (StorageManager.isUserKeyUnlocked(user.id)
-                    && smInternal.isCeStoragePrepared(user.id)) {
-                flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
-            } else if (umInternal.isUserRunning(user.id)) {
-                flags = StorageManager.FLAG_STORAGE_DE;
-            } else {
-                continue;
-            }
-
-            // TODO@ashfall check ScanResult.mNeedsNewAppId, and if true instead
-            // of creating app data, migrate / change ownership of existing
-            // data.
-
-            if (ps.getInstalled(user.id)) {
-                // TODO: when user data is locked, mark that we're still dirty
-                prepareAppData(batch, pkg, user.id, flags).thenRun(() -> {
-                    // Note: this code block is executed with the Installer lock
-                    // already held, since it's invoked as a side-effect of
-                    // executeBatchLI()
-                    if (umInternal.isUserUnlockingOrUnlocked(user.id)) {
-                        // Prepare app data on external storage; currently this is used to
-                        // setup any OBB dirs that were created by the installer correctly.
-                        int uid = UserHandle.getUid(user.id, UserHandle.getAppId(pkg.getUid()));
-                        smInternal.prepareAppDataAfterInstall(pkg.getPackageName(), uid);
-                    }
-                });
-            }
-        }
-        executeBatchLI(batch);
-    }
-
-    /**
-     * Prepare app data for the given app.
-     * <p>
-     * Verifies that directories exist and that ownership and labeling is
-     * correct for all installed apps. If there is an ownership mismatch, this
-     * will try recovering system apps by wiping data; third-party app data is
-     * left intact.
-     */
-    private @NonNull CompletableFuture<?> prepareAppData(@NonNull Installer.Batch batch,
-            @Nullable AndroidPackage pkg, int userId, int flags) {
-        if (pkg == null) {
-            Slog.wtf(TAG, "Package was null!", new Throwable());
-            return CompletableFuture.completedFuture(null);
-        }
-        return prepareAppDataLeaf(batch, pkg, userId, flags);
-    }
-
-    private void prepareAppDataAndMigrate(@NonNull Installer.Batch batch,
-            @NonNull AndroidPackage pkg, int userId, int flags, boolean maybeMigrateAppData) {
-        prepareAppData(batch, pkg, userId, flags).thenRun(() -> {
-            // Note: this code block is executed with the Installer lock
-            // already held, since it's invoked as a side-effect of
-            // executeBatchLI()
-            if (maybeMigrateAppData && maybeMigrateAppDataLIF(pkg, userId)) {
-                // We may have just shuffled around app data directories, so
-                // prepare them one more time
-                final Installer.Batch batchInner = new Installer.Batch();
-                prepareAppData(batchInner, pkg, userId, flags);
-                executeBatchLI(batchInner);
-            }
-        });
-    }
-
-    private @NonNull CompletableFuture<?> prepareAppDataLeaf(@NonNull Installer.Batch batch,
-            @NonNull AndroidPackage pkg, int userId, int flags) {
-        if (DEBUG_APP_DATA) {
-            Slog.v(TAG, "prepareAppData for " + pkg.getPackageName() + " u" + userId + " 0x"
-                    + Integer.toHexString(flags));
-        }
-
-        final PackageSetting ps;
-        final String seInfoUser;
-        synchronized (mLock) {
-            ps = mSettings.getPackageLPr(pkg.getPackageName());
-            seInfoUser = SELinuxUtil.getSeinfoUser(ps.readUserState(userId));
-        }
-        final String volumeUuid = pkg.getVolumeUuid();
-        final String packageName = pkg.getPackageName();
-
-        final int appId = UserHandle.getAppId(pkg.getUid());
-
-        String pkgSeInfo = AndroidPackageUtils.getSeInfo(pkg, ps);
-
-        Preconditions.checkNotNull(pkgSeInfo);
-
-        final String seInfo = pkgSeInfo + seInfoUser;
-        final int targetSdkVersion = pkg.getTargetSdkVersion();
-
-        return batch.createAppData(volumeUuid, packageName, userId, flags, appId, seInfo,
-                targetSdkVersion).whenComplete((ceDataInode, e) -> {
-                    // Note: this code block is executed with the Installer lock
-                    // already held, since it's invoked as a side-effect of
-                    // executeBatchLI()
-                    if (e != null) {
-                        logCriticalInfo(Log.WARN, "Failed to create app data for " + packageName
-                                + ", but trying to recover: " + e);
-                        mRemovePackageHelper.destroyAppDataLeafLIF(pkg, userId, flags);
-                        try {
-                            ceDataInode = mInstaller.createAppData(volumeUuid, packageName, userId,
-                                    flags, appId, seInfo, pkg.getTargetSdkVersion());
-                            logCriticalInfo(Log.DEBUG, "Recovery succeeded!");
-                        } catch (InstallerException e2) {
-                            logCriticalInfo(Log.DEBUG, "Recovery failed!");
-                        }
-                    }
-
-                    // Prepare the application profiles only for upgrades and
-                    // first boot (so that we don't repeat the same operation at
-                    // each boot).
-                    //
-                    // We only have to cover the upgrade and first boot here
-                    // because for app installs we prepare the profiles before
-                    // invoking dexopt (in installPackageLI).
-                    //
-                    // We also have to cover non system users because we do not
-                    // call the usual install package methods for them.
-                    //
-                    // NOTE: in order to speed up first boot time we only create
-                    // the current profile and do not update the content of the
-                    // reference profile. A system image should already be
-                    // configured with the right profile keys and the profiles
-                    // for the speed-profile prebuilds should already be copied.
-                    // That's done in #performDexOptUpgrade.
-                    //
-                    // TODO(calin, mathieuc): We should use .dm files for
-                    // prebuilds profiles instead of manually copying them in
-                    // #performDexOptUpgrade. When we do that we should have a
-                    // more granular check here and only update the existing
-                    // profiles.
-                    if (mIsUpgrade || mFirstBoot || (userId != UserHandle.USER_SYSTEM)) {
-                        mArtManagerService.prepareAppProfiles(pkg, userId,
-                            /* updateReferenceProfileContent= */ false);
-                    }
-
-                    if ((flags & StorageManager.FLAG_STORAGE_CE) != 0 && ceDataInode != -1) {
-                        // TODO: mark this structure as dirty so we persist it!
-                        synchronized (mLock) {
-                            if (ps != null) {
-                                ps.setCeDataInode(ceDataInode, userId);
-                            }
-                        }
-                    }
-
-                    prepareAppDataContentsLeafLIF(pkg, ps, userId, flags);
-                });
-    }
-
-    private void prepareAppDataContentsLIF(AndroidPackage pkg, @Nullable PackageSetting pkgSetting,
-            int userId, int flags) {
-        if (pkg == null) {
-            Slog.wtf(TAG, "Package was null!", new Throwable());
-            return;
-        }
-        prepareAppDataContentsLeafLIF(pkg, pkgSetting, userId, flags);
-    }
-
-    private void prepareAppDataContentsLeafLIF(AndroidPackage pkg,
-            @Nullable PackageSetting pkgSetting, int userId, int flags) {
-        final String volumeUuid = pkg.getVolumeUuid();
-        final String packageName = pkg.getPackageName();
-
-        if ((flags & StorageManager.FLAG_STORAGE_CE) != 0) {
-            // Create a native library symlink only if we have native libraries
-            // and if the native libraries are 32 bit libraries. We do not provide
-            // this symlink for 64 bit libraries.
-            String primaryCpuAbi = AndroidPackageUtils.getPrimaryCpuAbi(pkg, pkgSetting);
-            if (primaryCpuAbi != null && !VMRuntime.is64BitAbi(primaryCpuAbi)) {
-                final String nativeLibPath = pkg.getNativeLibraryDir();
-                try {
-                    mInstaller.linkNativeLibraryDirectory(volumeUuid, packageName,
-                            nativeLibPath, userId);
-                } catch (InstallerException e) {
-                    Slog.e(TAG, "Failed to link native for " + packageName + ": " + e);
-                }
-            }
-        }
-    }
-
-    /**
-     * For system apps on non-FBE devices, this method migrates any existing
-     * CE/DE data to match the {@code defaultToDeviceProtectedStorage} flag
-     * requested by the app.
-     */
-    private boolean maybeMigrateAppDataLIF(AndroidPackage pkg, int userId) {
-        if (pkg.isSystem() && !StorageManager.isFileEncryptedNativeOrEmulated()
-                && PackageManager.APPLY_DEFAULT_TO_DEVICE_PROTECTED_STORAGE) {
-            final int storageTarget = pkg.isDefaultToDeviceProtectedStorage()
-                    ? StorageManager.FLAG_STORAGE_DE : StorageManager.FLAG_STORAGE_CE;
-            try {
-                mInstaller.migrateAppData(pkg.getVolumeUuid(), pkg.getPackageName(), userId,
-                        storageTarget);
-            } catch (InstallerException e) {
-                logCriticalInfo(Log.WARN,
-                        "Failed to migrate " + pkg.getPackageName() + ": " + e.getMessage());
-            }
-            return true;
-        } else {
-            return false;
-        }
-    }
-
     public PackageFreezer freezePackage(String packageName, String killReason) {
         return freezePackage(packageName, UserHandle.USER_ALL, killReason);
     }
@@ -14635,6 +14171,12 @@
                 return block.apply(snapshot::getPackageSetting);
             }
         }
+
+        @Override
+        public void reconcileAppsData(int userId, int flags, boolean migrateAppsData) {
+            PackageManagerService.this.mAppDataHelper.reconcileAppsData(userId, flags,
+                    migrateAppsData);
+        }
     }
 
     @GuardedBy("mLock")
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 0b5c5c8..bcf2f92 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -57,14 +57,20 @@
     private final Installer mInstaller;
     private final UserManagerInternal mUserManagerInternal;
     private final PermissionManagerServiceInternal mPermissionManager;
+    private final AppDataHelper mAppDataHelper;
 
     // TODO(b/198166813): remove PMS dependency
-    RemovePackageHelper(PackageManagerService pm) {
+    RemovePackageHelper(PackageManagerService pm, AppDataHelper appDataHelper) {
         mPm = pm;
         mIncrementalManager = mPm.mInjector.getIncrementalManager();
         mInstaller = mPm.mInjector.getInstaller();
         mUserManagerInternal = mPm.mInjector.getUserManagerInternal();
         mPermissionManager = mPm.mInjector.getPermissionManagerServiceInternal();
+        mAppDataHelper = appDataHelper;
+    }
+
+    RemovePackageHelper(PackageManagerService pm) {
+        this(pm, new AppDataHelper(pm));
     }
 
     @GuardedBy("mPm.mInstallLock")
@@ -214,9 +220,8 @@
             outInfo.mInstallerPackageName = deletedPs.getInstallSource().installerPackageName;
             outInfo.mIsStaticSharedLib = deletedPkg != null
                     && deletedPkg.getStaticSharedLibName() != null;
-            outInfo.populateUsers(
-                    deletedPs == null ? null : deletedPs.queryInstalledUsers(
-                            mUserManagerInternal.getUserIds(), true), deletedPs);
+            outInfo.populateUsers(deletedPs.queryInstalledUsers(
+                    mUserManagerInternal.getUserIds(), true), deletedPs);
         }
 
         removePackageLI(deletedPs.getPackageName(), (flags & PackageManager.DELETE_CHATTY) != 0);
@@ -231,9 +236,9 @@
                 resolvedPkg = PackageImpl.buildFakeForDeletion(deletedPs.getPackageName(),
                         deletedPs.getVolumeUuid());
             }
-            destroyAppDataLIF(resolvedPkg, UserHandle.USER_ALL,
+            mAppDataHelper.destroyAppDataLIF(resolvedPkg, UserHandle.USER_ALL,
                     FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL);
-            destroyAppProfilesLIF(resolvedPkg);
+            mAppDataHelper.destroyAppProfilesLIF(resolvedPkg);
             if (outInfo != null) {
                 outInfo.mDataRemoved = true;
             }
@@ -243,58 +248,53 @@
 
         // writer
         boolean installedStateChanged = false;
-        if (deletedPs != null) {
-            if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) {
-                final SparseBooleanArray changedUsers = new SparseBooleanArray();
-                synchronized (mPm.mLock) {
-                    mPm.mDomainVerificationManager.clearPackage(deletedPs.getPackageName());
-                    mPm.mSettings.getKeySetManagerService().removeAppKeySetDataLPw(packageName);
-                    mPm.mAppsFilter.removePackage(mPm.getPackageSetting(packageName),
-                            false /* isReplace */);
-                    removedAppId = mPm.mSettings.removePackageLPw(packageName);
-                    if (outInfo != null) {
-                        outInfo.mRemovedAppId = removedAppId;
-                    }
-                    if (!mPm.mSettings.isDisabledSystemPackageLPr(packageName)) {
-                        // If we don't have a disabled system package to reinstall, the package is
-                        // really gone and its permission state should be removed.
-                        final SharedUserSetting sus = deletedPs.getSharedUser();
-                        List<AndroidPackage> sharedUserPkgs = sus != null ? sus.getPackages()
-                                : null;
-                        if (sharedUserPkgs == null) {
-                            sharedUserPkgs = Collections.emptyList();
-                        }
-                        mPermissionManager.onPackageUninstalled(packageName, deletedPs.getAppId(),
-                                deletedPs.getPkg(), sharedUserPkgs, UserHandle.USER_ALL);
-                    }
-                    mPm.clearPackagePreferredActivitiesLPw(
-                            deletedPs.getPackageName(), changedUsers, UserHandle.USER_ALL);
+        if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) {
+            final SparseBooleanArray changedUsers = new SparseBooleanArray();
+            synchronized (mPm.mLock) {
+                mPm.mDomainVerificationManager.clearPackage(deletedPs.getPackageName());
+                mPm.mSettings.getKeySetManagerService().removeAppKeySetDataLPw(packageName);
+                mPm.mAppsFilter.removePackage(mPm.getPackageSetting(packageName),
+                        false /* isReplace */);
+                removedAppId = mPm.mSettings.removePackageLPw(packageName);
+                if (outInfo != null) {
+                    outInfo.mRemovedAppId = removedAppId;
+                }
+                if (!mPm.mSettings.isDisabledSystemPackageLPr(packageName)) {
+                    // If we don't have a disabled system package to reinstall, the package is
+                    // really gone and its permission state should be removed.
+                    final SharedUserSetting sus = deletedPs.getSharedUser();
+                    List<AndroidPackage> sharedUserPkgs =
+                            sus != null ? sus.getPackages() : Collections.emptyList();
+                    mPermissionManager.onPackageUninstalled(packageName, deletedPs.getAppId(),
+                            deletedPs.getPkg(), sharedUserPkgs, UserHandle.USER_ALL);
+                }
+                mPm.clearPackagePreferredActivitiesLPw(
+                        deletedPs.getPackageName(), changedUsers, UserHandle.USER_ALL);
 
-                    mPm.mSettings.removeRenamedPackageLPw(deletedPs.getRealName());
-                }
-                if (changedUsers.size() > 0) {
-                    mPm.updateDefaultHomeNotLocked(changedUsers);
-                    mPm.postPreferredActivityChangedBroadcast(UserHandle.USER_ALL);
-                }
+                mPm.mSettings.removeRenamedPackageLPw(deletedPs.getRealName());
             }
-            // make sure to preserve per-user disabled state if this removal was just
-            // a downgrade of a system app to the factory package
-            if (outInfo != null && outInfo.mOrigUsers != null) {
+            if (changedUsers.size() > 0) {
+                mPm.updateDefaultHomeNotLocked(changedUsers);
+                mPm.postPreferredActivityChangedBroadcast(UserHandle.USER_ALL);
+            }
+        }
+        // make sure to preserve per-user disabled state if this removal was just
+        // a downgrade of a system app to the factory package
+        if (outInfo != null && outInfo.mOrigUsers != null) {
+            if (DEBUG_REMOVE) {
+                Slog.d(TAG, "Propagating install state across downgrade");
+            }
+            for (int userId : allUserHandles) {
+                final boolean installed = ArrayUtils.contains(outInfo.mOrigUsers, userId);
                 if (DEBUG_REMOVE) {
-                    Slog.d(TAG, "Propagating install state across downgrade");
+                    Slog.d(TAG, "    user " + userId + " => " + installed);
                 }
-                for (int userId : allUserHandles) {
-                    final boolean installed = ArrayUtils.contains(outInfo.mOrigUsers, userId);
-                    if (DEBUG_REMOVE) {
-                        Slog.d(TAG, "    user " + userId + " => " + installed);
-                    }
-                    if (installed != deletedPs.getInstalled(userId)) {
-                        installedStateChanged = true;
-                    }
-                    deletedPs.setInstalled(installed, userId);
-                    if (installed) {
-                        deletedPs.setUninstallReason(UNINSTALL_REASON_UNKNOWN, userId);
-                    }
+                if (installed != deletedPs.getInstalled(userId)) {
+                    installedStateChanged = true;
+                }
+                deletedPs.setInstalled(installed, userId);
+                if (installed) {
+                    deletedPs.setUninstallReason(UNINSTALL_REASON_UNKNOWN, userId);
                 }
             }
         }
@@ -315,45 +315,4 @@
                     mUserManagerInternal, UserHandle.USER_ALL, removedAppId);
         }
     }
-
-    public void destroyAppDataLIF(AndroidPackage pkg, int userId, int flags) {
-        if (pkg == null) {
-            Slog.wtf(TAG, "Package was null!", new Throwable());
-            return;
-        }
-        destroyAppDataLeafLIF(pkg, userId, flags);
-    }
-
-    public void destroyAppDataLeafLIF(AndroidPackage pkg, int userId, int flags) {
-        final PackageSetting ps;
-        synchronized (mPm.mLock) {
-            ps = mPm.mSettings.getPackageLPr(pkg.getPackageName());
-        }
-        for (int realUserId : mPm.resolveUserIds(userId)) {
-            final long ceDataInode = (ps != null) ? ps.getCeDataInode(realUserId) : 0;
-            try {
-                mInstaller.destroyAppData(pkg.getVolumeUuid(), pkg.getPackageName(), realUserId,
-                        flags, ceDataInode);
-            } catch (Installer.InstallerException e) {
-                Slog.w(TAG, String.valueOf(e));
-            }
-            mPm.getDexManager().notifyPackageDataDestroyed(pkg.getPackageName(), userId);
-        }
-    }
-
-    public void destroyAppProfilesLIF(AndroidPackage pkg) {
-        if (pkg == null) {
-            Slog.wtf(TAG, "Package was null!", new Throwable());
-            return;
-        }
-        destroyAppProfilesLeafLIF(pkg);
-    }
-
-    private void destroyAppProfilesLeafLIF(AndroidPackage pkg) {
-        try {
-            mInstaller.destroyAppProfiles(pkg.getPackageName());
-        } catch (Installer.InstallerException e) {
-            Slog.w(TAG, String.valueOf(e));
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/pm/ScanPackageHelper.java b/services/core/java/com/android/server/pm/ScanPackageHelper.java
index 3bf0af7..ef06c58 100644
--- a/services/core/java/com/android/server/pm/ScanPackageHelper.java
+++ b/services/core/java/com/android/server/pm/ScanPackageHelper.java
@@ -1125,7 +1125,8 @@
             return;
         }
 
-        mPm.clearAppProfilesLIF(pkg);
+        final AppDataHelper appDataHelper = new AppDataHelper(mPm);
+        appDataHelper.clearAppProfilesLIF(pkg);
         if (DEBUG_INSTALL) {
             Slog.d(TAG, originalPkgSetting.getPackageName()
                     + " clear profile due to version change "
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 85adaa0..0902e4a 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -802,7 +802,9 @@
                 disabled = p;
             }
             mDisabledSysPackages.put(name, disabled);
-
+            if (disabled.getSharedUser() != null) {
+                disabled.getSharedUser().mDisabledPackages.add(disabled);
+            }
             return true;
         }
         return false;
@@ -814,6 +816,9 @@
             Log.w(PackageManagerService.TAG, "Package " + name + " is not disabled");
             return null;
         }
+        if (p.getSharedUser() != null) {
+            p.getSharedUser().mDisabledPackages.remove(p);
+        }
         p.getPkgState().setUpdatedSystemApp(false);
         PackageSetting ret = addPackageLPw(name, p.getRealName(), p.getPath(),
                 p.getLegacyNativeLibraryPath(), p.getPrimaryCpuAbi(),
@@ -833,7 +838,11 @@
     }
 
     void removeDisabledSystemPackageLPw(String name) {
-        mDisabledSysPackages.remove(name);
+        final PackageSetting p = mDisabledSysPackages.remove(name);
+        if (p != null && p.getSharedUser() != null) {
+            p.getSharedUser().mDisabledPackages.remove(p);
+            checkAndPruneSharedUserLPw(p.getSharedUser(), false);
+        }
     }
 
     PackageSetting addPackageLPw(String name, String realName, File codePath,
@@ -883,27 +892,24 @@
     }
 
     void pruneSharedUsersLPw() {
-        ArrayList<String> removeStage = new ArrayList<String>();
-        for (Map.Entry<String,SharedUserSetting> entry : mSharedUsers.entrySet()) {
+        List<String> removeKeys = new ArrayList<>();
+        List<SharedUserSetting> removeValues = new ArrayList<>();
+        for (Map.Entry<String, SharedUserSetting> entry : mSharedUsers.entrySet()) {
             final SharedUserSetting sus = entry.getValue();
             if (sus == null) {
-                removeStage.add(entry.getKey());
+                removeKeys.add(entry.getKey());
                 continue;
             }
             // remove packages that are no longer installed
-            for (Iterator<PackageSetting> iter = sus.packages.iterator(); iter.hasNext();) {
-                PackageSetting ps = iter.next();
-                if (mPackages.get(ps.getPackageName()) == null) {
-                    iter.remove();
-                }
-            }
-            if (sus.packages.size() == 0) {
-                removeStage.add(entry.getKey());
+            sus.packages.removeIf(ps -> mPackages.get(ps.getPackageName()) == null);
+            sus.mDisabledPackages.removeIf(
+                    ps -> mDisabledSysPackages.get(ps.getPackageName()) == null);
+            if (sus.packages.isEmpty() && sus.mDisabledPackages.isEmpty()) {
+                removeValues.add(sus);
             }
         }
-        for (int i = 0; i < removeStage.size(); i++) {
-            mSharedUsers.remove(removeStage.get(i));
-        }
+        removeKeys.forEach(mSharedUsers::remove);
+        removeValues.forEach(sus -> checkAndPruneSharedUserLPw(sus, true));
     }
 
     /**
@@ -1233,18 +1239,20 @@
         }
     }
 
+    private void checkAndPruneSharedUserLPw(SharedUserSetting s, boolean skipCheck) {
+        if (skipCheck || (s.packages.isEmpty() && s.mDisabledPackages.isEmpty())) {
+            mSharedUsers.remove(s.name);
+            removeAppIdLPw(s.userId);
+        }
+    }
+
     int removePackageLPw(String name) {
-        final PackageSetting p = mPackages.get(name);
+        final PackageSetting p = mPackages.remove(name);
         if (p != null) {
-            mPackages.remove(name);
             removeInstallerPackageStatus(name);
             if (p.getSharedUser() != null) {
                 p.getSharedUser().removePackage(p);
-                if (p.getSharedUser().packages.size() == 0) {
-                    mSharedUsers.remove(p.getSharedUser().name);
-                    removeAppIdLPw(p.getSharedUser().userId);
-                    return p.getSharedUser().userId;
-                }
+                checkAndPruneSharedUserLPw(p.getSharedUser(), false);
             } else {
                 removeAppIdLPw(p.getAppId());
                 return p.getAppId();
@@ -3052,17 +3060,16 @@
          * Make sure all the updated system packages have their shared users
          * associated with them.
          */
-        final Iterator<PackageSetting> disabledIt = mDisabledSysPackages.values().iterator();
-        while (disabledIt.hasNext()) {
-            final PackageSetting disabledPs = disabledIt.next();
+        for (PackageSetting disabledPs : mDisabledSysPackages.values()) {
             final Object id = getSettingLPr(disabledPs.getAppId());
-            if (id != null && id instanceof SharedUserSetting) {
+            if (id instanceof SharedUserSetting) {
                 disabledPs.setSharedUser((SharedUserSetting) id);
+                disabledPs.getSharedUser().mDisabledPackages.add(disabledPs);
             }
         }
 
-        mReadMessages.append("Read completed successfully: " + mPackages.size() + " packages, "
-                + mSharedUsers.size() + " shared uids\n");
+        mReadMessages.append("Read completed successfully: ").append(mPackages.size())
+                .append(" packages, ").append(mSharedUsers.size()).append(" shared uids\n");
 
         writeKernelMappingLPr();
 
diff --git a/services/core/java/com/android/server/pm/SharedUserSetting.java b/services/core/java/com/android/server/pm/SharedUserSetting.java
index 15df249..3055747 100644
--- a/services/core/java/com/android/server/pm/SharedUserSetting.java
+++ b/services/core/java/com/android/server/pm/SharedUserSetting.java
@@ -16,7 +16,7 @@
 
 package com.android.server.pm;
 
-import android.annotation.Nullable;
+import android.annotation.NonNull;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.parsing.component.ParsedProcess;
 import android.service.pm.PackageServiceDumpProto;
@@ -31,6 +31,7 @@
 import libcore.util.EmptyArray;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -52,6 +53,11 @@
 
     final ArraySet<PackageSetting> packages;
 
+    // It is possible for a system app to leave shared user ID by an update.
+    // We need to keep track of the shadowed PackageSettings so that it is possible to uninstall
+    // the update and revert the system app back into the original shared user ID.
+    final ArraySet<PackageSetting> mDisabledPackages;
+
     final PackageSignatures signatures = new PackageSignatures();
     Boolean signaturesChanged;
 
@@ -77,6 +83,7 @@
         name = _name;
         seInfoTargetSdkVersion = android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
         packages = new ArraySet<>();
+        mDisabledPackages = new ArraySet<>();
         processes = new ArrayMap<>();
         mSnapshot = makeCache();
     }
@@ -87,13 +94,14 @@
         name = orig.name;
         uidFlags = orig.uidFlags;
         uidPrivateFlags = orig.uidPrivateFlags;
-        packages = new ArraySet(orig.packages);
+        packages = new ArraySet<>(orig.packages);
+        mDisabledPackages = new ArraySet<>(orig.mDisabledPackages);
         // A SigningDetails seems to consist solely of final attributes, so
         // it is safe to copy the reference.
         signatures.mSigningDetails = orig.signatures.mSigningDetails;
         signaturesChanged = orig.signaturesChanged;
-        processes = new ArrayMap(orig.processes);
-        mSnapshot = new SnapshotCache.Sealed();
+        processes = new ArrayMap<>(orig.processes);
+        mSnapshot = new SnapshotCache.Sealed<>();
     }
 
     /**
@@ -174,9 +182,12 @@
         }
     }
 
-    public @Nullable List<AndroidPackage> getPackages() {
+    /**
+     * @return the list of packages that uses this shared UID
+     */
+    public @NonNull List<AndroidPackage> getPackages() {
         if (packages == null || packages.size() == 0) {
-            return null;
+            return Collections.emptyList();
         }
         final ArrayList<AndroidPackage> pkgList = new ArrayList<>(packages.size());
         for (PackageSetting ps : packages) {
diff --git a/services/core/java/com/android/server/pm/StorageEventHelper.java b/services/core/java/com/android/server/pm/StorageEventHelper.java
index 4b530b5..a82cb9e 100644
--- a/services/core/java/com/android/server/pm/StorageEventHelper.java
+++ b/services/core/java/com/android/server/pm/StorageEventHelper.java
@@ -129,6 +129,7 @@
             return;
         }
 
+        final AppDataHelper appDataHelper = new AppDataHelper(mPm);
         final ArrayList<PackageFreezer> freezers = new ArrayList<>();
         final ArrayList<AndroidPackage> loaded = new ArrayList<>();
         final int parseFlags = mPm.mDefParseFlags | ParsingPackageUtils.PARSE_EXTERNAL_STORAGE;
@@ -155,7 +156,7 @@
                 }
 
                 if (!Build.FINGERPRINT.equals(ver.fingerprint)) {
-                    mPm.clearAppDataLIF(
+                    appDataHelper.clearAppDataLIF(
                             ps.getPkg(), UserHandle.USER_ALL, FLAG_STORAGE_DE | FLAG_STORAGE_CE
                             | FLAG_STORAGE_EXTERNAL | Installer.FLAG_CLEAR_CODE_CACHE_ONLY
                             | Installer.FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES);
@@ -182,7 +183,8 @@
             try {
                 sm.prepareUserStorage(volumeUuid, user.id, user.serialNumber, flags);
                 synchronized (mPm.mInstallLock) {
-                    mPm.reconcileAppsDataLI(volumeUuid, user.id, flags, true /* migrateAppData */);
+                    appDataHelper.reconcileAppsDataLI(volumeUuid, user.id, flags,
+                            true /* migrateAppData */);
                 }
             } catch (IllegalStateException e) {
                 // Device was probably ejected, and we'll process that event momentarily
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 30c77f9..302826f 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -4816,7 +4816,8 @@
         mUserDataPreparer.prepareUserData(userId, userSerial, StorageManager.FLAG_STORAGE_DE);
         t.traceEnd();
         t.traceBegin("reconcileAppsData");
-        mPm.reconcileAppsData(userId, StorageManager.FLAG_STORAGE_DE, migrateAppsData);
+        getPackageManagerInternal().reconcileAppsData(userId, StorageManager.FLAG_STORAGE_DE,
+                migrateAppsData);
         t.traceEnd();
 
         if (userId != UserHandle.USER_SYSTEM) {
@@ -4851,7 +4852,8 @@
         smInternal.markCeStoragePrepared(userId);
 
         t.traceBegin("reconcileAppsData-" + userId);
-        mPm.reconcileAppsData(userId, StorageManager.FLAG_STORAGE_CE, migrateAppsData);
+        getPackageManagerInternal().reconcileAppsData(userId, StorageManager.FLAG_STORAGE_CE,
+                migrateAppsData);
         t.traceEnd();
     }
 
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index 3a1ffe1..626146c2 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -140,7 +140,8 @@
             UserManager.DISALLOW_PRINTING,
             UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
             UserManager.DISALLOW_MICROPHONE_TOGGLE,
-            UserManager.DISALLOW_CAMERA_TOGGLE
+            UserManager.DISALLOW_CAMERA_TOGGLE,
+            UserManager.DISALLOW_CHANGE_WIFI_STATE
     });
 
     public static final Set<String> DEPRECATED_USER_RESTRICTIONS = Sets.newArraySet(
@@ -184,7 +185,8 @@
             UserManager.DISALLOW_USER_SWITCH,
             UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
             UserManager.DISALLOW_MICROPHONE_TOGGLE,
-            UserManager.DISALLOW_CAMERA_TOGGLE
+            UserManager.DISALLOW_CAMERA_TOGGLE,
+            UserManager.DISALLOW_CHANGE_WIFI_STATE
     );
 
     /**
@@ -219,7 +221,8 @@
             Sets.newArraySet(
                     UserManager.DISALLOW_AIRPLANE_MODE,
                     UserManager.DISALLOW_CONFIG_DATE_TIME,
-                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS
+                    UserManager.DISALLOW_CONFIG_PRIVATE_DNS,
+                    UserManager.DISALLOW_CHANGE_WIFI_STATE
     );
 
     /**
diff --git a/services/core/java/com/android/server/pm/dex/OWNERS b/services/core/java/com/android/server/pm/dex/OWNERS
index 5a4431e..052a4ca 100644
--- a/services/core/java/com/android/server/pm/dex/OWNERS
+++ b/services/core/java/com/android/server/pm/dex/OWNERS
@@ -1,2 +1,3 @@
-calin@google.com
+alanstokes@google.com
+jiakaiz@google.com
 ngeoffray@google.com
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 0e939ef..ece0a62b 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -213,6 +213,7 @@
         NEARBY_DEVICES_PERMISSIONS.add(Manifest.permission.BLUETOOTH_CONNECT);
         NEARBY_DEVICES_PERMISSIONS.add(Manifest.permission.BLUETOOTH_SCAN);
         NEARBY_DEVICES_PERMISSIONS.add(Manifest.permission.UWB_RANGING);
+        NEARBY_DEVICES_PERMISSIONS.add(Manifest.permission.NEARBY_WIFI_DEVICES);
     }
 
     private static final int MSG_READ_DEFAULT_PERMISSION_EXCEPTIONS = 1;
@@ -915,6 +916,11 @@
         }
         grantPermissionsToSystemPackage(pm, dialerPackage, userId,
                 CONTACTS_PERMISSIONS, SMS_PERMISSIONS, MICROPHONE_PERMISSIONS, CAMERA_PERMISSIONS);
+        boolean isAndroidAutomotive =
+                mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE, 0);
+        if (isAndroidAutomotive) {
+            grantPermissionsToSystemPackage(pm, dialerPackage, userId, NEARBY_DEVICES_PERMISSIONS);
+        }
     }
 
     private void grantDefaultPermissionsToDefaultSystemSmsApp(PackageManagerWrapper pm,
diff --git a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java
index b1676d0..ea554d3 100644
--- a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java
@@ -30,6 +30,7 @@
 import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.permission.ILegacyPermissionManager;
+import android.util.EventLog;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -187,10 +188,25 @@
     private void verifyCallerCanCheckAccess(String packageName, String message, int pid, int uid) {
         // If the check is being requested by an app then only allow the app to query its own
         // access status.
+        boolean reportError = false;
         int callingUid = mInjector.getCallingUid();
         int callingPid = mInjector.getCallingPid();
         if (UserHandle.getAppId(callingUid) >= Process.FIRST_APPLICATION_UID && (callingUid != uid
                 || callingPid != pid)) {
+            reportError = true;
+        }
+        // If the query is against an app on the device, then the check should only be allowed if
+        // the provided uid matches that of the specified package.
+        if (packageName != null && UserHandle.getAppId(uid) >= Process.FIRST_APPLICATION_UID) {
+            int packageUid = mInjector.getPackageUidForUser(packageName, UserHandle.getUserId(uid));
+            if (uid != packageUid) {
+                EventLog.writeEvent(0x534e4554, "193441322",
+                        UserHandle.getAppId(callingUid) >= Process.FIRST_APPLICATION_UID
+                                ? callingUid : uid, "Package uid mismatch");
+                reportError = true;
+            }
+        }
+        if (reportError) {
             String response = String.format(
                     "Calling uid %d, pid %d cannot access for package %s (uid=%d, pid=%d): %s",
                     callingUid, callingPid, packageName, uid, pid, message);
@@ -385,12 +401,14 @@
     @VisibleForTesting
     public static class Injector {
         private final Context mContext;
+        private final PackageManagerInternal mPackageManagerInternal;
 
         /**
          * Public constructor that accepts a {@code context} within which to operate.
          */
         public Injector(@NonNull Context context) {
             mContext = context;
+            mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         }
 
         /**
@@ -453,5 +471,12 @@
             return mContext.getPackageManager().getApplicationInfoAsUser(packageName, 0,
                     UserHandle.getUserHandleForUid(uid));
         }
+
+        /**
+         * Returns the uid for the specified {@code packageName} under the provided {@code userId}.
+         */
+        public int getPackageUidForUser(String packageName, int userId) {
+            return mPackageManagerInternal.getPackageUid(packageName, 0, userId);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 12e6086d..fa837ba 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -503,7 +503,6 @@
     private boolean mPendingKeyguardOccluded;
     private boolean mKeyguardOccludedChanged;
 
-    private ActivityTaskManagerInternal.SleepTokenAcquirer mScreenOffSleepTokenAcquirer;
     Intent mHomeIntent;
     Intent mCarDockIntent;
     Intent mDeskDockIntent;
@@ -1627,9 +1626,6 @@
                 new AccessibilityShortcutController(mContext, new Handler(), mCurrentUserId);
         mLogger = new MetricsLogger();
 
-        mScreenOffSleepTokenAcquirer = mActivityTaskManagerInternal
-                .createSleepTokenAcquirer("ScreenOff");
-
         Resources res = mContext.getResources();
         mWakeOnDpadKeyPress =
                 res.getBoolean(com.android.internal.R.bool.config_wakeOnDpadKeyPress);
@@ -4392,7 +4388,6 @@
         if (DEBUG_WAKEUP) Slog.i(TAG, "Display" + displayId + " turned off...");
 
         if (displayId == DEFAULT_DISPLAY) {
-            updateScreenOffSleepToken(true);
             mRequestedOrSleepingDefaultDisplay = false;
             mDefaultDisplayPolicy.screenTurnedOff();
             synchronized (mLock) {
@@ -4425,7 +4420,6 @@
         if (displayId == DEFAULT_DISPLAY) {
             Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "screenTurningOn",
                     0 /* cookie */);
-            updateScreenOffSleepToken(false);
             mDefaultDisplayPolicy.screenTurnedOn(screenOnListener);
             mBootAnimationDismissable = false;
 
@@ -4940,15 +4934,6 @@
         }
     }
 
-    // TODO (multidisplay): Support multiple displays in WindowManagerPolicy.
-    private void updateScreenOffSleepToken(boolean acquire) {
-        if (acquire) {
-            mScreenOffSleepTokenAcquirer.acquire(DEFAULT_DISPLAY);
-        } else {
-            mScreenOffSleepTokenAcquirer.release(DEFAULT_DISPLAY);
-        }
-    }
-
     /** {@inheritDoc} */
     @Override
     public void enableScreenAfterBoot() {
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 78b03b2..496cd9e3 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -27,6 +27,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_BOOT_PROGRESS;
 import static android.view.WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
@@ -78,7 +79,6 @@
 import android.util.Slog;
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
-import android.view.IApplicationToken;
 import android.view.IDisplayFoldListener;
 import android.view.IWindowManager;
 import android.view.KeyEvent;
@@ -203,14 +203,6 @@
         public int getBaseType();
 
         /**
-         * Return the token for the application (actually activity) that owns
-         * this window.  May return null for system windows.
-         *
-         * @return An IApplicationToken identifying the owning activity.
-         */
-        public IApplicationToken getAppToken();
-
-        /**
          * Return true if this window (or a window it is attached to, but not
          * considering its app token) is currently animating.
          */
@@ -676,7 +668,7 @@
      */
     default boolean canBeHiddenByKeyguardLw(WindowState win) {
         // Keyguard visibility of window from activities are determined over activity visibility.
-        if (win.getAppToken() != null) {
+        if (win.getBaseType() == TYPE_BASE_APPLICATION) {
             return false;
         }
         switch (win.getAttrs().type) {
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
index 051f555..c356fec 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
@@ -195,6 +195,12 @@
 
     @Override // Binder interface
     public void doKeyguardTimeout(Bundle options) {
+        int userId = mKeyguardStateMonitor.getCurrentUser();
+        if (mKeyguardStateMonitor.isSecure(userId)) {
+            // Preemptively inform the cache that the keyguard will soon be showing, as calls to
+            // doKeyguardTimeout are a signal to lock the device as soon as possible.
+            mKeyguardStateMonitor.onShowingStateChanged(true, userId);
+        }
         try {
             mService.doKeyguardTimeout(options);
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardStateMonitor.java b/services/core/java/com/android/server/policy/keyguard/KeyguardStateMonitor.java
index add0b01..f0f62ed 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardStateMonitor.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardStateMonitor.java
@@ -83,8 +83,14 @@
         return mHasLockscreenWallpaper;
     }
 
+    public int getCurrentUser() {
+        return mCurrentUserId;
+    }
+
     @Override // Binder interface
-    public void onShowingStateChanged(boolean showing) {
+    public void onShowingStateChanged(boolean showing, int userId) {
+        if (userId != mCurrentUserId) return;
+
         mIsShowing = showing;
 
         mCallback.onShowingChanged();
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 9943b99..2cc9c86 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -2701,7 +2701,7 @@
 
             mHandler.removeMessages(MSG_ATTENTIVE_TIMEOUT);
 
-            if (isBeingKeptFromShowingInattentiveSleepWarningLocked()) {
+            if (isBeingKeptFromInattentiveSleepLocked()) {
                 return;
             }
 
@@ -2717,10 +2717,6 @@
                 }
                 mInattentiveSleepWarningOverlayController.show();
                 nextTimeout = goToSleepTime;
-            } else {
-                if (DEBUG && getWakefulnessLocked() != WAKEFULNESS_ASLEEP) {
-                    Slog.i(TAG, "Going to sleep now due to long user inactivity");
-                }
             }
 
             if (nextTimeout >= 0) {
@@ -2739,7 +2735,7 @@
         if (getWakefulnessLocked() != WAKEFULNESS_AWAKE) {
             mInattentiveSleepWarningOverlayController.dismiss(false);
             return true;
-        } else if (attentiveTimeout < 0 || isBeingKeptFromShowingInattentiveSleepWarningLocked()
+        } else if (attentiveTimeout < 0 || isBeingKeptFromInattentiveSleepLocked()
                 || now < showWarningTime) {
             mInattentiveSleepWarningOverlayController.dismiss(true);
             return true;
@@ -2861,8 +2857,11 @@
                         Slog.d(TAG, "updateWakefulnessLocked: Bed time for group " + id);
                     }
                     if (isAttentiveTimeoutExpired(id, time)) {
+                        if (DEBUG) {
+                            Slog.i(TAG, "Going to sleep now due to long user inactivity");
+                        }
                         changed = sleepDisplayGroupNoUpdateLocked(id, time,
-                                PowerManager.GO_TO_SLEEP_REASON_TIMEOUT,
+                                PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE,
                                 PowerManager.GO_TO_SLEEP_FLAG_NO_DOZE, Process.SYSTEM_UID);
                     } else if (shouldNapAtBedTimeLocked()) {
                         changed = dreamDisplayGroupNoUpdateLocked(id, time, Process.SYSTEM_UID);
@@ -2898,7 +2897,7 @@
 
         long now = mClock.uptimeMillis();
         if (isAttentiveTimeoutExpired(groupId, now)) {
-            return !isBeingKeptFromInattentiveSleepLocked(groupId);
+            return !isBeingKeptFromInattentiveSleepLocked();
         } else {
             return !isBeingKeptAwakeLocked(groupId);
         }
@@ -2928,13 +2927,7 @@
      * a phone call. This function only controls whether the device will go to sleep which is
      * independent of whether it will be allowed to suspend.
      */
-    private boolean isBeingKeptFromInattentiveSleepLocked(int groupId) {
-        return mStayOn || mScreenBrightnessBoostInProgress || mProximityPositive
-                || (mDisplayGroupPowerStateMapper.getUserActivitySummaryLocked(groupId) & (
-                USER_ACTIVITY_SCREEN_BRIGHT | USER_ACTIVITY_SCREEN_DIM)) != 0;
-    }
-
-    private boolean isBeingKeptFromShowingInattentiveSleepWarningLocked() {
+    private boolean isBeingKeptFromInattentiveSleepLocked() {
         return mStayOn || mScreenBrightnessBoostInProgress || mProximityPositive || !mBootCompleted;
     }
 
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index e7b2756..0f1563b 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -43,7 +43,6 @@
 import android.content.rollback.RollbackInfo;
 import android.content.rollback.RollbackManager;
 import android.os.Binder;
-import android.os.Build;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.HandlerExecutor;
@@ -86,6 +85,7 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
@@ -843,6 +843,11 @@
         final String packageName = newPackage.getPackageName();
         final int rollbackDataPolicy = computeRollbackDataPolicy(
                 session.rollbackDataPolicy, newPackage.getRollbackDataPolicy());
+        if (!session.isStaged() && (installFlags & PackageManager.INSTALL_APEX) != 0
+                && rollbackDataPolicy != PackageManager.ROLLBACK_DATA_POLICY_RETAIN) {
+            Slog.e(TAG, "Only RETAIN is supported for rebootless APEX: " + packageName);
+            return false;
+        }
         Slog.i(TAG, "Enabling rollback for install of " + packageName
                 + ", session:" + session.sessionId
                 + ", rollbackDataPolicy=" + rollbackDataPolicy);
@@ -1284,14 +1289,10 @@
     }
 
     private SparseIntArray getExtensionVersions() {
-        // This list must be updated whenever the current API level is increased, or should be
-        // replaced when we have another way of determining the relevant SDK versions.
-        final int[] relevantSdkVersions = { Build.VERSION_CODES.R, Build.VERSION_CODES.S };
-
-        SparseIntArray result = new SparseIntArray(relevantSdkVersions.length);
-        for (int i = 0; i < relevantSdkVersions.length; i++) {
-            result.put(relevantSdkVersions[i],
-                    SdkExtensions.getExtensionVersion(relevantSdkVersions[i]));
+        Map<Integer, Integer> allExtensionVersions = SdkExtensions.getAllExtensionVersions();
+        SparseIntArray result = new SparseIntArray(allExtensionVersions.size());
+        for (int extension : allExtensionVersions.keySet()) {
+            result.put(extension, allExtensionVersions.get(extension));
         }
         return result;
     }
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java b/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java
index 47bd72a..9ac7e3b 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/ConversionUtil.java
@@ -441,15 +441,7 @@
     private static @NonNull
     HidlMemory parcelFileDescriptorToHidlMemory(@Nullable ParcelFileDescriptor data, int dataSize) {
         if (dataSize > 0) {
-            // Extract a dup of the underlying FileDescriptor out of data.
-            FileDescriptor fd = new FileDescriptor();
-            try {
-                ParcelFileDescriptor dup = data.dup();
-                fd.setInt$(dup.detachFd());
-                return HidlMemoryUtil.fileDescriptorToHidlMemory(fd, dataSize);
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
+            return HidlMemoryUtil.fileDescriptorToHidlMemory(data.getFileDescriptor(), dataSize);
         } else {
             return HidlMemoryUtil.fileDescriptorToHidlMemory(null, 0);
         }
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index 7ac9110..00d01e1 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -23,6 +23,7 @@
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.ActivityThread;
 import android.app.ITransientNotificationCallback;
@@ -1689,7 +1690,7 @@
     }
 
     @Override
-    public int requestAddTile(
+    public void requestAddTile(
             @NonNull ComponentName componentName,
             @NonNull CharSequence label,
             @NonNull Icon icon,
@@ -1710,13 +1711,35 @@
 
         // Check current user
         if (userId != currentUser) {
-            return StatusBarManager.TILE_ADD_REQUEST_ANSWER_FAILED_NOT_CURRENT_USER;
+            try {
+                callback.onTileRequest(StatusBarManager.TILE_ADD_REQUEST_ERROR_NOT_CURRENT_USER);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "requestAddTile", e);
+            }
+            return;
         }
 
         // We've checked that the package, component name and uid all match.
         ResolveInfo r = isComponentValidTileService(componentName, userId);
-        if (r == null) {
-            return StatusBarManager.TILE_ADD_REQUEST_ANSWER_FAILED_BAD_COMPONENT;
+        if (r == null || !r.serviceInfo.exported) {
+            try {
+                callback.onTileRequest(StatusBarManager.TILE_ADD_REQUEST_ERROR_BAD_COMPONENT);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "requestAddTile", e);
+            }
+            return;
+        }
+
+        final int procState = mActivityManagerInternal.getUidProcessState(callingUid);
+        if (ActivityManager.RunningAppProcessInfo.procStateToImportance(procState)
+                != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
+            try {
+                callback.onTileRequest(
+                        StatusBarManager.TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "requestAddTile", e);
+            }
+            return;
         }
 
         IAddTileResultCallback proxyCallback = new IAddTileResultCallback.Stub() {
@@ -1734,12 +1757,16 @@
         if (mBar != null) {
             try {
                 mBar.requestAddTile(componentName, appName, label, icon, proxyCallback);
-                return StatusBarManager.TILE_ADD_REQUEST_ANSWER_SUCCESS;
             } catch (RemoteException e) {
                 Slog.e(TAG, "requestAddTile", e);
             }
+            return;
         }
-        return StatusBarManager.TILE_ADD_REQUEST_ANSWER_FAILED_UNKNOWN_REASON;
+        try {
+            callback.onTileRequest(StatusBarManager.TILE_ADD_REQUEST_ERROR_NO_STATUS_BAR_SERVICE);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "requestAddTile", e);
+        }
     }
 
     public String[] getStatusBarIcons() {
diff --git a/services/core/java/com/android/server/timedetector/OWNERS b/services/core/java/com/android/server/timedetector/OWNERS
index 8f80897..67fc9d6 100644
--- a/services/core/java/com/android/server/timedetector/OWNERS
+++ b/services/core/java/com/android/server/timedetector/OWNERS
@@ -1,3 +1,3 @@
 # Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+# This code is maintained by the same OWNERS as timezonedetector.
+include /services/core/java/com/android/server/timezonedetector/OWNERS
diff --git a/services/core/java/com/android/server/timedetector/ServerFlags.java b/services/core/java/com/android/server/timedetector/ServerFlags.java
index ac2d76e..477ebf6 100644
--- a/services/core/java/com/android/server/timedetector/ServerFlags.java
+++ b/services/core/java/com/android/server/timedetector/ServerFlags.java
@@ -55,10 +55,11 @@
      */
     @StringDef(prefix = "KEY_", value = {
             KEY_LOCATION_TIME_ZONE_DETECTION_FEATURE_SUPPORTED,
-            KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE,
-            KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE,
-            KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_FUZZ_MILLIS,
-            KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_MILLIS,
+            KEY_PRIMARY_LTZP_MODE_OVERRIDE,
+            KEY_SECONDARY_LTZP_MODE_OVERRIDE,
+            KEY_LTZP_INITIALIZATION_TIMEOUT_FUZZ_MILLIS,
+            KEY_LTZP_INITIALIZATION_TIMEOUT_MILLIS,
+            KEY_LTZP_EVENT_FILTERING_AGE_THRESHOLD_MILLIS,
             KEY_LOCATION_TIME_ZONE_DETECTION_UNCERTAINTY_DELAY_MILLIS,
             KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE,
             KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT,
@@ -82,16 +83,14 @@
      * The key for the server flag that can override the device config for whether the primary
      * location time zone provider is enabled, disabled, or (for testing) in simulation mode.
      */
-    public static final @DeviceConfigKey String
-            KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE =
+    public static final @DeviceConfigKey String KEY_PRIMARY_LTZP_MODE_OVERRIDE =
             "primary_location_time_zone_provider_mode_override";
 
     /**
      * The key for the server flag that can override the device config for whether the secondary
      * location time zone provider is enabled or disabled, or (for testing) in simulation mode.
      */
-    public static final @DeviceConfigKey String
-            KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE =
+    public static final @DeviceConfigKey String KEY_SECONDARY_LTZP_MODE_OVERRIDE =
             "secondary_location_time_zone_provider_mode_override";
 
     /**
@@ -106,18 +105,20 @@
      * The key for the timeout passed to a location time zone provider that tells it how long it has
      * to provide an explicit first suggestion without being declared uncertain.
      */
-    public static final @DeviceConfigKey String
-            KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_MILLIS =
-            "ltpz_init_timeout_millis";
+    public static final @DeviceConfigKey String KEY_LTZP_INITIALIZATION_TIMEOUT_MILLIS =
+            "ltzp_init_timeout_millis";
 
     /**
      * The key for the extra time added to {@link
-     * #KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_MILLIS} by the location time zone
+     * #KEY_LTZP_INITIALIZATION_TIMEOUT_MILLIS} by the location time zone
      * manager before the location time zone provider will actually be declared uncertain.
      */
-    public static final @DeviceConfigKey String
-            KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_FUZZ_MILLIS =
-            "ltpz_init_timeout_fuzz_millis";
+    public static final @DeviceConfigKey String KEY_LTZP_INITIALIZATION_TIMEOUT_FUZZ_MILLIS =
+            "ltzp_init_timeout_fuzz_millis";
+
+    /** The key for the setting that controls rate limiting of provider events. */
+    public static final @DeviceConfigKey String KEY_LTZP_EVENT_FILTERING_AGE_THRESHOLD_MILLIS =
+            "ltzp_event_filtering_age_threshold_millis";
 
     /**
      * The key for the server flag that can override location time zone detection being enabled for
diff --git a/services/core/java/com/android/server/timezone/OWNERS b/services/core/java/com/android/server/timezone/OWNERS
index 8f80897..2d36574 100644
--- a/services/core/java/com/android/server/timezone/OWNERS
+++ b/services/core/java/com/android/server/timezone/OWNERS
@@ -1,3 +1,2 @@
-# Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+# Bug component: 24949
+include platform/libcore:/OWNERS
diff --git a/services/core/java/com/android/server/timezonedetector/OWNERS b/services/core/java/com/android/server/timezonedetector/OWNERS
index 8f80897..0293242 100644
--- a/services/core/java/com/android/server/timezonedetector/OWNERS
+++ b/services/core/java/com/android/server/timezonedetector/OWNERS
@@ -1,3 +1,7 @@
 # Bug component: 847766
+# This is the main list for platform time / time zone detection maintainers, for this dir and
+# ultimately referenced by other OWNERS files for components maintained by the same team.
+nfuller@google.com
+jmorace@google.com
 mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+narayan@google.com
diff --git a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessor.java b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessor.java
index 5828130..20f4fa1 100644
--- a/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessor.java
+++ b/services/core/java/com/android/server/timezonedetector/ServiceConfigAccessor.java
@@ -72,17 +72,18 @@
                     ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_FEATURE_SUPPORTED,
                     ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_DEFAULT,
                     ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_SETTING_ENABLED_OVERRIDE,
-                    ServerFlags.KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE,
-                    ServerFlags.KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE,
-                    ServerFlags.KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_MILLIS,
-                    ServerFlags.KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_FUZZ_MILLIS,
+                    ServerFlags.KEY_PRIMARY_LTZP_MODE_OVERRIDE,
+                    ServerFlags.KEY_SECONDARY_LTZP_MODE_OVERRIDE,
+                    ServerFlags.KEY_LTZP_INITIALIZATION_TIMEOUT_MILLIS,
+                    ServerFlags.KEY_LTZP_INITIALIZATION_TIMEOUT_FUZZ_MILLIS,
+                    ServerFlags.KEY_LTZP_EVENT_FILTERING_AGE_THRESHOLD_MILLIS,
                     ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_UNCERTAINTY_DELAY_MILLIS
             }));
 
-    private static final Duration DEFAULT_PROVIDER_INITIALIZATION_TIMEOUT = Duration.ofMinutes(5);
-    private static final Duration DEFAULT_PROVIDER_INITIALIZATION_TIMEOUT_FUZZ =
-            Duration.ofMinutes(1);
-    private static final Duration DEFAULT_PROVIDER_UNCERTAINTY_DELAY = Duration.ofMinutes(5);
+    private static final Duration DEFAULT_LTZP_INITIALIZATION_TIMEOUT = Duration.ofMinutes(5);
+    private static final Duration DEFAULT_LTZP_INITIALIZATION_TIMEOUT_FUZZ = Duration.ofMinutes(1);
+    private static final Duration DEFAULT_LTZP_UNCERTAINTY_DELAY = Duration.ofMinutes(5);
+    private static final Duration DEFAULT_LTZP_EVENT_FILTER_AGE_THRESHOLD = Duration.ofMinutes(1);
 
     private static final Object SLOCK = new Object();
 
@@ -326,8 +327,7 @@
             // In test mode: use the test setting value.
             return mTestPrimaryLocationTimeZoneProviderMode;
         }
-        return mServerFlags.getOptionalString(
-                ServerFlags.KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE)
+        return mServerFlags.getOptionalString(ServerFlags.KEY_PRIMARY_LTZP_MODE_OVERRIDE)
                 .orElse(getPrimaryLocationTimeZoneProviderModeFromConfig());
     }
 
@@ -346,8 +346,7 @@
             // In test mode: use the test setting value.
             return mTestSecondaryLocationTimeZoneProviderMode;
         }
-        return mServerFlags.getOptionalString(
-                ServerFlags.KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE)
+        return mServerFlags.getOptionalString(ServerFlags.KEY_SECONDARY_LTZP_MODE_OVERRIDE)
                 .orElse(getSecondaryLocationTimeZoneProviderModeFromConfig());
     }
 
@@ -385,8 +384,8 @@
     @NonNull
     public Duration getLocationTimeZoneProviderInitializationTimeout() {
         return mServerFlags.getDurationFromMillis(
-                ServerFlags.KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_MILLIS,
-                DEFAULT_PROVIDER_INITIALIZATION_TIMEOUT);
+                ServerFlags.KEY_LTZP_INITIALIZATION_TIMEOUT_MILLIS,
+                DEFAULT_LTZP_INITIALIZATION_TIMEOUT);
     }
 
     /**
@@ -396,8 +395,8 @@
     @NonNull
     public Duration getLocationTimeZoneProviderInitializationTimeoutFuzz() {
         return mServerFlags.getDurationFromMillis(
-                ServerFlags.KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_FUZZ_MILLIS,
-                DEFAULT_PROVIDER_INITIALIZATION_TIMEOUT_FUZZ);
+                ServerFlags.KEY_LTZP_INITIALIZATION_TIMEOUT_FUZZ_MILLIS,
+                DEFAULT_LTZP_INITIALIZATION_TIMEOUT_FUZZ);
     }
 
     /**
@@ -408,7 +407,18 @@
     public Duration getLocationTimeZoneUncertaintyDelay() {
         return mServerFlags.getDurationFromMillis(
                 ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_UNCERTAINTY_DELAY_MILLIS,
-                DEFAULT_PROVIDER_UNCERTAINTY_DELAY);
+                DEFAULT_LTZP_UNCERTAINTY_DELAY);
+    }
+
+    /**
+     * Returns the time between equivalent events before the provider process will send the event
+     * to the system server.
+     */
+    @NonNull
+    public Duration getLocationTimeZoneProviderEventFilteringAgeThreshold() {
+        return mServerFlags.getDurationFromMillis(
+                ServerFlags.KEY_LTZP_EVENT_FILTERING_AGE_THRESHOLD_MILLIS,
+                DEFAULT_LTZP_EVENT_FILTER_AGE_THRESHOLD);
     }
 
     /** Clears all in-memory test config. */
diff --git a/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java
index 9d340e4..a1de294 100644
--- a/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java
+++ b/services/core/java/com/android/server/timezonedetector/location/BinderLocationTimeZoneProvider.java
@@ -26,6 +26,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.service.timezone.TimeZoneProviderEvent;
 import android.util.IndentingPrintWriter;
 
 import java.time.Duration;
@@ -111,11 +112,12 @@
     }
 
     @Override
-    void onStartUpdates(@NonNull Duration initializationTimeout) {
+    void onStartUpdates(@NonNull Duration initializationTimeout,
+            @NonNull Duration eventFilteringAgeThreshold) {
         // Set a request on the proxy - it will be sent immediately if the service is bound,
         // or will be sent as soon as the service becomes bound.
-        TimeZoneProviderRequest request =
-                TimeZoneProviderRequest.createStartUpdatesRequest(initializationTimeout);
+        TimeZoneProviderRequest request = TimeZoneProviderRequest.createStartUpdatesRequest(
+                initializationTimeout, eventFilteringAgeThreshold);
         mProxy.setRequest(request);
     }
 
diff --git a/services/core/java/com/android/server/timezonedetector/location/ControllerEnvironmentImpl.java b/services/core/java/com/android/server/timezonedetector/location/ControllerEnvironmentImpl.java
index 98e984d..551a059 100644
--- a/services/core/java/com/android/server/timezonedetector/location/ControllerEnvironmentImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/location/ControllerEnvironmentImpl.java
@@ -78,4 +78,9 @@
     Duration getUncertaintyDelay() {
         return mServiceConfigAccessor.getLocationTimeZoneUncertaintyDelay();
     }
+
+    @Override
+    Duration getProviderEventFilteringAgeThreshold() {
+        return mServiceConfigAccessor.getLocationTimeZoneProviderEventFilteringAgeThreshold();
+    }
 }
diff --git a/services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java b/services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java
index 76ef958..1d58cea1 100644
--- a/services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java
+++ b/services/core/java/com/android/server/timezonedetector/location/ControllerImpl.java
@@ -16,6 +16,10 @@
 
 package com.android.server.timezonedetector.location;
 
+import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE;
+import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION;
+import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN;
+
 import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.debugLog;
 import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.warnLog;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState;
@@ -25,14 +29,12 @@
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED;
-import static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE;
-import static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION;
-import static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN;
 
 import android.annotation.DurationMillisLong;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.service.timezone.TimeZoneProviderEvent;
 import android.util.IndentingPrintWriter;
 
 import com.android.internal.annotations.GuardedBy;
@@ -281,6 +283,7 @@
         }
     }
 
+    @GuardedBy("mSharedLock")
     private void tryStartProvider(@NonNull LocationTimeZoneProvider provider,
             @NonNull ConfigurationInternal configuration) {
         ProviderState providerState = provider.getCurrentState();
@@ -289,7 +292,8 @@
                 debugLog("Enabling " + provider);
                 provider.startUpdates(configuration,
                         mEnvironment.getProviderInitializationTimeout(),
-                        mEnvironment.getProviderInitializationTimeoutFuzz());
+                        mEnvironment.getProviderInitializationTimeoutFuzz(),
+                        mEnvironment.getProviderEventFilteringAgeThreshold());
                 break;
             }
             case PROVIDER_STATE_STARTED_INITIALIZING:
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java
index 8dbc520..c5c59ce 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerService.java
@@ -28,6 +28,7 @@
 import android.os.Handler;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
+import android.service.timezone.TimeZoneProviderEvent;
 import android.service.timezone.TimeZoneProviderService;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java
index 3488956..6c9e174 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneManagerShellCommand.java
@@ -26,10 +26,11 @@
 import static android.provider.DeviceConfig.NAMESPACE_SYSTEM_TIME;
 
 import static com.android.server.timedetector.ServerFlags.KEY_LOCATION_TIME_ZONE_DETECTION_UNCERTAINTY_DELAY_MILLIS;
-import static com.android.server.timedetector.ServerFlags.KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_FUZZ_MILLIS;
-import static com.android.server.timedetector.ServerFlags.KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_MILLIS;
-import static com.android.server.timedetector.ServerFlags.KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE;
-import static com.android.server.timedetector.ServerFlags.KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE;
+import static com.android.server.timedetector.ServerFlags.KEY_LTZP_EVENT_FILTERING_AGE_THRESHOLD_MILLIS;
+import static com.android.server.timedetector.ServerFlags.KEY_LTZP_INITIALIZATION_TIMEOUT_FUZZ_MILLIS;
+import static com.android.server.timedetector.ServerFlags.KEY_LTZP_INITIALIZATION_TIMEOUT_MILLIS;
+import static com.android.server.timedetector.ServerFlags.KEY_PRIMARY_LTZP_MODE_OVERRIDE;
+import static com.android.server.timedetector.ServerFlags.KEY_SECONDARY_LTZP_MODE_OVERRIDE;
 import static com.android.server.timezonedetector.ServiceConfigAccessor.PROVIDER_MODE_DISABLED;
 import static com.android.server.timezonedetector.ServiceConfigAccessor.PROVIDER_MODE_ENABLED;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED;
@@ -126,20 +127,23 @@
         pw.println();
         pw.printf("This service is also affected by the following device_config flags in the"
                 + " %s namespace:\n", NAMESPACE_SYSTEM_TIME);
-        pw.printf("  %s\n", KEY_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE);
+        pw.printf("  %s\n", KEY_PRIMARY_LTZP_MODE_OVERRIDE);
         pw.printf("    Overrides the mode of the primary provider. Values=%s|%s\n",
                 PROVIDER_MODE_DISABLED, PROVIDER_MODE_ENABLED);
-        pw.printf("  %s\n", KEY_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_MODE_OVERRIDE);
+        pw.printf("  %s\n", KEY_SECONDARY_LTZP_MODE_OVERRIDE);
         pw.printf("    Overrides the mode of the secondary provider. Values=%s|%s\n",
                 PROVIDER_MODE_DISABLED, PROVIDER_MODE_ENABLED);
         pw.printf("  %s\n", KEY_LOCATION_TIME_ZONE_DETECTION_UNCERTAINTY_DELAY_MILLIS);
         pw.printf("    Sets the amount of time the service waits when uncertain before making an"
                 + " 'uncertain' suggestion to the time zone detector.\n");
-        pw.printf("  %s\n", KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_MILLIS);
+        pw.printf("  %s\n", KEY_LTZP_INITIALIZATION_TIMEOUT_MILLIS);
         pw.printf("    Sets the initialization time passed to the providers.\n");
-        pw.printf("  %s\n", KEY_LOCATION_TIME_ZONE_PROVIDER_INITIALIZATION_TIMEOUT_FUZZ_MILLIS);
+        pw.printf("  %s\n", KEY_LTZP_INITIALIZATION_TIMEOUT_FUZZ_MILLIS);
         pw.printf("    Sets the amount of extra time added to the providers' initialization time."
                 + "\n");
+        pw.printf("  %s\n", KEY_LTZP_EVENT_FILTERING_AGE_THRESHOLD_MILLIS);
+        pw.printf("    Sets the amount of time that must pass between equivalent LTZP events before"
+                + " they will be reported to the system server.\n");
         pw.println();
         pw.printf("Typically, use '%s' to stop the service before setting individual"
                 + " flags and '%s' after to restart it.\n",
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java
index ee85ee4..90540b0 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProvider.java
@@ -16,6 +16,10 @@
 
 package com.android.server.timezonedetector.location;
 
+import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE;
+import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION;
+import static android.service.timezone.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN;
+
 import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.debugLog;
 import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.warnLog;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_DESTROYED;
@@ -24,9 +28,6 @@
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_INITIALIZING;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STARTED_UNCERTAIN;
 import static com.android.server.timezonedetector.location.LocationTimeZoneProvider.ProviderState.PROVIDER_STATE_STOPPED;
-import static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_PERMANENT_FAILURE;
-import static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_SUGGESTION;
-import static com.android.server.timezonedetector.location.TimeZoneProviderEvent.EVENT_TYPE_UNCERTAIN;
 
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.IntDef;
@@ -34,6 +35,7 @@
 import android.annotation.Nullable;
 import android.os.Handler;
 import android.os.SystemClock;
+import android.service.timezone.TimeZoneProviderEvent;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -527,7 +529,8 @@
      * called using the handler thread from the {@link ThreadingDomain}.
      */
     final void startUpdates(@NonNull ConfigurationInternal currentUserConfiguration,
-            @NonNull Duration initializationTimeout, @NonNull Duration initializationTimeoutFuzz) {
+            @NonNull Duration initializationTimeout, @NonNull Duration initializationTimeoutFuzz,
+            @NonNull Duration eventFilteringAgeThreshold) {
         mThreadingDomain.assertCurrentThread();
 
         synchronized (mSharedLock) {
@@ -542,7 +545,7 @@
             mInitializationTimeoutQueue.runDelayed(
                     this::handleInitializationTimeout, delay.toMillis());
 
-            onStartUpdates(initializationTimeout);
+            onStartUpdates(initializationTimeout, eventFilteringAgeThreshold);
         }
     }
 
@@ -568,9 +571,11 @@
      * Implemented by subclasses to do work during {@link #startUpdates}. This is where the logic
      * to start the real provider should be implemented.
      *
-     * @param initializationTimeout the initialization timeout to pass to the real provider
+     * @param initializationTimeout the initialization timeout to pass to the provider
+     * @param eventFilteringAgeThreshold the event filtering age threshold to pass to the provider
      */
-    abstract void onStartUpdates(@NonNull Duration initializationTimeout);
+    abstract void onStartUpdates(@NonNull Duration initializationTimeout,
+            @NonNull Duration eventFilteringAgeThreshold);
 
     /**
      * Stops the provider. It is an error to call this method except when the {@link
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java
index b4aff3e..d2fd9a9 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderController.java
@@ -130,6 +130,12 @@
         abstract Duration getProviderInitializationTimeoutFuzz();
 
         /**
+         * Returns the value passed to LocationTimeZoneProviders to control rate limiting of
+         * equivalent events.
+         */
+        abstract Duration getProviderEventFilteringAgeThreshold();
+
+        /**
          * Returns the delay allowed after receiving uncertainty from a provider before it should be
          * passed on.
          */
diff --git a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderProxy.java
index 7b1a77c..f187db1 100644
--- a/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderProxy.java
+++ b/services/core/java/com/android/server/timezonedetector/location/LocationTimeZoneProviderProxy.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.os.Handler;
+import android.service.timezone.TimeZoneProviderEvent;
 import android.util.IndentingPrintWriter;
 
 import com.android.internal.annotations.GuardedBy;
diff --git a/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java
index 4ef819f..9cb1813 100644
--- a/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java
+++ b/services/core/java/com/android/server/timezonedetector/location/NullLocationTimeZoneProviderProxy.java
@@ -19,6 +19,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.os.SystemClock;
+import android.service.timezone.TimeZoneProviderEvent;
 import android.util.IndentingPrintWriter;
 
 /**
@@ -58,7 +60,7 @@
     void setRequest(@NonNull TimeZoneProviderRequest request) {
         if (request.sendUpdates()) {
             TimeZoneProviderEvent event = TimeZoneProviderEvent.createPermanentFailureEvent(
-                    "Provider is disabled");
+                    SystemClock.elapsedRealtime(), "Provider is disabled");
             handleTimeZoneProviderEvent(event);
         }
     }
diff --git a/services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java b/services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java
index fcac3e8..f54fe48 100644
--- a/services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java
+++ b/services/core/java/com/android/server/timezonedetector/location/RealLocationTimeZoneProviderProxy.java
@@ -26,7 +26,7 @@
 import android.os.IBinder;
 import android.service.timezone.ITimeZoneProvider;
 import android.service.timezone.ITimeZoneProviderManager;
-import android.service.timezone.TimeZoneProviderSuggestion;
+import android.service.timezone.TimeZoneProviderEvent;
 import android.util.IndentingPrintWriter;
 
 import com.android.internal.annotations.GuardedBy;
@@ -152,7 +152,9 @@
         mServiceWatcher.runOnBinder(binder -> {
             ITimeZoneProvider service = ITimeZoneProvider.Stub.asInterface(binder);
             if (request.sendUpdates()) {
-                service.startUpdates(managerProxy, request.getInitializationTimeout().toMillis());
+                service.startUpdates(managerProxy,
+                        request.getInitializationTimeout().toMillis(),
+                        request.getEventFilteringAgeThreshold().toMillis());
             } else {
                 service.stopUpdates();
             }
@@ -177,25 +179,7 @@
 
         // executed on binder thread
         @Override
-        public void onTimeZoneProviderSuggestion(TimeZoneProviderSuggestion suggestion) {
-            onTimeZoneProviderEvent(TimeZoneProviderEvent.createSuggestionEvent(suggestion));
-        }
-
-        // executed on binder thread
-        @Override
-        public void onTimeZoneProviderUncertain() {
-            onTimeZoneProviderEvent(TimeZoneProviderEvent.createUncertainEvent());
-
-        }
-
-        // executed on binder thread
-        @Override
-        public void onTimeZoneProviderPermanentFailure(String failureReason) {
-            onTimeZoneProviderEvent(
-                    TimeZoneProviderEvent.createPermanentFailureEvent(failureReason));
-        }
-
-        private void onTimeZoneProviderEvent(TimeZoneProviderEvent event) {
+        public void onTimeZoneProviderEvent(TimeZoneProviderEvent event) {
             synchronized (mSharedLock) {
                 if (mManagerProxy != this) {
                     // Ignore incoming calls if this instance is no longer the current
diff --git a/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderEvent.java b/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderEvent.java
deleted file mode 100644
index 7648795..0000000
--- a/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderEvent.java
+++ /dev/null
@@ -1,148 +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.timezonedetector.location;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.service.timezone.TimeZoneProviderService;
-import android.service.timezone.TimeZoneProviderSuggestion;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import java.util.Objects;
-
-/**
- * An event from a {@link TimeZoneProviderService}.
- */
-final class TimeZoneProviderEvent {
-
-    @IntDef(prefix = "EVENT_TYPE_",
-            value = { EVENT_TYPE_PERMANENT_FAILURE, EVENT_TYPE_SUGGESTION, EVENT_TYPE_UNCERTAIN })
-    @Retention(RetentionPolicy.SOURCE)
-    @Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER })
-    public @interface EventType {}
-
-    /**
-     * The provider failed permanently. See {@link
-     * TimeZoneProviderService#reportPermanentFailure(Throwable)}
-     */
-    public static final @EventType int EVENT_TYPE_PERMANENT_FAILURE = 1;
-
-    /**
-     * The provider made a suggestion. See {@link
-     * TimeZoneProviderService#reportSuggestion(TimeZoneProviderSuggestion)}
-     */
-    public static final @EventType int EVENT_TYPE_SUGGESTION = 2;
-
-    /**
-     * The provider was uncertain about the time zone. See {@link
-     * TimeZoneProviderService#reportUncertain()}
-     */
-    public static final @EventType int EVENT_TYPE_UNCERTAIN = 3;
-
-    private static final TimeZoneProviderEvent UNCERTAIN_EVENT =
-            new TimeZoneProviderEvent(EVENT_TYPE_UNCERTAIN, null, null);
-
-    private final @EventType int mType;
-
-    @Nullable
-    private final TimeZoneProviderSuggestion mSuggestion;
-
-    @Nullable
-    private final String mFailureCause;
-
-    private TimeZoneProviderEvent(@EventType int type,
-            @Nullable TimeZoneProviderSuggestion suggestion,
-            @Nullable String failureCause) {
-        mType = type;
-        mSuggestion = suggestion;
-        mFailureCause = failureCause;
-    }
-
-    /** Returns a event of type {@link #EVENT_TYPE_SUGGESTION}. */
-    public static TimeZoneProviderEvent createSuggestionEvent(
-            @NonNull TimeZoneProviderSuggestion suggestion) {
-        return new TimeZoneProviderEvent(EVENT_TYPE_SUGGESTION,
-                Objects.requireNonNull(suggestion), null);
-    }
-
-    /** Returns a event of type {@link #EVENT_TYPE_UNCERTAIN}. */
-    public static TimeZoneProviderEvent createUncertainEvent() {
-        return UNCERTAIN_EVENT;
-    }
-
-    /** Returns a event of type {@link #EVENT_TYPE_PERMANENT_FAILURE}. */
-    public static TimeZoneProviderEvent createPermanentFailureEvent(@NonNull String cause) {
-        return new TimeZoneProviderEvent(EVENT_TYPE_PERMANENT_FAILURE, null,
-                Objects.requireNonNull(cause));
-    }
-
-    /**
-     * Returns the event type.
-     */
-    public @EventType int getType() {
-        return mType;
-    }
-
-    /**
-     * Returns the suggestion. Populated when {@link #getType()} is {@link #EVENT_TYPE_SUGGESTION}.
-     */
-    @Nullable
-    public TimeZoneProviderSuggestion getSuggestion() {
-        return mSuggestion;
-    }
-
-    /**
-     * Returns the failure cauese. Populated when {@link #getType()} is {@link
-     * #EVENT_TYPE_PERMANENT_FAILURE}.
-     */
-    @Nullable
-    public String getFailureCause() {
-        return mFailureCause;
-    }
-
-    @Override
-    public String toString() {
-        return "TimeZoneProviderEvent{"
-                + "mType=" + mType
-                + ", mSuggestion=" + mSuggestion
-                + ", mFailureCause=" + mFailureCause
-                + '}';
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-        TimeZoneProviderEvent that = (TimeZoneProviderEvent) o;
-        return mType == that.mType
-                && Objects.equals(mSuggestion, that.mSuggestion)
-                && Objects.equals(mFailureCause, that.mFailureCause);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(mType, mSuggestion, mFailureCause);
-    }
-}
diff --git a/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderEventPreProcessor.java b/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderEventPreProcessor.java
index 951e9d0..dda7291 100644
--- a/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderEventPreProcessor.java
+++ b/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderEventPreProcessor.java
@@ -17,6 +17,7 @@
 package com.android.server.timezonedetector.location;
 
 import android.annotation.NonNull;
+import android.service.timezone.TimeZoneProviderEvent;
 
 /**
  * Used by {@link LocationTimeZoneProvider} to ensure that all time zone IDs are understood by the
diff --git a/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderRequest.java b/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderRequest.java
index e8386bc..a9d94dd 100644
--- a/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderRequest.java
+++ b/services/core/java/com/android/server/timezonedetector/location/TimeZoneProviderRequest.java
@@ -31,23 +31,32 @@
     private static final TimeZoneProviderRequest STOP_UPDATES =
             new TimeZoneProviderRequest(
                     false /* sendUpdates */,
-                    null /* initializationTimeout */);
+                    null /* initializationTimeout */,
+                    null /* eventFilteringAgeThreshold */);
 
     private final boolean mSendUpdates;
 
     @Nullable
     private final Duration mInitializationTimeout;
 
+    @Nullable
+    private final Duration mEventFilteringAgeThreshold;
+
     private TimeZoneProviderRequest(
-            boolean sendUpdates, @Nullable Duration initializationTimeout) {
+            boolean sendUpdates, @Nullable Duration initializationTimeout,
+            @Nullable Duration eventFilteringAgeThreshold) {
         mSendUpdates = sendUpdates;
         mInitializationTimeout = initializationTimeout;
+        mEventFilteringAgeThreshold = eventFilteringAgeThreshold;
     }
 
     /** Creates a request to start updates with the specified timeout. */
     public static TimeZoneProviderRequest createStartUpdatesRequest(
-            @NonNull Duration initializationTimeout) {
-        return new TimeZoneProviderRequest(true, Objects.requireNonNull(initializationTimeout));
+            @NonNull Duration initializationTimeout,
+            @NonNull Duration eventFilteringAgeThreshold) {
+        return new TimeZoneProviderRequest(true,
+                Objects.requireNonNull(initializationTimeout),
+                Objects.requireNonNull(eventFilteringAgeThreshold));
     }
 
     /** Creates a request to stop updates. */
@@ -74,6 +83,17 @@
         return mInitializationTimeout;
     }
 
+    /**
+     * Returns the threshold the remote process is to use to filter equivalent events. Only valid
+     * when {@link #sendUpdates()} is {@code true}.
+     *
+     * <p>Guaranteed to be set when {@link #sendUpdates()} returns {@code true}.
+     */
+    @NonNull
+    public Duration getEventFilteringAgeThreshold() {
+        return mEventFilteringAgeThreshold;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -82,15 +102,15 @@
         if (o == null || getClass() != o.getClass()) {
             return false;
         }
-        TimeZoneProviderRequest
-                that = (TimeZoneProviderRequest) o;
+        TimeZoneProviderRequest that = (TimeZoneProviderRequest) o;
         return mSendUpdates == that.mSendUpdates
-                && mInitializationTimeout == that.mInitializationTimeout;
+                && Objects.equals(mInitializationTimeout, that.mInitializationTimeout)
+                && Objects.equals(mEventFilteringAgeThreshold, that.mEventFilteringAgeThreshold);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mSendUpdates, mInitializationTimeout);
+        return Objects.hash(mSendUpdates, mInitializationTimeout, mEventFilteringAgeThreshold);
     }
 
     @Override
@@ -98,6 +118,7 @@
         return "TimeZoneProviderRequest{"
                 + "mSendUpdates=" + mSendUpdates
                 + ", mInitializationTimeout=" + mInitializationTimeout
+                + ", mEventFilteringAgeThreshold=" + mEventFilteringAgeThreshold
                 + "}";
     }
 }
diff --git a/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java b/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
index 0f4367d..ff0529f 100644
--- a/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
+++ b/services/core/java/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessor.java
@@ -19,6 +19,7 @@
 import static com.android.server.timezonedetector.location.LocationTimeZoneManagerService.infoLog;
 
 import android.annotation.NonNull;
+import android.service.timezone.TimeZoneProviderEvent;
 
 import com.android.i18n.timezone.ZoneInfoDb;
 
@@ -52,7 +53,7 @@
         // enables immediate failover to a secondary provider, one that might provide valid IDs for
         // the same location, which should provide better behavior than just ignoring the event.
         if (hasInvalidZones(event)) {
-            return TimeZoneProviderEvent.createUncertainEvent();
+            return TimeZoneProviderEvent.createUncertainEvent(event.getCreationElapsedMillis());
         }
 
         return event;
diff --git a/services/core/java/com/android/server/trust/TrustAgentWrapper.java b/services/core/java/com/android/server/trust/TrustAgentWrapper.java
index 5f6fff1..59f8e54 100644
--- a/services/core/java/com/android/server/trust/TrustAgentWrapper.java
+++ b/services/core/java/com/android/server/trust/TrustAgentWrapper.java
@@ -425,7 +425,8 @@
         mBound = context.bindServiceAsUser(intent, mConnection,
                 Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, user);
         if (mBound) {
-            mContext.registerReceiver(mBroadcastReceiver, alarmFilter, PERMISSION, null);
+            mContext.registerReceiver(mBroadcastReceiver, alarmFilter, PERMISSION, null,
+                    Context.RECEIVER_EXPORTED);
         } else {
             Log.e(TAG, "Can't bind to TrustAgent " + mName.flattenToShortString());
         }
diff --git a/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java
new file mode 100644
index 0000000..620be3a
--- /dev/null
+++ b/services/core/java/com/android/server/tv/interactive/TvIAppManagerService.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.tv.interactive;
+
+import android.content.Context;
+import android.media.tv.interactive.ITvIAppManager;
+import android.media.tv.interactive.ITvIAppSession;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import com.android.server.SystemService;
+import com.android.server.utils.Slogf;
+
+/**
+ * This class provides a system service that manages interactive TV applications.
+ */
+public class TvIAppManagerService extends SystemService {
+    private static final boolean DEBUG = false;
+    private static final String TAG = "TvIAppManagerService";
+
+    /**
+     * Initializes the system service.
+     * <p>
+     * Subclasses must define a single argument constructor that accepts the context
+     * and passes it to super.
+     * </p>
+     *
+     * @param context The system server context.
+     */
+    public TvIAppManagerService(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void onStart() {
+        if (DEBUG) {
+            Slogf.d(TAG, "onStart");
+        }
+        // TODO: make service name a constant in Context
+        publishBinderService("tv_interactive_app", new BinderService());
+    }
+
+    private SessionState getSessionState(IBinder sessionToken) {
+        // TODO: implement user state and get session from it.
+        return null;
+    }
+
+    private final class BinderService extends ITvIAppManager.Stub {
+        @Override
+        public void startIApp(IBinder sessionToken, int userId) {
+            if (DEBUG) {
+                Slogf.d(TAG, "BinderService#start(userId=%d)", userId);
+            }
+            try {
+                SessionState sessionState = getSessionState(sessionToken);
+                if (sessionState != null && sessionState.mSession != null) {
+                    sessionState.mSession.startIApp();
+                }
+            } catch (RemoteException e) {
+                Slogf.e(TAG, "error in start", e);
+            }
+        }
+    }
+
+    private final class SessionState implements IBinder.DeathRecipient {
+        private final IBinder mSessionToken;
+        private ITvIAppSession mSession;
+
+        private SessionState(IBinder sessionToken) {
+            mSessionToken = sessionToken;
+        }
+
+        @Override
+        public void binderDied() {
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/utils/AlarmQueue.java b/services/core/java/com/android/server/utils/AlarmQueue.java
new file mode 100644
index 0000000..3f4def6
--- /dev/null
+++ b/services/core/java/com/android/server/utils/AlarmQueue.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.utils;
+
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
+import android.annotation.ElapsedRealtimeLong;
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.AlarmManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.PriorityQueue;
+import java.util.function.Predicate;
+
+/**
+ * An {@link AlarmManager.OnAlarmListener} that will queue up all pending alarms and only
+ * schedule one alarm for the earliest alarm. Since {@link AlarmManager} has a maximum limit on the
+ * number of alarms that can be set at one time, this allows clients to maintain alarm times for
+ * various keys without risking hitting the AlarmManager alarm limit. Only one alarm time will be
+ * kept for each key {@code K}.
+ *
+ * @param <K> Any class that will be used as the key. Must have a proper equals() implementation.
+ * @hide
+ */
+public abstract class AlarmQueue<K> implements AlarmManager.OnAlarmListener {
+    private static final String TAG = AlarmQueue.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    private static final long NOT_SCHEDULED = -1;
+
+    /**
+     * Internal priority queue for each key's alarm, ordered by the time the alarm should go off.
+     * The pair is the key and its associated alarm time (in the elapsed realtime timebase).
+     */
+    private static class AlarmPriorityQueue<Q> extends PriorityQueue<Pair<Q, Long>> {
+        AlarmPriorityQueue() {
+            super(1, (o1, o2) -> (int) (o1.second - o2.second));
+        }
+
+        /**
+         * Remove any instances of the key from the queue.
+         *
+         * @return true if an instance was removed, false otherwise.
+         */
+        public boolean removeKey(@NonNull Q key) {
+            boolean removed = false;
+            Pair[] alarms = toArray(new Pair[size()]);
+            for (int i = alarms.length - 1; i >= 0; --i) {
+                if (key.equals(alarms[i].first)) {
+                    remove(alarms[i]);
+                    removed = true;
+                }
+            }
+            return removed;
+        }
+    }
+
+    @VisibleForTesting
+    static class Injector {
+        long getElapsedRealtime() {
+            return SystemClock.elapsedRealtime();
+        }
+    }
+
+    /** Runnable used to schedule an alarm with AlarmManager. NEVER run this with the lock held. */
+    private final Runnable mScheduleAlarmRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mHandler.removeCallbacks(this);
+
+            final AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
+            if (alarmManager == null) {
+                // The system isn't fully booted. Clients of this class may not have
+                // direct access to (be notified when) the system is ready, so retry
+                // setting the alarm after some delay. Leave enough time so that we don't cause
+                // any unneeded startup delay.
+                mHandler.postDelayed(this, 30_000);
+                return;
+            }
+            final long nextTriggerTimeElapsed;
+            final long minTimeBetweenAlarmsMs;
+            synchronized (mLock) {
+                if (mTriggerTimeElapsed == NOT_SCHEDULED) {
+                    return;
+                }
+                nextTriggerTimeElapsed = mTriggerTimeElapsed;
+                minTimeBetweenAlarmsMs = mMinTimeBetweenAlarmsMs;
+            }
+            // Never call out to AlarmManager with the lock held. This could sit below AM.
+            if (mExactAlarm) {
+                alarmManager.setExact(AlarmManager.ELAPSED_REALTIME,
+                        nextTriggerTimeElapsed, mAlarmTag, AlarmQueue.this, mHandler);
+            } else {
+                alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME,
+                        nextTriggerTimeElapsed, minTimeBetweenAlarmsMs / 2,
+                        mAlarmTag, AlarmQueue.this, mHandler);
+            }
+        }
+    };
+
+    private final Object mLock = new Object();
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final Injector mInjector;
+
+    @GuardedBy("mLock")
+    private final AlarmPriorityQueue<K> mAlarmPriorityQueue = new AlarmPriorityQueue<>();
+    private final String mAlarmTag;
+    private final String mDumpTitle;
+    /** Whether to use an exact alarm or an inexact alarm. */
+    private final boolean mExactAlarm;
+    /** The minimum amount of time between check alarms. */
+    @GuardedBy("mLock")
+    private long mMinTimeBetweenAlarmsMs;
+    /** The next time the alarm is set to go off, in the elapsed realtime timebase. */
+    @GuardedBy("mLock")
+    @ElapsedRealtimeLong
+    private long mTriggerTimeElapsed = NOT_SCHEDULED;
+
+    /**
+     * @param alarmTag               The tag to use when scheduling the alarm with AlarmManager.
+     * @param dumpTitle              The title to use when dumping state.
+     * @param exactAlarm             Whether or not to use an exact alarm. If false, this will use
+     *                               an inexact window alarm.
+     * @param minTimeBetweenAlarmsMs The minimum amount of time that should be between alarms. If
+     *                               one alarm will go off too soon after another, the second one
+     *                               will be delayed to meet this minimum time.
+     */
+    public AlarmQueue(@NonNull Context context, @NonNull Looper looper, @NonNull String alarmTag,
+            @NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs) {
+        this(context, looper, alarmTag, dumpTitle, exactAlarm, minTimeBetweenAlarmsMs,
+                new Injector());
+    }
+
+    @VisibleForTesting
+    AlarmQueue(@NonNull Context context, @NonNull Looper looper, @NonNull String alarmTag,
+            @NonNull String dumpTitle, boolean exactAlarm, long minTimeBetweenAlarmsMs,
+            @NonNull Injector injector) {
+        mContext = context;
+        mAlarmTag = alarmTag;
+        mDumpTitle = dumpTitle.trim();
+        mExactAlarm = exactAlarm;
+        mHandler = new Handler(looper);
+        mInjector = injector;
+        if (minTimeBetweenAlarmsMs < 0) {
+            throw new IllegalArgumentException("min time between alarms must be non-negative");
+        }
+        mMinTimeBetweenAlarmsMs = minTimeBetweenAlarmsMs;
+    }
+
+    /**
+     * Add an alarm for the specified key that should go off at the provided time
+     * (in the elapsed realtime timebase). This will also remove any existing alarm for the key.
+     */
+    public void addAlarm(K key, @ElapsedRealtimeLong long alarmTimeElapsed) {
+        synchronized (mLock) {
+            final boolean removed = mAlarmPriorityQueue.removeKey(key);
+            mAlarmPriorityQueue.offer(new Pair<>(key, alarmTimeElapsed));
+            if (mTriggerTimeElapsed == NOT_SCHEDULED || removed
+                    || alarmTimeElapsed < mTriggerTimeElapsed) {
+                setNextAlarmLocked();
+            }
+        }
+    }
+
+    /**
+     * Get the current minimum time between alarms.
+     *
+     * @see #setMinTimeBetweenAlarmsMs(long)
+     */
+    public long getMinTimeBetweenAlarmsMs() {
+        synchronized (mLock) {
+            return mMinTimeBetweenAlarmsMs;
+        }
+    }
+
+    /** Remove the alarm for this specific key. */
+    public void removeAlarmForKey(K key) {
+        synchronized (mLock) {
+            if (mAlarmPriorityQueue.removeKey(key)) {
+                setNextAlarmLocked();
+            }
+        }
+    }
+
+    /** Remove all alarms tied to the specified user. */
+    public void removeAlarmsForUserId(@UserIdInt int userId) {
+        boolean removed = false;
+        synchronized (mLock) {
+            Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
+            for (int i = alarms.length - 1; i >= 0; --i) {
+                final K key = (K) alarms[i].first;
+                if (isForUser(key, userId)) {
+                    mAlarmPriorityQueue.remove(alarms[i]);
+                    removed = true;
+                }
+            }
+            if (removed) {
+                setNextAlarmLocked();
+            }
+        }
+    }
+
+    /** Cancel and remove all alarms. */
+    public void removeAllAlarms() {
+        synchronized (mLock) {
+            mAlarmPriorityQueue.clear();
+            setNextAlarmLocked(0);
+        }
+    }
+
+    /** Remove all alarms that satisfy the predicate. */
+    protected void removeAlarmsIf(@NonNull Predicate<K> predicate) {
+        boolean removed = false;
+        synchronized (mLock) {
+            Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
+            for (int i = alarms.length - 1; i >= 0; --i) {
+                final K key = (K) alarms[i].first;
+                if (predicate.test(key)) {
+                    mAlarmPriorityQueue.remove(alarms[i]);
+                    removed = true;
+                }
+            }
+            if (removed) {
+                setNextAlarmLocked();
+            }
+        }
+    }
+
+    /**
+     * Update the minimum time that should be between alarms. This helps avoid thrashing when alarms
+     * are scheduled very closely together and may result in some batching of expired alarms.
+     */
+    public void setMinTimeBetweenAlarmsMs(long minTimeMs) {
+        if (minTimeMs < 0) {
+            throw new IllegalArgumentException("min time between alarms must be non-negative");
+        }
+        synchronized (mLock) {
+            mMinTimeBetweenAlarmsMs = minTimeMs;
+        }
+    }
+
+    /** Return true if the key is for the specified user. */
+    protected abstract boolean isForUser(@NonNull K key, int userId);
+
+    /** Handle all of the alarms that have now expired (their trigger time has passed). */
+    protected abstract void processExpiredAlarms(@NonNull ArraySet<K> expired);
+
+    /** Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue after now. */
+    @GuardedBy("mLock")
+    private void setNextAlarmLocked() {
+        setNextAlarmLocked(mInjector.getElapsedRealtime());
+    }
+
+    /**
+     * Sets an alarm with {@link AlarmManager} for the earliest alarm in the queue, using
+     * {@code earliestTriggerElapsed} as a floor.
+     */
+    @GuardedBy("mLock")
+    private void setNextAlarmLocked(long earliestTriggerElapsed) {
+        if (mAlarmPriorityQueue.size() == 0) {
+            mHandler.post(() -> {
+                // Never call out to AlarmManager with the lock held. This could sit below AM.
+                final AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
+                if (alarmManager != null) {
+                    // This should only be null at boot time. No concerns around not
+                    // cancelling if we get null here, so no need to retry.
+                    alarmManager.cancel(this);
+                }
+            });
+            mTriggerTimeElapsed = NOT_SCHEDULED;
+            return;
+        }
+
+        final Pair<K, Long> alarm = mAlarmPriorityQueue.peek();
+        final long nextTriggerTimeElapsed = Math.max(earliestTriggerElapsed, alarm.second);
+        // Only schedule the alarm if one of the following is true:
+        // 1. There isn't one currently scheduled
+        // 2. The new alarm is significantly earlier than the previous alarm. If it's
+        // earlier but not significantly so, then we essentially delay the check for some
+        // apps by up to a minute.
+        // 3. The alarm is after the current alarm.
+        if (mTriggerTimeElapsed == NOT_SCHEDULED
+                || nextTriggerTimeElapsed < mTriggerTimeElapsed - MINUTE_IN_MILLIS
+                || mTriggerTimeElapsed < nextTriggerTimeElapsed) {
+            if (DEBUG) {
+                Slog.d(TAG, "Scheduling alarm at " + nextTriggerTimeElapsed
+                        + " for key " + alarm.first);
+            }
+            mTriggerTimeElapsed = nextTriggerTimeElapsed;
+            mHandler.post(mScheduleAlarmRunnable);
+        }
+    }
+
+    @Override
+    public void onAlarm() {
+        final ArraySet<K> expired = new ArraySet<>();
+        synchronized (mLock) {
+            final long nowElapsed = mInjector.getElapsedRealtime();
+            while (mAlarmPriorityQueue.size() > 0) {
+                final Pair<K, Long> alarm = mAlarmPriorityQueue.peek();
+                if (alarm.second <= nowElapsed) {
+                    expired.add(alarm.first);
+                    mAlarmPriorityQueue.remove(alarm);
+                } else {
+                    break;
+                }
+            }
+            setNextAlarmLocked(nowElapsed + mMinTimeBetweenAlarmsMs);
+        }
+        // Don't "call out" with the lock held to avoid potential deadlocks.
+        if (expired.size() > 0) {
+            processExpiredAlarms(expired);
+        }
+    }
+
+    /** Dump internal state. */
+    public void dump(IndentingPrintWriter pw) {
+        synchronized (mLock) {
+            pw.print(mDumpTitle);
+            pw.println(" alarms:");
+            pw.increaseIndent();
+
+            if (mAlarmPriorityQueue.size() == 0) {
+                pw.println("NOT WAITING");
+            } else {
+                Pair[] alarms = mAlarmPriorityQueue.toArray(new Pair[mAlarmPriorityQueue.size()]);
+                for (int i = 0; i < alarms.length; ++i) {
+                    final K key = (K) alarms[i].first;
+                    pw.print(key);
+                    pw.print(": ");
+                    pw.print(alarms[i].second);
+                    pw.println();
+                }
+            }
+
+            pw.decreaseIndent();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/utils/quota/QuotaTracker.java b/services/core/java/com/android/server/utils/quota/QuotaTracker.java
index 802ab5b..a0363ef 100644
--- a/services/core/java/com/android/server/utils/quota/QuotaTracker.java
+++ b/services/core/java/com/android/server/utils/quota/QuotaTracker.java
@@ -29,11 +29,11 @@
 import android.content.IntentFilter;
 import android.net.Uri;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
-import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArrayMap;
 import android.util.proto.ProtoOutputStream;
@@ -45,8 +45,7 @@
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemServiceManager;
-
-import java.util.PriorityQueue;
+import com.android.server.utils.AlarmQueue;
 
 /**
  * Base class for trackers that track whether an app has exceeded a count quota.
@@ -88,10 +87,10 @@
     private final ArraySet<QuotaChangeListener> mQuotaChangeListeners = new ArraySet<>();
 
     /**
-     * Listener to track and manage when each package comes back within quota.
+     * Alarm queue to track and manage when each package comes back within quota.
      */
     @GuardedBy("mLock")
-    private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener();
+    private final InQuotaAlarmQueue mInQuotaAlarmQueue;
 
     /** "Free quota status" for apps. */
     @GuardedBy("mLock")
@@ -163,6 +162,8 @@
         mContext = context;
         mInjector = injector;
         mAlarmManager = mContext.getSystemService(AlarmManager.class);
+        // The operation should be fast enough to put it on the FgThread.
+        mInQuotaAlarmQueue = new InQuotaAlarmQueue(mContext, FgThread.getHandler().getLooper());
 
         final IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
@@ -179,7 +180,7 @@
     /** Remove all saved events from the tracker. */
     public void clear() {
         synchronized (mLock) {
-            mInQuotaAlarmListener.clearLocked();
+            mInQuotaAlarmQueue.removeAllAlarms();
             mFreeQuota.clear();
 
             dropEverythingLocked();
@@ -367,7 +368,7 @@
             return;
         }
 
-        mInQuotaAlarmListener.removeAlarmsLocked(userId, packageName);
+        mInQuotaAlarmQueue.removeAlarms(userId, packageName);
 
         mFreeQuota.delete(userId, packageName);
 
@@ -379,7 +380,7 @@
 
     @GuardedBy("mLock")
     private void onUserRemovedLocked(int userId) {
-        mInQuotaAlarmListener.removeAlarmsLocked(userId);
+        mInQuotaAlarmQueue.removeAlarmsForUserId(userId);
         mFreeQuota.delete(userId);
 
         handleRemovedUserLocked(userId);
@@ -434,190 +435,43 @@
                 Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
                         + " even though it's within quota");
             }
-            mInQuotaAlarmListener.removeAlarmLocked(new Uptc(userId, packageName, tag));
+            mInQuotaAlarmQueue.removeAlarmForKey(new Uptc(userId, packageName, tag));
             maybeUpdateQuotaStatus(userId, packageName, tag);
             return;
         }
 
-        mInQuotaAlarmListener.addAlarmLocked(new Uptc(userId, packageName, tag),
+        mInQuotaAlarmQueue.addAlarm(new Uptc(userId, packageName, tag),
                 getInQuotaTimeElapsedLocked(userId, packageName, tag));
     }
 
     @GuardedBy("mLock")
     void cancelScheduledStartAlarmLocked(final int userId,
             @NonNull final String packageName, @Nullable final String tag) {
-        mInQuotaAlarmListener.removeAlarmLocked(new Uptc(userId, packageName, tag));
-    }
-
-    static class AlarmQueue extends PriorityQueue<Pair<Uptc, Long>> {
-        AlarmQueue() {
-            super(1, (o1, o2) -> (int) (o1.second - o2.second));
-        }
-
-        /**
-         * Remove any instances of the Uptc from the queue.
-         *
-         * @return true if an instance was removed, false otherwise.
-         */
-        boolean remove(@NonNull Uptc uptc) {
-            boolean removed = false;
-            Pair[] alarms = toArray(new Pair[size()]);
-            for (int i = alarms.length - 1; i >= 0; --i) {
-                if (uptc.equals(alarms[i].first)) {
-                    remove(alarms[i]);
-                    removed = true;
-                }
-            }
-            return removed;
-        }
+        mInQuotaAlarmQueue.removeAlarmForKey(new Uptc(userId, packageName, tag));
     }
 
     /** Track when UPTCs are expected to come back into quota. */
-    private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener {
-        @GuardedBy("mLock")
-        private final AlarmQueue mAlarmQueue = new AlarmQueue();
-        /** The next time the alarm is set to go off, in the elapsed realtime timebase. */
-        @GuardedBy("mLock")
-        private long mTriggerTimeElapsed = 0;
-
-        @GuardedBy("mLock")
-        void addAlarmLocked(@NonNull Uptc uptc, long inQuotaTimeElapsed) {
-            mAlarmQueue.remove(uptc);
-            mAlarmQueue.offer(new Pair<>(uptc, inQuotaTimeElapsed));
-            setNextAlarmLocked();
-        }
-
-        @GuardedBy("mLock")
-        void clearLocked() {
-            cancelAlarm(this);
-            mAlarmQueue.clear();
-            mTriggerTimeElapsed = 0;
-        }
-
-        @GuardedBy("mLock")
-        void removeAlarmLocked(@NonNull Uptc uptc) {
-            if (mAlarmQueue.remove(uptc)) {
-                if (mAlarmQueue.size() == 0) {
-                    cancelAlarm(this);
-                } else {
-                    setNextAlarmLocked();
-                }
-            }
-        }
-
-        @GuardedBy("mLock")
-        void removeAlarmsLocked(int userId) {
-            boolean removed = false;
-            Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
-            for (int i = alarms.length - 1; i >= 0; --i) {
-                final Uptc uptc = (Uptc) alarms[i].first;
-                if (userId == uptc.userId) {
-                    mAlarmQueue.remove(alarms[i]);
-                    removed = true;
-                }
-            }
-            if (removed) {
-                setNextAlarmLocked();
-            }
-        }
-
-        @GuardedBy("mLock")
-        void removeAlarmsLocked(int userId, @NonNull String packageName) {
-            boolean removed = false;
-            Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
-            for (int i = alarms.length - 1; i >= 0; --i) {
-                final Uptc uptc = (Uptc) alarms[i].first;
-                if (userId == uptc.userId && packageName.equals(uptc.packageName)) {
-                    mAlarmQueue.remove(alarms[i]);
-                    removed = true;
-                }
-            }
-            if (removed) {
-                setNextAlarmLocked();
-            }
-        }
-
-        @GuardedBy("mLock")
-        private void setNextAlarmLocked() {
-            if (mAlarmQueue.size() > 0) {
-                final long nextTriggerTimeElapsed = mAlarmQueue.peek().second;
-                // Only schedule the alarm if one of the following is true:
-                // 1. There isn't one currently scheduled
-                // 2. The new alarm is significantly earlier than the previous alarm. If it's
-                // earlier but not significantly so, then we essentially delay the notification a
-                // few extra minutes.
-                if (mTriggerTimeElapsed == 0
-                        || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS
-                        || mTriggerTimeElapsed < nextTriggerTimeElapsed) {
-                    // Use a non-wakeup alarm for this
-                    scheduleAlarm(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed,
-                            ALARM_TAG_QUOTA_CHECK, this);
-                    mTriggerTimeElapsed = nextTriggerTimeElapsed;
-                }
-            } else {
-                cancelAlarm(this);
-                mTriggerTimeElapsed = 0;
-            }
+    private class InQuotaAlarmQueue extends AlarmQueue<Uptc> {
+        private InQuotaAlarmQueue(Context context, Looper looper) {
+            super(context, looper, ALARM_TAG_QUOTA_CHECK, "In quota", false, 0);
         }
 
         @Override
-        public void onAlarm() {
-            synchronized (mLock) {
-                while (mAlarmQueue.size() > 0) {
-                    final Pair<Uptc, Long> alarm = mAlarmQueue.peek();
-                    if (alarm.second <= mInjector.getElapsedRealtime()) {
-                        getHandler().post(() -> maybeUpdateQuotaStatus(
-                                alarm.first.userId, alarm.first.packageName, alarm.first.tag));
-                        mAlarmQueue.remove(alarm);
-                    } else {
-                        break;
-                    }
-                }
-                setNextAlarmLocked();
-            }
+        protected boolean isForUser(@NonNull Uptc uptc, int userId) {
+            return userId == uptc.userId;
         }
 
-        @GuardedBy("mLock")
-        void dumpLocked(IndentingPrintWriter pw) {
-            pw.println("In quota alarms:");
-            pw.increaseIndent();
-
-            if (mAlarmQueue.size() == 0) {
-                pw.println("NOT WAITING");
-            } else {
-                Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
-                for (int i = 0; i < alarms.length; ++i) {
-                    final Uptc uptc = (Uptc) alarms[i].first;
-                    pw.print(uptc);
-                    pw.print(": ");
-                    pw.print(alarms[i].second);
-                    pw.println();
-                }
-            }
-
-            pw.decreaseIndent();
+        void removeAlarms(int userId, @NonNull String packageName) {
+            removeAlarmsIf((uptc) -> userId == uptc.userId && packageName.equals(uptc.packageName));
         }
 
-        @GuardedBy("mLock")
-        void dumpLocked(ProtoOutputStream proto, long fieldId) {
-            final long token = proto.start(fieldId);
-
-            proto.write(QuotaTrackerProto.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED,
-                    mTriggerTimeElapsed);
-
-            Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
-            for (int i = 0; i < alarms.length; ++i) {
-                final long aToken = proto.start(QuotaTrackerProto.InQuotaAlarmListener.ALARMS);
-
-                final Uptc uptc = (Uptc) alarms[i].first;
-                uptc.dumpDebug(proto, QuotaTrackerProto.InQuotaAlarmListener.Alarm.UPTC);
-                proto.write(QuotaTrackerProto.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED,
-                        (Long) alarms[i].second);
-
-                proto.end(aToken);
+        @Override
+        protected void processExpiredAlarms(@NonNull ArraySet<Uptc> expired) {
+            for (int i = 0; i < expired.size(); ++i) {
+                Uptc uptc = expired.valueAt(i);
+                getHandler().post(
+                        () -> maybeUpdateQuotaStatus(uptc.userId, uptc.packageName, uptc.tag));
             }
-
-            proto.end(token);
         }
     }
 
@@ -635,7 +489,7 @@
             pw.println();
 
             pw.println();
-            mInQuotaAlarmListener.dumpLocked(pw);
+            mInQuotaAlarmQueue.dump(pw);
 
             pw.println();
             pw.println("Per-app free quota:");
@@ -669,7 +523,6 @@
             proto.write(QuotaTrackerProto.IS_ENABLED, mIsEnabled);
             proto.write(QuotaTrackerProto.IS_GLOBAL_QUOTA_FREE, mIsQuotaFree);
             proto.write(QuotaTrackerProto.ELAPSED_REALTIME, mInjector.getElapsedRealtime());
-            mInQuotaAlarmListener.dumpLocked(proto, QuotaTrackerProto.IN_QUOTA_ALARM_LISTENER);
         }
 
         proto.end(token);
diff --git a/services/core/java/com/android/server/vibrator/OWNERS b/services/core/java/com/android/server/vibrator/OWNERS
index 7e7335d..08f0a90 100644
--- a/services/core/java/com/android/server/vibrator/OWNERS
+++ b/services/core/java/com/android/server/vibrator/OWNERS
@@ -1 +1,3 @@
+lsandrade@google.com
 michaelwr@google.com
+sbowden@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/vibrator/RampDownAdapter.java b/services/core/java/com/android/server/vibrator/RampDownAdapter.java
index d5cd344..e97ed4c 100644
--- a/services/core/java/com/android/server/vibrator/RampDownAdapter.java
+++ b/services/core/java/com/android/server/vibrator/RampDownAdapter.java
@@ -77,9 +77,8 @@
      */
     private int addRampDownToZeroAmplitudeSegments(List<VibrationEffectSegment> segments,
             int repeatIndex) {
-        int newRepeatIndex = repeatIndex;
-        int newSegmentCount = segments.size();
-        for (int i = 1; i < newSegmentCount; i++) {
+        int segmentCount = segments.size();
+        for (int i = 1; i < segmentCount; i++) {
             VibrationEffectSegment previousSegment = segments.get(i - 1);
             if (!isOffSegment(segments.get(i))
                     || !endsWithNonZeroAmplitude(previousSegment)) {
@@ -116,16 +115,23 @@
             if (replacementSegments != null) {
                 int segmentsAdded = replacementSegments.size() - 1;
 
-                segments.remove(i);
+                VibrationEffectSegment originalOffSegment = segments.remove(i);
                 segments.addAll(i, replacementSegments);
-                if (repeatIndex > i) {
-                    newRepeatIndex += segmentsAdded;
+                if (repeatIndex >= i) {
+                    if (repeatIndex == i) {
+                        // This effect is repeating to the removed off segment: add it back at the
+                        // end of the vibration so the loop timings are preserved, and skip it.
+                        segments.add(originalOffSegment);
+                        repeatIndex++;
+                        segmentCount++;
+                    }
+                    repeatIndex += segmentsAdded;
                 }
                 i += segmentsAdded;
-                newSegmentCount += segmentsAdded;
+                segmentCount += segmentsAdded;
             }
         }
-        return newRepeatIndex;
+        return repeatIndex;
     }
 
     /**
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 25321c1..a327382 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -1077,7 +1077,7 @@
             newSegments.remove(segmentIndex);
             newSegments.addAll(segmentIndex, fallback.getSegments());
             if (segmentIndex < effect.getRepeatIndex()) {
-                newRepeatIndex += fallback.getSegments().size();
+                newRepeatIndex += fallback.getSegments().size() - 1;
             }
             return new VibrationEffect.Composed(newSegments, newRepeatIndex);
         }
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 567463f..239a112 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -1344,24 +1344,18 @@
                 return IExternalVibratorService.SCALE_MUTE;
             }
 
-            int mode = checkAppOpModeLocked(vib.getUid(), vib.getPackage(),
-                    vib.getVibrationAttributes());
-            if (mode != AppOpsManager.MODE_ALLOWED) {
-                ExternalVibrationHolder vibHolder = new ExternalVibrationHolder(vib);
-                vibHolder.scale = IExternalVibratorService.SCALE_MUTE;
-                if (mode == AppOpsManager.MODE_ERRORED) {
-                    Slog.w(TAG, "Would be an error: external vibrate from uid " + vib.getUid());
-                    endVibrationLocked(vibHolder, Vibration.Status.IGNORED_ERROR_APP_OPS);
-                } else {
-                    endVibrationLocked(vibHolder, Vibration.Status.IGNORED_APP_OPS);
-                }
-                return vibHolder.scale;
-            }
-
             ExternalVibrationHolder cancelingExternalVibration = null;
             VibrationThread cancelingVibration = null;
             int scale;
             synchronized (mLock) {
+                Vibration.Status ignoreStatus = shouldIgnoreVibrationLocked(
+                        vib.getUid(), vib.getPackage(), vib.getVibrationAttributes());
+                if (ignoreStatus != null) {
+                    ExternalVibrationHolder vibHolder = new ExternalVibrationHolder(vib);
+                    vibHolder.scale = IExternalVibratorService.SCALE_MUTE;
+                    endVibrationLocked(vibHolder, ignoreStatus);
+                    return vibHolder.scale;
+                }
                 if (mCurrentExternalVibration != null
                         && mCurrentExternalVibration.externalVibration.equals(vib)) {
                     // We are already playing this external vibration, so we can return the same
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 5441414..fb337e2 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -51,6 +51,7 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Application;
 import android.content.Context;
 import android.content.pm.PackageManagerInternal;
@@ -134,6 +135,8 @@
     private SparseArray<DisplayMagnifier> mDisplayMagnifiers = new SparseArray<>();
     private SparseArray<WindowsForAccessibilityObserver> mWindowsForAccessibilityObserver =
             new SparseArray<>();
+    private SparseArray<IBinder> mFocusedWindow = new SparseArray<>();
+    private int mFocusedDisplay = -1;
 
     // Set to true if initializing window population complete.
     private boolean mAllObserversInitialized = true;
@@ -516,6 +519,29 @@
                 + "mWindowsForAccessibilityObserver=" + mWindowsForAccessibilityObserver);
     }
 
+    void onFocusChanged(InputTarget lastTarget, InputTarget newTarget) {
+        if (lastTarget != null) {
+            mFocusedWindow.remove(lastTarget.getDisplayId());
+        }
+        if (newTarget != null) {
+            int displayId = newTarget.getDisplayId();
+            IBinder clientBinder = newTarget.getIWindow().asBinder();
+            mFocusedWindow.put(displayId, clientBinder);
+        }
+    }
+
+    public void onDisplayRemoved(int displayId) {
+        mFocusedWindow.remove(displayId);
+    }
+
+    public void setFocusedDisplay(int focusedDisplayId) {
+        mFocusedDisplay = focusedDisplayId;
+    }
+
+    @Nullable IBinder getFocusedWindowToken() {
+        return mFocusedWindow.get(mFocusedDisplay);
+    }
+
     /**
      * This class encapsulates the functionality related to display magnification.
      */
@@ -1838,36 +1864,11 @@
                     tempWindowStatesList.add(w);
                 }
             }, false /* traverseTopToBottom */);
-            // Insert the re-parented windows in another display below their parents in
-            // default display.
-            mService.mRoot.forAllWindows(w -> {
-                final WindowState parentWindow = findRootDisplayParentWindow(w);
-                if (parentWindow == null) {
-                    return;
-                }
-
-                if (w.isVisible() && tempWindowStatesList.contains(parentWindow)) {
-                    tempWindowStatesList.add(tempWindowStatesList.lastIndexOf(parentWindow), w);
-                }
-            }, false /* traverseTopToBottom */);
             for (int i = 0; i < tempWindowStatesList.size(); i++) {
                 outWindows.put(i, tempWindowStatesList.get(i));
             }
         }
 
-        private WindowState findRootDisplayParentWindow(WindowState win) {
-            WindowState displayParentWindow = win.getDisplayContent().getParentWindow();
-            if (displayParentWindow == null) {
-                return null;
-            }
-            WindowState candidate = displayParentWindow;
-            while (candidate != null) {
-                displayParentWindow = candidate;
-                candidate = displayParentWindow.getDisplayContent().getParentWindow();
-            }
-            return displayParentWindow;
-        }
-
         private WindowState getTopFocusWindow() {
             return mService.mRoot.getTopFocusedDisplayContent().mCurrentFocus;
         }
diff --git a/services/core/java/com/android/server/wm/ActivityAssistInfo.java b/services/core/java/com/android/server/wm/ActivityAssistInfo.java
index 054044b..e1e7ee4 100644
--- a/services/core/java/com/android/server/wm/ActivityAssistInfo.java
+++ b/services/core/java/com/android/server/wm/ActivityAssistInfo.java
@@ -30,7 +30,7 @@
     private final int mTaskId;
 
     public ActivityAssistInfo(ActivityRecord activityRecord) {
-        this.mActivityToken = activityRecord.appToken;
+        this.mActivityToken = activityRecord.token;
         this.mAssistToken = activityRecord.assistToken;
         this.mTaskId = activityRecord.getTask().mTaskId;
     }
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index ee72fc8..6c9f1e5 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -558,7 +558,7 @@
                 final ActivityRecord below = ar.getTask().getActivity((r) -> !r.finishing,
                         ar, false /*includeBoundary*/, true /*traverseTopToBottom*/);
                 if (below != null && below.getUid() == ar.getUid()) {
-                    return below.appToken.asBinder();
+                    return below.token;
                 }
             }
         } finally {
diff --git a/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java
new file mode 100644
index 0000000..00e1f55
--- /dev/null
+++ b/services/core/java/com/android/server/wm/ActivityInterceptorCallback.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ResolveInfo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Callback to intercept activity starts and possibly block/redirect them.
+ */
+public abstract class ActivityInterceptorCallback {
+    /**
+     * Intercept the launch intent based on various signals. If an interception happened, returns
+     * a new/existing non-null {@link Intent} which may redirect to another activity.
+     *
+     * @return null if no interception occurred, or a non-null intent which replaces the
+     * existing intent.
+     */
+    public abstract @Nullable Intent intercept(ActivityInterceptorInfo info);
+
+    /**
+     * The unique id of each interceptor which determines the order it will execute in.
+     */
+    @IntDef(suffix = { "_ORDERED_ID" }, value = {
+            FIRST_ORDERED_ID,
+            COMMUNAL_MODE_ORDERED_ID,
+            LAST_ORDERED_ID // Update this when adding new ids
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface OrderedId {}
+
+    /**
+     * The first id, used by the framework to determine the valid range of ids.
+     */
+    static final int FIRST_ORDERED_ID = 0;
+
+    /**
+     * The identifier for {@link com.android.server.communal.CommunalManagerService} interceptor.
+     */
+    public static final int COMMUNAL_MODE_ORDERED_ID = 1;
+
+    /**
+     * The final id, used by the framework to determine the valid range of ids. Update this when
+     * adding new ids.
+     */
+    static final int LAST_ORDERED_ID = COMMUNAL_MODE_ORDERED_ID;
+
+    /**
+     * Data class for storing the various arguments needed for activity interception.
+     */
+    public static final class ActivityInterceptorInfo {
+        public final int realCallingUid;
+        public final int realCallingPid;
+        public final int userId;
+        public final String callingPackage;
+        public final String callingFeatureId;
+        public final Intent intent;
+        public final ResolveInfo rInfo;
+        public final ActivityInfo aInfo;
+        public final String resolvedType;
+        public final int callingPid;
+        public final int callingUid;
+        public final ActivityOptions checkedOptions;
+
+        public ActivityInterceptorInfo(int realCallingUid, int realCallingPid, int userId,
+                String callingPackage, String callingFeatureId, Intent intent,
+                ResolveInfo rInfo, ActivityInfo aInfo, String resolvedType, int callingPid,
+                int callingUid, ActivityOptions checkedOptions) {
+            this.realCallingUid = realCallingUid;
+            this.realCallingPid = realCallingPid;
+            this.userId = userId;
+            this.callingPackage = callingPackage;
+            this.callingFeatureId = callingFeatureId;
+            this.intent = intent;
+            this.rInfo = rInfo;
+            this.aInfo = aInfo;
+            this.resolvedType = resolvedType;
+            this.callingPid = callingPid;
+            this.callingUid = callingUid;
+            this.checkedOptions = checkedOptions;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 3b1da1d..d084b13 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -46,8 +46,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.activityTypeToString;
 import static android.app.WindowConfiguration.isSplitScreenWindowingMode;
-import static android.app.servertransaction.TransferSplashScreenViewStateItem.ATTACH_TO;
-import static android.app.servertransaction.TransferSplashScreenViewStateItem.HANDOVER_TO;
 import static android.content.Intent.ACTION_MAIN;
 import static android.content.Intent.CATEGORY_HOME;
 import static android.content.Intent.CATEGORY_LAUNCHER;
@@ -302,7 +300,6 @@
 import android.view.DisplayCutout;
 import android.view.DisplayInfo;
 import android.view.IAppTransitionAnimationSpecsFuture;
-import android.view.IApplicationToken;
 import android.view.InputApplicationHandle;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationDefinition;
@@ -331,7 +328,6 @@
 import com.android.internal.os.TransferPipe;
 import com.android.internal.policy.AttributeCache;
 import com.android.internal.protolog.common.ProtoLog;
-import com.android.internal.util.ToBooleanFunction;
 import com.android.internal.util.XmlUtils;
 import com.android.server.LocalServices;
 import com.android.server.am.AppTimeTracker;
@@ -361,7 +357,6 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Consumer;
-import java.util.function.Function;
 import java.util.function.Predicate;
 
 /**
@@ -401,10 +396,6 @@
     private static final int STARTING_WINDOW_TYPE_SNAPSHOT = 1;
     private static final int STARTING_WINDOW_TYPE_SPLASH_SCREEN = 2;
 
-    /**
-     * Value to increment the z-layer when boosting a layer during animations. BOOST in l33tsp34k.
-     */
-    @VisibleForTesting static final int Z_BOOST_BASE = 800570000;
     static final int INVALID_PID = -1;
 
     // How long we wait until giving up on the last activity to pause.  This
@@ -430,8 +421,6 @@
 
     final ActivityTaskManagerService mAtmService;
     final ActivityInfo info; // activity info provided by developer in AndroidManifest
-    // TODO: rename to mActivityToken
-    final ActivityRecord.Token appToken;
     // Which user is this running for?
     final int mUserId;
     // The package implementing intent's component
@@ -1229,7 +1218,7 @@
                 TransferPipe tp = new TransferPipe();
                 try {
                     r.app.getThread().dumpActivity(
-                            tp.getWriteFd(), r.appToken, innerPrefix, args);
+                            tp.getWriteFd(), r.token, innerPrefix, args);
                     // Short timeout, since blocking here can deadlock with the application.
                     tp.go(fd, 2000);
                 } finally {
@@ -1296,7 +1285,7 @@
                     + "display, activityRecord=%s, displayId=%d, config=%s", this, displayId,
                     config);
 
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
+            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
                     MoveToDisplayItem.obtain(displayId, config));
         } catch (RemoteException e) {
             // If process died, whatever.
@@ -1313,7 +1302,7 @@
             ProtoLog.v(WM_DEBUG_CONFIGURATION, "Sending new config to %s, "
                     + "config: %s", this, config);
 
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
+            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
                     ActivityConfigurationChangeItem.obtain(config));
         } catch (RemoteException e) {
             // If process died, whatever.
@@ -1331,7 +1320,7 @@
             ProtoLog.v(WM_DEBUG_STATES, "Sending position change to %s, onTop: %b",
                     this, onTop);
 
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
+            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
                     TopResumedActivityChangeItem.obtain(onTop));
         } catch (RemoteException e) {
             // If process died, whatever.
@@ -1450,8 +1439,6 @@
             if (getDisplayContent() != null) {
                 getDisplayContent().mClosingApps.remove(this);
             }
-        } else if (oldTask != null && oldTask.getRootTask() != null) {
-            task.getRootTask().mExitingActivities.remove(this);
         }
         final Task rootTask = getRootTask();
 
@@ -1581,60 +1568,27 @@
         return mLetterboxUiController.isFullyTransparentBarAllowed(rect);
     }
 
-    static class Token extends IApplicationToken.Stub {
-        private WeakReference<ActivityRecord> weakActivity;
-        private final String name;
-        private final String tokenString;
-
-        Token(Intent intent) {
-            name = intent.getComponent().flattenToShortString();
-            tokenString = "Token{" + Integer.toHexString(System.identityHashCode(this)) + "}";
-        }
-
-        private void attach(ActivityRecord activity) {
-            if (weakActivity != null) {
-                throw new IllegalStateException("Already attached..." + this);
-            }
-            weakActivity = new WeakReference<>(activity);
-        }
-
-        private static @Nullable ActivityRecord tokenToActivityRecordLocked(Token token) {
-            if (token == null) {
-                return null;
-            }
-            ActivityRecord r = token.weakActivity.get();
-            if (r == null || r.getRootTask() == null) {
-                return null;
-            }
-            return r;
-        }
+    private static class Token extends Binder {
+        @NonNull WeakReference<ActivityRecord> mActivityRef;
 
         @Override
         public String toString() {
-            StringBuilder sb = new StringBuilder(128);
-            sb.append("Token{");
-            sb.append(Integer.toHexString(System.identityHashCode(this)));
-            sb.append(' ');
-            if (weakActivity != null) {
-                sb.append(weakActivity.get());
-            }
-            sb.append('}');
-            return sb.toString();
-        }
-
-        @Override
-        public String getName() {
-            return name;
+            return "Token{" + Integer.toHexString(System.identityHashCode(this)) + " "
+                    + mActivityRef.get() + "}";
         }
     }
 
     static @Nullable ActivityRecord forTokenLocked(IBinder token) {
+        if (token == null) return null;
+        final Token activityToken;
         try {
-            return Token.tokenToActivityRecordLocked((Token)token);
+            activityToken = (Token) token;
         } catch (ClassCastException e) {
             Slog.w(TAG, "Bad activity token: " + token, e);
             return null;
         }
+        final ActivityRecord r = activityToken.mActivityRef.get();
+        return r == null || r.getRootTask() == null ? null : r;
     }
 
     static boolean isResolverActivity(String className) {
@@ -1666,11 +1620,11 @@
             boolean _rootVoiceInteraction, ActivityTaskSupervisor supervisor,
             ActivityOptions options, ActivityRecord sourceRecord, PersistableBundle persistentState,
             TaskDescription _taskDescription, long _createTime) {
-        super(_service.mWindowManager, new Token(_intent).asBinder(), TYPE_APPLICATION, true,
+        super(_service.mWindowManager, new Token(), TYPE_APPLICATION, true,
                 null /* displayContent */, false /* ownerCanManageAppTokens */);
 
         mAtmService = _service;
-        appToken = (Token) token;
+        ((Token) token).mActivityRef = new WeakReference<>(this);
         info = aInfo;
         mUserId = UserHandle.getUserId(info.applicationInfo.uid);
         packageName = info.applicationInfo.packageName;
@@ -1734,8 +1688,6 @@
         cds.attachColorTransformController(packageName, mUserId,
                 new WeakReference<>(mColorTransformController));
 
-        appToken.attach(this);
-
         mRootWindowContainer = _service.mRootWindowContainer;
         launchedFromPid = _launchedFromPid;
         launchedFromUid = _launchedFromUid;
@@ -1873,13 +1825,13 @@
 
     @NonNull InputApplicationHandle getInputApplicationHandle(boolean update) {
         if (mInputApplicationHandle == null) {
-            mInputApplicationHandle = new InputApplicationHandle(appToken, toString(),
+            mInputApplicationHandle = new InputApplicationHandle(token, toString(),
                     mInputDispatchingTimeoutMillis);
         } else if (update) {
             final String name = toString();
             if (mInputDispatchingTimeoutMillis != mInputApplicationHandle.dispatchingTimeoutMillis
                     || !name.equals(mInputApplicationHandle.name)) {
-                mInputApplicationHandle = new InputApplicationHandle(appToken, name,
+                mInputApplicationHandle = new InputApplicationHandle(token, name,
                         mInputDispatchingTimeoutMillis);
             }
         }
@@ -2045,7 +1997,8 @@
     boolean addStartingWindow(String pkg, int resolvedTheme, CompatibilityInfo compatInfo,
             CharSequence nonLocalizedLabel, int labelRes, int icon, int logo, int windowFlags,
             ActivityRecord from, boolean newTask, boolean taskSwitch, boolean processRunning,
-            boolean allowTaskSnapshot, boolean activityCreated, boolean useEmpty) {
+            boolean allowTaskSnapshot, boolean activityCreated, boolean useEmpty,
+            boolean activityAllDrawn) {
         // If the display is frozen, we won't do anything until the actual window is
         // displayed so there is no reason to put in the starting window.
         if (!okToDisplay()) {
@@ -2066,7 +2019,7 @@
                 mWmService.mTaskSnapshotController.getSnapshot(task.mTaskId, task.mUserId,
                         false /* restoreFromDisk */, false /* isLowResolution */);
         final int type = getStartingWindowType(newTask, taskSwitch, processRunning,
-                allowTaskSnapshot, activityCreated, snapshot);
+                allowTaskSnapshot, activityCreated, activityAllDrawn, snapshot);
 
         //TODO(191787740) Remove for T
         final boolean useLegacy = type == STARTING_WINDOW_TYPE_SPLASH_SCREEN
@@ -2080,7 +2033,7 @@
 
         final int typeParameter = mWmService.mStartingSurfaceController
                 .makeStartingWindowTypeParameter(newTask, taskSwitch, processRunning,
-                        allowTaskSnapshot, activityCreated, useEmpty, useLegacy);
+                        allowTaskSnapshot, activityCreated, useEmpty, useLegacy, activityAllDrawn);
 
         if (type == STARTING_WINDOW_TYPE_SNAPSHOT) {
             if (isActivityTypeHome()) {
@@ -2215,10 +2168,10 @@
     private final AddStartingWindow mAddStartingWindow = new AddStartingWindow();
 
     private int getStartingWindowType(boolean newTask, boolean taskSwitch, boolean processRunning,
-            boolean allowTaskSnapshot, boolean activityCreated,
+            boolean allowTaskSnapshot, boolean activityCreated, boolean activityAllDrawn,
             TaskSnapshot snapshot) {
-        if ((newTask || !processRunning || (taskSwitch && !activityCreated))
-                && !isActivityTypeHome()) {
+        if ((newTask || !processRunning || (taskSwitch && !activityCreated)
+                || (taskSwitch && !activityAllDrawn)) && !isActivityTypeHome()) {
             return STARTING_WINDOW_TYPE_SPLASH_SCREEN;
         } else if (taskSwitch && allowTaskSnapshot) {
             if (isSnapshotCompatible(snapshot)) {
@@ -2326,7 +2279,8 @@
         // unable to copy from shell, maybe it's not a splash screen. or something went wrong.
         // either way, abort and reset the sequence.
         if (parcelable == null
-                || mTransferringSplashScreenState != TRANSFER_SPLASH_SCREEN_COPYING) {
+                || mTransferringSplashScreenState != TRANSFER_SPLASH_SCREEN_COPYING
+                || mStartingWindow == null) {
             if (parcelable != null) {
                 parcelable.clearIfNeeded();
             }
@@ -2335,13 +2289,17 @@
             return;
         }
         // schedule attach splashScreen to client
+        final SurfaceControl windowAnimationLeash = TaskOrganizerController
+                .applyStartingWindowAnimation(mStartingWindow);
         try {
             mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_ATTACH_TO_CLIENT;
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
-                    TransferSplashScreenViewStateItem.obtain(ATTACH_TO, parcelable));
+            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
+                    TransferSplashScreenViewStateItem.obtain(parcelable,
+                            windowAnimationLeash));
             scheduleTransferSplashScreenTimeout();
         } catch (Exception e) {
             Slog.w(TAG, "onCopySplashScreenComplete fail: " + this);
+            mStartingWindow.cancelAnimation();
             parcelable.clearIfNeeded();
             mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_FINISH;
         }
@@ -2351,14 +2309,9 @@
         removeTransferSplashScreenTimeout();
         // Client has draw the splash screen, so we can remove the starting window.
         if (mStartingWindow != null) {
+            mStartingWindow.cancelAnimation();
             mStartingWindow.hide(false, false);
         }
-        try {
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
-                    TransferSplashScreenViewStateItem.obtain(HANDOVER_TO, null));
-        } catch (Exception e) {
-            Slog.w(TAG, "onSplashScreenAttachComplete fail: " + this);
-        }
         // no matter what, remove the starting window.
         mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_FINISH;
         removeStartingWindowAnimation(false /* prepareAnimation */);
@@ -2405,17 +2358,6 @@
         });
     }
 
-    void removeStartingWindowIfNeeded() {
-        // Removing the task snapshot after the task is actually focused (see
-        // Task#onWindowFocusChanged). Since some of the app contents may draw in this time and
-        // requires more times to draw finish, in case flicking may happen when removing the task
-        // snapshot too early. (i.e. Showing IME.)
-        if ((mStartingData instanceof SnapshotStartingData) && !getTask().isFocused()) {
-            return;
-        }
-        removeStartingWindow();
-    }
-
     void removeStartingWindow() {
         if (transferSplashScreenIfNeeded()) {
             return;
@@ -2493,7 +2435,7 @@
      */
     void reparent(TaskFragment newTaskFrag, int position, String reason) {
         if (getParent() == null) {
-            Slog.w(TAG, "reparent: Attempted to reparent non-existing app token: " + appToken);
+            Slog.w(TAG, "reparent: Attempted to reparent non-existing app token: " + token);
             return;
         }
         final TaskFragment prevTaskFrag = getTaskFragment();
@@ -3449,7 +3391,7 @@
 
             try {
                 if (DEBUG_SWITCH) Slog.i(TAG_SWITCH, "Destroying: " + this);
-                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
+                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
                         DestroyActivityItem.obtain(finishing, configChangeFlags));
             } catch (Exception e) {
                 // We can just ignore exceptions here...  if the process has crashed, our death
@@ -3529,7 +3471,7 @@
         detachFromProcess();
         // Resume key dispatching if it is currently paused before we remove the container.
         resumeKeyDispatchingLocked();
-        mDisplayContent.removeAppToken(appToken);
+        mDisplayContent.removeAppToken(token);
 
         cleanUpActivityServices();
         removeUriPermissionsLocked();
@@ -3849,22 +3791,15 @@
             getDisplayContent().mNoAnimationNotifyOnTransitionFinished.add(token);
         }
 
-        final Task rootTask = getRootTask();
         if (delayed && !isEmpty()) {
             // set the token aside because it has an active animation to be finished
             ProtoLog.v(WM_DEBUG_ADD_REMOVE,
                     "removeAppToken make exiting: %s", this);
-            if (rootTask != null) {
-                rootTask.mExitingActivities.add(this);
-            }
             mIsExiting = true;
         } else {
             // Make sure there is no animation running on this token, so any windows associated
             // with it will be removed as soon as their animations are complete
             cancelAnimation();
-            if (rootTask != null) {
-                rootTask.mExitingActivities.remove(this);
-            }
             removeIfPossible();
         }
 
@@ -4222,26 +4157,8 @@
     }
 
     @Override
-    boolean forAllWindows(ToBooleanFunction<WindowState> callback, boolean traverseTopToBottom) {
-        // For legacy reasons we process the TaskStack.mExitingActivities first in DisplayContent
-        // before the non-exiting app tokens. So, we skip the exiting app tokens here.
-        // TODO: Investigate if we need to continue to do this or if we can just process them
-        // in-order.
-        if (mIsExiting && !forAllWindowsUnchecked(WindowState::waitingForReplacement, true)) {
-            return false;
-        }
-        return forAllWindowsUnchecked(callback, traverseTopToBottom);
-    }
-
-    boolean forAllWindowsUnchecked(ToBooleanFunction<WindowState> callback,
-            boolean traverseTopToBottom) {
-        return super.forAllWindows(callback, traverseTopToBottom);
-    }
-
-    @Override
-    boolean forAllActivities(
-            Function<ActivityRecord, Boolean> callback, boolean traverseTopToBottom) {
-        return callback.apply(this);
+    boolean forAllActivities(Predicate<ActivityRecord> callback, boolean traverseTopToBottom) {
+        return callback.test(this);
     }
 
     @Override
@@ -4255,20 +4172,6 @@
         return callback.test(this) ? this : null;
     }
 
-    @Override
-    protected void setLayer(Transaction t, int layer) {
-        if (!mSurfaceAnimator.hasLeash()) {
-            t.setLayer(mSurfaceControl, layer);
-        }
-    }
-
-    @Override
-    protected void setRelativeLayer(Transaction t, SurfaceControl relativeTo, int layer) {
-        if (!mSurfaceAnimator.hasLeash()) {
-            t.setRelativeLayer(mSurfaceControl, relativeTo, layer);
-        }
-    }
-
     void logStartActivity(int tag, Task task) {
         final Uri data = intent.getData();
         final String strData = data != null ? data.toSafeString() : null;
@@ -4331,7 +4234,7 @@
             try {
                 final ArrayList<ResultInfo> list = new ArrayList<ResultInfo>();
                 list.add(new ResultInfo(resultWho, requestCode, resultCode, data));
-                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
+                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
                         ActivityResultItem.obtain(list));
                 return;
             } catch (Exception e) {
@@ -4379,7 +4282,7 @@
                 // Making sure the client state is RESUMED after transaction completed and doing
                 // so only if activity is currently RESUMED. Otherwise, client may have extra
                 // life-cycle calls to RESUMED (and PAUSED later).
-                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
+                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
                         NewIntentItem.obtain(ar, mState == RESUMED));
                 unsent = false;
             } catch (RemoteException e) {
@@ -4740,8 +4643,7 @@
      */
     void setVisibility(boolean visible) {
         if (getParent() == null) {
-            Slog.w(TAG_WM, "Attempted to set visibility of non-existing app token: "
-                    + appToken);
+            Slog.w(TAG_WM, "Attempted to set visibility of non-existing app token: " + token);
             return;
         }
         if (visible) {
@@ -4779,7 +4681,7 @@
 
         ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                 "setAppVisibility(%s, visible=%b): %s visible=%b mVisibleRequested=%b Callers=%s",
-                appToken, visible, appTransition, isVisible(), mVisibleRequested,
+                token, visible, appTransition, isVisible(), mVisibleRequested,
                 Debug.getCallers(6));
 
         // Before setting mVisibleRequested so we can track changes.
@@ -4919,8 +4821,9 @@
      * @param visible {@code true} if this {@link ActivityRecord} should become visible, otherwise
      *                this should become invisible.
      * @param performLayout if {@code true}, perform surface placement after committing visibility.
+     * @param fromTransition {@code true} if this is part of finishing a transition.
      */
-    void commitVisibility(boolean visible, boolean performLayout) {
+    void commitVisibility(boolean visible, boolean performLayout, boolean fromTransition) {
         // Reset the state of mVisibleSetFromTransferredStartingWindow since visibility is actually
         // been set by the app now.
         mVisibleSetFromTransferredStartingWindow = false;
@@ -4940,7 +4843,8 @@
         } else {
             // If we are being set visible, and the starting window is not yet displayed,
             // then make sure it doesn't get displayed.
-            if (mStartingWindow != null && !mStartingWindow.isDrawn()) {
+            if (mStartingWindow != null && !mStartingWindow.isDrawn()
+                    && (firstWindowDrawn || allDrawn)) {
                 mStartingWindow.clearPolicyVisibilityFlag(LEGACY_POLICY_VISIBILITY);
                 mStartingWindow.mLegacyPolicyVisibilityAfterAnim = false;
             }
@@ -4969,7 +4873,11 @@
         displayContent.getInputMonitor().updateInputWindowsLw(false /*force*/);
         mUseTransferredAnimation = false;
 
-        postApplyAnimation(visible);
+        postApplyAnimation(visible, fromTransition);
+    }
+
+    void commitVisibility(boolean visible, boolean performLayout) {
+        commitVisibility(visible, performLayout, false /* fromTransition */);
     }
 
     /**
@@ -4980,8 +4888,11 @@
      *
      * @param visible {@code true} if this {@link ActivityRecord} has become visible, otherwise
      *                this has become invisible.
+     * @param fromTransition {@code true} if this call is part of finishing a transition. This is
+     *                       needed because the shell transition is no-longer active by the time
+     *                       commitVisibility is called.
      */
-    private void postApplyAnimation(boolean visible) {
+    private void postApplyAnimation(boolean visible, boolean fromTransition) {
         final boolean usingShellTransitions = mTransitionController.isShellTransitionsEnabled();
         final boolean delayed = isAnimating(TRANSITION | PARENTS | CHILDREN,
                 ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION
@@ -5022,7 +4933,8 @@
 
         final DisplayContent displayContent = getDisplayContent();
         if (!displayContent.mClosingApps.contains(this)
-                && !displayContent.mOpeningApps.contains(this)) {
+                && !displayContent.mOpeningApps.contains(this)
+                && !fromTransition) {
             // Take the screenshot before possibly hiding the WSA, otherwise the screenshot
             // will not be taken.
             mWmService.mTaskSnapshotController.notifyAppVisibilityChanged(this, visible);
@@ -5138,7 +5050,7 @@
         if (state == STOPPING && !isSleeping()) {
             if (getParent() == null) {
                 Slog.w(TAG_WM, "Attempted to notify stopping on non-existing app token: "
-                        + appToken);
+                        + token);
                 return;
             }
         }
@@ -5269,8 +5181,7 @@
 
     void notifyAppResumed(boolean wasStopped) {
         if (getParent() == null) {
-            Slog.w(TAG_WM, "Attempted to notify resumed of non-existing app token: "
-                    + appToken);
+            Slog.w(TAG_WM, "Attempted to notify resumed of non-existing app token: " + token);
             return;
         }
         ProtoLog.v(WM_DEBUG_ADD_REMOVE, "notifyAppResumed: wasStopped=%b %s",
@@ -5486,7 +5397,7 @@
             // the move to {@link PAUSED}.
             setState(PAUSING, "makeActiveIfNeeded");
             try {
-                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
+                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
                         PauseActivityItem.obtain(finishing, false /* userLeaving */,
                                 configChangeFlags, false /* dontReport */));
             } catch (Exception e) {
@@ -5499,7 +5410,7 @@
             setState(STARTED, "makeActiveIfNeeded");
 
             try {
-                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
+                mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
                         StartActivityItem.obtain(takeOptions()));
             } catch (Exception e) {
                 Slog.w(TAG, "Exception thrown sending start: " + intent.getComponent(), e);
@@ -5607,7 +5518,7 @@
         stopFreezingScreenLocked(false);
         try {
             if (returningOptions != null) {
-                app.getThread().scheduleOnNewActivityOptions(appToken, returningOptions.toBundle());
+                app.getThread().scheduleOnNewActivityOptions(token, returningOptions.toBundle());
             }
         } catch(RemoteException e) {
         }
@@ -5686,7 +5597,7 @@
     }
 
     void activityPaused(boolean timeout) {
-        ProtoLog.v(WM_DEBUG_STATES, "Activity paused: token=%s, timeout=%b", appToken,
+        ProtoLog.v(WM_DEBUG_STATES, "Activity paused: token=%s, timeout=%b", token,
                 timeout);
 
         final TaskFragment taskFragment = getTaskFragment();
@@ -5786,7 +5697,7 @@
             }
             EventLogTags.writeWmStopActivity(
                     mUserId, System.identityHashCode(this), shortComponentName);
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
+            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
                     StopActivityItem.obtain(configChangeFlags));
 
             mAtmService.mH.postDelayed(mStopTimeoutRunnable, STOP_TIMEOUT);
@@ -5934,14 +5845,14 @@
         if (mayFreezeScreenLocked(app)) {
             if (getParent() == null) {
                 Slog.w(TAG_WM,
-                        "Attempted to freeze screen with non-existing app token: " + appToken);
+                        "Attempted to freeze screen with non-existing app token: " + token);
                 return;
             }
 
             // Window configuration changes only effect windows, so don't require a screen freeze.
             int freezableConfigChanges = configChanges & ~(CONFIG_WINDOW_CONFIGURATION);
             if (freezableConfigChanges == 0 && okToDisplay()) {
-                ProtoLog.v(WM_DEBUG_ORIENTATION, "Skipping set freeze of %s", appToken);
+                ProtoLog.v(WM_DEBUG_ORIENTATION, "Skipping set freeze of %s", token);
                 return;
             }
 
@@ -5959,7 +5870,7 @@
         }
         ProtoLog.i(WM_DEBUG_ORIENTATION,
                 "Set freezing of %s: visible=%b freezing=%b visibleRequested=%b. %s",
-                appToken, isVisible(), mFreezingScreen, mVisibleRequested,
+                token, isVisible(), mFreezingScreen, mVisibleRequested,
                 new RuntimeException().fillInStackTrace());
         if (!mVisibleRequested) {
             return;
@@ -6015,7 +5926,7 @@
                 return;
             }
             ProtoLog.v(WM_DEBUG_ORIENTATION,
-                        "Clear freezing of %s: visible=%b freezing=%b", appToken,
+                        "Clear freezing of %s: visible=%b freezing=%b", token,
                                 isVisible(), isFreezingScreen());
             stopFreezingScreen(true, force);
         }
@@ -6080,13 +5991,13 @@
         final Task associatedTask =
                 mSharedStartingData != null ? mSharedStartingData.mAssociatedTask : null;
         if (associatedTask == null) {
-            removeStartingWindowIfNeeded();
+            removeStartingWindow();
         } else if (associatedTask.getActivity(
                 r -> r.mVisibleRequested && !r.firstWindowDrawn) == null) {
             // The last drawn activity may not be the one that owns the starting window.
             final ActivityRecord r = associatedTask.topActivityContainsStartingWindow();
             if (r != null) {
-                r.removeStartingWindowIfNeeded();
+                r.removeStartingWindow();
             }
         }
         updateReportedVisibilityLocked();
@@ -6139,7 +6050,7 @@
 
     /** Called when the windows associated app window container are visible. */
     void onWindowsVisible() {
-        if (DEBUG_VISIBILITY) Slog.v(TAG_WM, "Reporting visible in " + appToken);
+        if (DEBUG_VISIBILITY) Slog.v(TAG_WM, "Reporting visible in " + token);
         mTaskSupervisor.stopWaitingForActivityVisible(this);
         if (DEBUG_SWITCH) Log.v(TAG_SWITCH, "windowsVisibleLocked(): " + this);
         if (!nowVisible) {
@@ -6165,7 +6076,7 @@
 
     /** Called when the windows associated app window container are no longer visible. */
     void onWindowsGone() {
-        if (DEBUG_VISIBILITY) Slog.v(TAG_WM, "Reporting gone in " + appToken);
+        if (DEBUG_VISIBILITY) Slog.v(TAG_WM, "Reporting gone in " + token);
         if (DEBUG_SWITCH) Log.v(TAG_SWITCH, "windowsGone(): " + this);
         nowVisible = false;
     }
@@ -6650,7 +6561,7 @@
         final boolean scheduled = addStartingWindow(packageName, resolvedTheme,
                 compatInfo, nonLocalizedLabel, labelRes, icon, logo, windowFlags,
                 prev, newTask || newSingleActivity, taskSwitch, isProcessRunning(),
-                allowTaskSnapshot(), activityCreated, mSplashScreenStyleEmpty);
+                allowTaskSnapshot(), activityCreated, mSplashScreenStyleEmpty, allDrawn);
         if (DEBUG_STARTING_WINDOW_VERBOSE && scheduled) {
             Slog.d(TAG, "Scheduled starting window for " + this);
         }
@@ -6797,12 +6708,6 @@
         return candidate;
     }
 
-    SurfaceControl getAppAnimationLayer() {
-        return getAppAnimationLayer(isActivityTypeHome() ? ANIMATION_LAYER_HOME
-                : needsZBoost() ? ANIMATION_LAYER_BOOSTED
-                        : ANIMATION_LAYER_STANDARD);
-    }
-
     @Override
     boolean needsZBoost() {
         return mNeedsZBoost || super.needsZBoost();
@@ -6863,29 +6768,9 @@
                 || mDisplayContent.isNextTransitionForward();
     }
 
-    private int getAnimationLayer() {
-        // The leash is parented to the animation layer. We need to preserve the z-order by using
-        // the prefix order index, but we boost if necessary.
-        int layer;
-        if (!inPinnedWindowingMode()) {
-            layer = getPrefixOrderIndex();
-        } else {
-            // Root pinned tasks have animations take place within themselves rather than an
-            // animation layer so we need to preserve the order relative to the root task (e.g.
-            // the order of our task/parent).
-            layer = getParent().getPrefixOrderIndex();
-        }
-
-        if (mNeedsZBoost) {
-            layer += Z_BOOST_BASE;
-        }
-        return layer;
-    }
-
     @Override
-    public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) {
-        t.setLayer(leash, getAnimationLayer());
-        getDisplayContent().assignRootTaskOrdering();
+    void resetSurfacePositionForAnimationLeash(SurfaceControl.Transaction t) {
+        // Noop as Activity may be offset for letterbox
     }
 
     @Override
@@ -6914,7 +6799,7 @@
 
             // Crop to root task bounds.
             t.setLayer(leash, 0);
-            t.setLayer(mAnimationBoundsLayer, getAnimationLayer());
+            t.setLayer(mAnimationBoundsLayer, getLastLayer());
 
             // Reparent leash to animation bounds layer.
             t.reparent(leash, mAnimationBoundsLayer);
@@ -8618,7 +8503,7 @@
             } else {
                 lifecycleItem = PauseActivityItem.obtain();
             }
-            final ClientTransaction transaction = ClientTransaction.obtain(app.getThread(), appToken);
+            final ClientTransaction transaction = ClientTransaction.obtain(app.getThread(), token);
             transaction.addCallback(callbackItem);
             transaction.setLifecycleStateRequest(lifecycleItem);
             mAtmService.getLifecycleManager().scheduleTransaction(transaction);
@@ -8691,7 +8576,7 @@
         // The process will be killed until the activity reports stopped with saved state (see
         // {@link ActivityTaskManagerService.activityStopped}).
         try {
-            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
+            mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), token,
                     StopActivityItem.obtain(0 /* configChanges */));
         } catch (RemoteException e) {
             Slog.w(TAG, "Exception thrown during restart " + this, e);
@@ -9058,7 +8943,7 @@
     }
 
     void writeNameToProto(ProtoOutputStream proto, long fieldId) {
-        proto.write(fieldId, appToken.getName());
+        proto.write(fieldId, shortComponentName);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index b71ad2e..210b0ae 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -470,7 +470,7 @@
                         if (started != null && started.getUid() == filterCallingUid) {
                             // Only the started activity which has the same uid as the source caller
                             // can be the caller of next activity.
-                            resultTo = started.appToken;
+                            resultTo = started.token;
                         } else {
                             resultTo = sourceResultTo;
                             // Different apps not adjacent to the caller are forced to be new task.
@@ -501,6 +501,8 @@
     int startActivityInTaskFragment(@NonNull TaskFragment taskFragment,
             @NonNull Intent activityIntent, @Nullable Bundle activityOptions,
             @Nullable IBinder resultTo) {
+        final ActivityRecord caller =
+                resultTo != null ? ActivityRecord.forTokenLocked(resultTo) : null;
         return obtainStarter(activityIntent, "startActivityInTaskFragment")
                 .setActivityOptions(activityOptions)
                 .setInTaskFragment(taskFragment)
@@ -508,6 +510,7 @@
                 .setRequestCode(-1)
                 .setCallingUid(Binder.getCallingUid())
                 .setCallingPid(Binder.getCallingPid())
+                .setUserId(caller != null ? caller.mUserId : mService.getCurrentUserId())
                 .execute();
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
index 979cea9..223f0be 100644
--- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
+++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
@@ -51,6 +51,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.BlockedAppActivity;
@@ -178,7 +179,33 @@
             // before issuing the work challenge.
             return true;
         }
-        return interceptLockedManagedProfileIfNeeded();
+        if (interceptLockedManagedProfileIfNeeded()) {
+            return true;
+        }
+
+        final SparseArray<ActivityInterceptorCallback> callbacks =
+                mService.getActivityInterceptorCallbacks();
+        final ActivityInterceptorCallback.ActivityInterceptorInfo interceptorInfo =
+                new ActivityInterceptorCallback.ActivityInterceptorInfo(mRealCallingUid,
+                        mRealCallingPid, mUserId, mCallingPackage, mCallingFeatureId, mIntent,
+                        mRInfo, mAInfo, mResolvedType, mCallingPid, mCallingUid,
+                        mActivityOptions);
+
+        for (int i = 0; i < callbacks.size(); i++) {
+            final ActivityInterceptorCallback callback = callbacks.valueAt(i);
+            final Intent newIntent = callback.intercept(interceptorInfo);
+            if (newIntent == null) {
+                continue;
+            }
+            mIntent = newIntent;
+            mCallingPid = mRealCallingPid;
+            mCallingUid = mRealCallingUid;
+            mRInfo = mSupervisor.resolveIntent(mIntent, null, mUserId, 0, mRealCallingUid);
+            mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags,
+                    null /*profilerInfo*/);
+            return true;
+        }
+        return false;
     }
 
     private boolean hasCrossProfileAnimation() {
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 93910f3..d944c58 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2013,7 +2013,7 @@
 
         // Allowing the embedding if the task is owned by system.
         final int hostUid = hostTask.effectiveUid;
-        if (hostUid == Process.SYSTEM_UID) {
+        if (UserHandle.getAppId(hostUid) == Process.SYSTEM_UID) {
             return true;
         }
 
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index 9db13ba..6457672 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -37,6 +37,7 @@
 import android.util.proto.ProtoOutputStream;
 import android.window.TaskSnapshot;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IVoiceInteractor;
 import com.android.server.am.PendingIntentRecord;
 import com.android.server.am.UserState;
@@ -124,35 +125,6 @@
     }
 
     /**
-     * Sleep tokens cause the activity manager to put the top activity to sleep.
-     * They are used by components such as dreams that may hide and block interaction
-     * with underlying activities.
-     * The Acquirer provides an interface that encapsulates the underlying work, so the user does
-     * not need to handle the token by him/herself.
-     */
-    public interface SleepTokenAcquirer {
-
-        /**
-         * Acquires a sleep token.
-         * @param displayId The display to apply to.
-         */
-        void acquire(int displayId);
-
-        /**
-         * Releases the sleep token.
-         * @param displayId The display to apply to.
-         */
-        void release(int displayId);
-    }
-
-    /**
-     * Creates a sleep token acquirer for the specified display with the specified tag.
-     *
-     * @param tag A string identifying the purpose (eg. "Dream").
-     */
-    public abstract SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag);
-
-    /**
      * Returns home activity for the specified user.
      *
      * @param userId ID of the user or {@link android.os.UserHandle#USER_ALL}
@@ -264,7 +236,7 @@
 
     /**
      * Set focus on an activity.
-     * @param token The IApplicationToken for the activity
+     * @param token The activity token.
      */
     public abstract void setFocusedActivity(IBinder token);
 
@@ -631,7 +603,8 @@
         @Nullable
         public final LocaleList mLocales;
 
-        PackageConfig(Integer nightMode, LocaleList locales) {
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public PackageConfig(Integer nightMode, LocaleList locales) {
             mNightMode = nightMode;
             mLocales = locales;
         }
@@ -678,4 +651,12 @@
 
     /** Called when the device is waking up */
     public abstract void notifyWakingUp();
+
+    /**
+     * Registers a callback which can intercept activity starts.
+     * @throws IllegalArgumentException if duplicate ids are provided
+     */
+    public abstract void registerActivityStartInterceptor(
+            @ActivityInterceptorCallback.OrderedId int id,
+            ActivityInterceptorCallback callback);
 }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 41928fc..0865a4f 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -92,6 +92,8 @@
 import static com.android.server.am.ActivityManagerServiceDumpProcessesProto.ScreenCompatPackage.PACKAGE;
 import static com.android.server.am.EventLogTags.writeBootProgressEnableScreen;
 import static com.android.server.am.EventLogTags.writeConfigurationChanged;
+import static com.android.server.wm.ActivityInterceptorCallback.FIRST_ORDERED_ID;
+import static com.android.server.wm.ActivityInterceptorCallback.LAST_ORDERED_ID;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ALL;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_ROOT_TASK;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_SWITCH;
@@ -457,6 +459,8 @@
     /** The controller for all operations related to locktask. */
     private LockTaskController mLockTaskController;
     private ActivityStartController mActivityStartController;
+    private SparseArray<ActivityInterceptorCallback> mActivityInterceptorCallbacks =
+            new SparseArray<>();
     PackageConfigPersister mPackageConfigPersister;
 
     boolean mSuppressResizeConfigChanges;
@@ -986,6 +990,7 @@
         synchronized (mGlobalLock) {
             mWindowManager = wm;
             mRootWindowContainer = wm.mRoot;
+            mWindowOrganizerController.setWindowManager(wm);
             mTempConfig.setToDefaults();
             mTempConfig.setLocales(LocaleList.getDefault());
             mConfigurationSeq = mTempConfig.seq = 1;
@@ -1114,6 +1119,10 @@
         return mBackgroundActivityStartCallback;
     }
 
+    SparseArray<ActivityInterceptorCallback> getActivityInterceptorCallbacks() {
+        return mActivityInterceptorCallbacks;
+    }
+
     private void start() {
         LocalServices.addService(ActivityTaskManagerInternal.class, mInternal);
     }
@@ -1341,7 +1350,7 @@
                     .setCaller(r.app.getThread())
                     .setResolvedType(r.resolvedType)
                     .setActivityInfo(aInfo)
-                    .setResultTo(resultTo != null ? resultTo.appToken : null)
+                    .setResultTo(resultTo != null ? resultTo.token : null)
                     .setResultWho(resultWho)
                     .setRequestCode(requestCode)
                     .setCallingPid(-1)
@@ -3185,7 +3194,7 @@
                 mViSessionId++;
             }
             try {
-                activity.app.getThread().requestAssistContextExtras(activity.appToken, pae,
+                activity.app.getThread().requestAssistContextExtras(activity.token, pae,
                         requestType, mViSessionId, flags);
                 mPendingAssistExtras.add(pae);
                 mUiHandler.postDelayed(pae, timeout);
@@ -3414,9 +3423,15 @@
                 final List<RemoteAction> actions = r.pictureInPictureArgs.getActions();
                 mRootWindowContainer.moveActivityToPinnedRootTask(
                         r, "enterPictureInPictureMode");
-                final Task rootTask = r.getRootTask();
-                rootTask.setPictureInPictureAspectRatio(aspectRatio);
-                rootTask.setPictureInPictureActions(actions);
+                final Task task = r.getTask();
+                task.setPictureInPictureAspectRatio(aspectRatio);
+                task.setPictureInPictureActions(actions);
+
+                // Continue the pausing process after entering pip.
+                if (task.getPausingActivity() == r) {
+                    task.schedulePauseActivity(r, false /* userLeaving */,
+                            false /* pauseImmediately */, "auto-pip");
+                }
             }
         };
 
@@ -3424,7 +3439,7 @@
             // If the keyguard is showing or occluded, then try and dismiss it before
             // entering picture-in-picture (this will prompt the user to authenticate if the
             // device is currently locked).
-            mActivityClientController.dismissKeyguard(r.appToken, new KeyguardDismissCallback() {
+            mActivityClientController.dismissKeyguard(r.token, new KeyguardDismissCallback() {
                 @Override
                 public void onDismissSucceeded() {
                     mH.post(enterPipRunnable);
@@ -3995,7 +4010,7 @@
             // to write to the file descriptor directly
             pw.flush();
             try (TransferPipe tp = new TransferPipe()) {
-                appThread.dumpActivity(tp.getWriteFd(), r.appToken, innerPrefix, args);
+                appThread.dumpActivity(tp.getWriteFd(), r.token, innerPrefix, args);
                 tp.go(fd);
             } catch (IOException e) {
                 pw.println(innerPrefix + "Failure while dumping the activity: " + e);
@@ -4517,17 +4532,25 @@
                 reason);
     }
 
-    final class SleepTokenAcquirerImpl implements ActivityTaskManagerInternal.SleepTokenAcquirer {
+    /**
+     * Sleep tokens cause the activity manager to put the top activity to sleep.
+     * They are used by components such as dreams that may hide and block interaction
+     * with underlying activities.
+     */
+    final class SleepTokenAcquirer {
         private final String mTag;
         private final SparseArray<RootWindowContainer.SleepToken> mSleepTokens =
                 new SparseArray<>();
 
-        SleepTokenAcquirerImpl(@NonNull String tag) {
+        SleepTokenAcquirer(@NonNull String tag) {
             mTag = tag;
         }
 
-        @Override
-        public void acquire(int displayId) {
+        /**
+         * Acquires a sleep token.
+         * @param displayId The display to apply to.
+         */
+        void acquire(int displayId) {
             synchronized (mGlobalLock) {
                 if (!mSleepTokens.contains(displayId)) {
                     mSleepTokens.append(displayId,
@@ -4537,8 +4560,11 @@
             }
         }
 
-        @Override
-        public void release(int displayId) {
+        /**
+         * Releases the sleep token.
+         * @param displayId The display to apply to.
+         */
+        void release(int displayId) {
             synchronized (mGlobalLock) {
                 final RootWindowContainer.SleepToken token = mSleepTokens.get(displayId);
                 if (token != null) {
@@ -4624,7 +4650,7 @@
 
         final Message m = PooledLambda.obtainMessage(
                 ActivityManagerInternal::updateActivityUsageStats, mAmInternal,
-                activity.mActivityComponent, activity.mUserId, event, activity.appToken, taskRoot);
+                activity.mActivityComponent, activity.mUserId, event, activity.token, taskRoot);
         mH.sendMessage(m);
     }
 
@@ -4737,7 +4763,7 @@
                                     mContext.getText(R.string.heavy_weight_notification_detail))
                             .setContentIntent(PendingIntent.getActivityAsUser(mContext, 0,
                                     intent, PendingIntent.FLAG_CANCEL_CURRENT
-                                    | PendingIntent.FLAG_IMMUTABLE, null,
+                                            | PendingIntent.FLAG_IMMUTABLE, null,
                                     new UserHandle(userId)))
                             .build();
             try {
@@ -5211,12 +5237,6 @@
 
     final class LocalService extends ActivityTaskManagerInternal {
         @Override
-        public SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag) {
-            Objects.requireNonNull(tag);
-            return new SleepTokenAcquirerImpl(tag);
-        }
-
-        @Override
         public ComponentName getHomeActivityForUser(int userId) {
             synchronized (mGlobalLock) {
                 final ActivityRecord homeActivity =
@@ -5732,7 +5752,7 @@
                             + activity);
                     return null;
                 }
-                return new ActivityTokens(activity.appToken, activity.assistToken,
+                return new ActivityTokens(activity.token, activity.assistToken,
                         activity.app.getThread(), activity.shareableActivityToken);
             }
         }
@@ -6577,5 +6597,22 @@
             getTransitionController().requestTransitionIfNeeded(TRANSIT_WAKE, 0 /* flags */,
                     null /* trigger */, mRootWindowContainer.getDefaultDisplay());
         }
+
+        @Override
+        public void registerActivityStartInterceptor(
+                @ActivityInterceptorCallback.OrderedId int id,
+                ActivityInterceptorCallback callback) {
+            synchronized (mGlobalLock) {
+                if (mActivityInterceptorCallbacks.contains(id)) {
+                    throw new IllegalArgumentException("Duplicate id provided: " + id);
+                }
+                if (id > LAST_ORDERED_ID || id < FIRST_ORDERED_ID) {
+                    throw new IllegalArgumentException(
+                            "Provided id " + id + " is not in range of valid ids ["
+                                    + FIRST_ORDERED_ID + "," + LAST_ORDERED_ID + "]");
+                }
+                mActivityInterceptorCallbacks.put(id, callback);
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index ba30592..11b685e 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -850,7 +850,7 @@
 
                 // Create activity launch transaction.
                 final ClientTransaction clientTransaction = ClientTransaction.obtain(
-                        proc.getThread(), r.appToken);
+                        proc.getThread(), r.token);
 
                 final boolean isTransitionForward = r.isTransitionForward();
                 clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
diff --git a/services/core/java/com/android/server/wm/AnimationAdapter.java b/services/core/java/com/android/server/wm/AnimationAdapter.java
index 529c4f6..5899a4e 100644
--- a/services/core/java/com/android/server/wm/AnimationAdapter.java
+++ b/services/core/java/com/android/server/wm/AnimationAdapter.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import android.annotation.NonNull;
 import android.util.proto.ProtoOutputStream;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
@@ -52,7 +53,7 @@
      * @param finishCallback The callback to be invoked when the animation has finished.
      */
     void startAnimation(SurfaceControl animationLeash, Transaction t, @AnimationType int type,
-            OnAnimationFinishedCallback finishCallback);
+            @NonNull OnAnimationFinishedCallback finishCallback);
 
     /**
      * Called when the animation that was started with {@link #startAnimation} was cancelled by the
diff --git a/services/core/java/com/android/server/wm/AnrController.java b/services/core/java/com/android/server/wm/AnrController.java
index bf66107..38e1c99 100644
--- a/services/core/java/com/android/server/wm/AnrController.java
+++ b/services/core/java/com/android/server/wm/AnrController.java
@@ -32,7 +32,6 @@
 
 import com.android.server.am.ActivityManagerService;
 import com.android.server.am.CriticalEventLog;
-import com.android.server.wm.EmbeddedWindowController.EmbeddedWindow;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -82,21 +81,19 @@
         final boolean aboveSystem;
         final ActivityRecord activity;
         synchronized (mService.mGlobalLock) {
-            WindowState windowState = mService.mInputToWindowMap.get(inputToken);
-            if (windowState != null) {
-                pid = windowState.mSession.mPid;
-                activity = windowState.mActivityRecord;
-                Slog.i(TAG_WM, "ANR in " + windowState.mAttrs.getTitle() + ". Reason:" + reason);
-            } else {
-                EmbeddedWindow embeddedWindow = mService.mEmbeddedWindowController.get(inputToken);
-                if (embeddedWindow == null) {
-                    Slog.e(TAG_WM, "Unknown token, dropping notifyConnectionUnresponsive request");
-                    return;
-                }
-                pid = embeddedWindow.mOwnerPid;
-                windowState = embeddedWindow.mHostWindowState;
-                activity = null; // Don't blame the host process, instead blame the embedded pid.
+            InputTarget target = mService.getInputTargetFromToken(inputToken);
+            if (target == null) {
+                Slog.e(TAG_WM, "Unknown token, dropping notifyConnectionUnresponsive request");
+                return;
             }
+
+            WindowState windowState = target.getWindowState();
+            pid = target.getPid();
+            // Blame the activity if the input token belongs to the window. If the target is
+            // embedded, then we will blame the pid instead.
+            activity = (windowState.mInputChannelToken == inputToken)
+                    ? windowState.mActivityRecord : null;
+            Slog.i(TAG_WM, "ANR in " + target + ". Reason:" + reason);
             aboveSystem = isWindowAboveSystem(windowState);
             dumpAnrStateLocked(activity, windowState, reason);
         }
@@ -110,19 +107,12 @@
     void notifyWindowResponsive(IBinder inputToken) {
         final int pid;
         synchronized (mService.mGlobalLock) {
-            WindowState windowState = mService.mInputToWindowMap.get(inputToken);
-            if (windowState != null) {
-                pid = windowState.mSession.mPid;
-            } else {
-                // Check if the token belongs to an embedded window.
-                EmbeddedWindow embeddedWindow = mService.mEmbeddedWindowController.get(inputToken);
-                if (embeddedWindow == null) {
-                    Slog.e(TAG_WM,
-                            "Unknown token, dropping notifyWindowConnectionResponsive request");
-                    return;
-                }
-                pid = embeddedWindow.mOwnerPid;
+            InputTarget target = mService.getInputTargetFromToken(inputToken);
+            if (target == null) {
+                Slog.e(TAG_WM, "Unknown token, dropping notifyWindowConnectionResponsive request");
+                return;
             }
+            pid = target.getPid();
         }
         mService.mAmInternal.inputDispatchingResumed(pid);
     }
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index ffaf710..535a061 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -124,6 +124,7 @@
     @interface TransitContainerType {}
 
     private final ArrayMap<WindowContainer, Integer> mTempTransitionReasons = new ArrayMap<>();
+    private final ArrayList<WindowContainer> mTempTransitionWindows = new ArrayList<>();
 
     AppTransitionController(WindowManagerService service, DisplayContent displayContent) {
         mService = service;
@@ -523,26 +524,44 @@
         }
     }
 
+    private boolean transitionMayContainNonAppWindows(@TransitionOldType int transit) {
+        // We don't want to have the client to animate any non-app windows.
+        // Having {@code transit} of those types doesn't mean it will contain non-app windows, but
+        // non-app windows will only be included with those transition types. And we don't currently
+        // have any use case of those for TaskFragment transition.
+        // @see NonAppWindowAnimationAdapter#startNonAppWindowAnimations
+        if (transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY
+                || transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER
+                || transit == TRANSIT_OLD_TASK_OPEN || transit == TRANSIT_OLD_TASK_TO_FRONT
+                || transit == TRANSIT_OLD_WALLPAPER_CLOSE) {
+            return true;
+        }
+
+        // Check if the wallpaper is going to participate in the transition. We don't want to have
+        // the client to animate the wallpaper windows.
+        // @see WallpaperAnimationAdapter#startWallpaperAnimations
+        return mDisplayContent.mWallpaperController.isWallpaperVisible();
+    }
+
     /**
-     * Overrides the pending transition with the remote animation defined by the
-     * {@link ITaskFragmentOrganizer} if all windows in the transition are children of
-     * {@link TaskFragment} that are organized by the same organizer.
-     *
-     * @return {@code true} if the transition is overridden.
+     * Finds the common {@link android.window.TaskFragmentOrganizer} that organizes all app windows
+     * in the current transition.
+     * @return {@code null} if there is no such organizer, or if there are more than one.
      */
-    private boolean overrideWithTaskFragmentRemoteAnimation(@TransitionOldType int transit,
-            ArraySet<Integer> activityTypes) {
-        final ArrayList<WindowContainer> allWindows = new ArrayList<>();
-        allWindows.addAll(mDisplayContent.mClosingApps);
-        allWindows.addAll(mDisplayContent.mOpeningApps);
-        allWindows.addAll(mDisplayContent.mChangingContainers);
+    @Nullable
+    private ITaskFragmentOrganizer findTaskFragmentOrganizerForAllWindows() {
+        mTempTransitionWindows.clear();
+        mTempTransitionWindows.addAll(mDisplayContent.mClosingApps);
+        mTempTransitionWindows.addAll(mDisplayContent.mOpeningApps);
+        mTempTransitionWindows.addAll(mDisplayContent.mChangingContainers);
 
         // It should only animated by the organizer if all windows are below the same leaf Task.
         Task leafTask = null;
-        for (int i = allWindows.size() - 1; i >= 0; i--) {
-            final ActivityRecord r = getAppFromContainer(allWindows.get(i));
+        for (int i = mTempTransitionWindows.size() - 1; i >= 0; i--) {
+            final ActivityRecord r = getAppFromContainer(mTempTransitionWindows.get(i));
             if (r == null) {
-                return false;
+                leafTask = null;
+                break;
             }
             // The activity may be a child of embedded Task, but we want to find the owner Task.
             // As a result, find the organized TaskFragment first.
@@ -561,26 +580,31 @@
                     ? organizedTaskFragment.getTask()
                     : r.getTask();
             if (task == null) {
-                return false;
+                leafTask = null;
+                break;
             }
             // We don't want the organizer to handle transition of other non-embedded Task.
             if (leafTask != null && leafTask != task) {
-                return false;
+                leafTask = null;
+                break;
             }
             final ActivityRecord rootActivity = task.getRootActivity();
             // We don't want the organizer to handle transition when the whole app is closing.
             if (rootActivity == null) {
-                return false;
+                leafTask = null;
+                break;
             }
             // We don't want the organizer to handle transition of non-embedded activity of other
             // app.
             if (r.getUid() != rootActivity.getUid() && !r.isEmbedded()) {
-                return false;
+                leafTask = null;
+                break;
             }
             leafTask = task;
         }
+        mTempTransitionWindows.clear();
         if (leafTask == null) {
-            return false;
+            return null;
         }
 
         // We don't support remote animation for Task with multiple TaskFragmentOrganizers.
@@ -599,12 +623,28 @@
         if (hasMultipleOrganizers) {
             ProtoLog.e(WM_DEBUG_APP_TRANSITIONS, "We don't support remote animation for"
                     + " Task with multiple TaskFragmentOrganizers.");
+            return null;
+        }
+        return organizer[0];
+    }
+
+    /**
+     * Overrides the pending transition with the remote animation defined by the
+     * {@link ITaskFragmentOrganizer} if all windows in the transition are children of
+     * {@link TaskFragment} that are organized by the same organizer.
+     *
+     * @return {@code true} if the transition is overridden.
+     */
+    private boolean overrideWithTaskFragmentRemoteAnimation(@TransitionOldType int transit,
+            ArraySet<Integer> activityTypes) {
+        if (transitionMayContainNonAppWindows(transit)) {
             return false;
         }
 
-        final RemoteAnimationDefinition definition = organizer[0] != null
+        final ITaskFragmentOrganizer organizer = findTaskFragmentOrganizerForAllWindows();
+        final RemoteAnimationDefinition definition = organizer != null
                 ? mDisplayContent.mAtmService.mTaskFragmentOrganizerController
-                    .getRemoteAnimationDefinition(organizer[0])
+                    .getRemoteAnimationDefinition(organizer)
                 : null;
         final RemoteAnimationAdapter adapter = definition != null
                 ? definition.getAdapter(transit, activityTypes)
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index 99f6fd4..98757f0 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -332,7 +332,7 @@
     }
 
     @Override
-    boolean forAllTaskDisplayAreas(Function<TaskDisplayArea, Boolean> callback,
+    boolean forAllTaskDisplayAreas(Predicate<TaskDisplayArea> callback,
             boolean traverseTopToBottom) {
         // Only DisplayArea of Type.ANY may contain TaskDisplayArea as children.
         if (mType != DisplayArea.Type.ANY) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 31126f5..4bfcdd1 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -81,7 +81,6 @@
 import static android.window.DisplayAreaOrganizer.FEATURE_ROOT;
 import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION;
 
-import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BOOT;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS;
@@ -90,6 +89,7 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_LAYER_MIRRORING;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SCREEN_ON;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WALLPAPER;
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_CONFIG;
@@ -127,7 +127,6 @@
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
 import static com.android.server.wm.WindowManagerDebugConfig.SHOW_STACK_CRAWLS;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -223,7 +222,6 @@
 import com.android.internal.util.ToBooleanFunction;
 import com.android.internal.util.function.TriConsumer;
 import com.android.internal.util.function.pooled.PooledConsumer;
-import com.android.internal.util.function.pooled.PooledFunction;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.internal.util.function.pooled.PooledPredicate;
 import com.android.server.inputmethod.InputMethodManagerInternal;
@@ -467,12 +465,6 @@
     @VisibleForTesting
     boolean isDefaultDisplay;
 
-    /**
-     * Flag indicating whether WindowManager should override info for this display in
-     * DisplayManager.
-     */
-    boolean mShouldOverrideDisplayConfiguration = true;
-
     /** Window tokens that are in the process of exiting, but still on screen for animations. */
     final ArrayList<WindowToken> mExitingTokens = new ArrayList<>();
 
@@ -585,7 +577,7 @@
     /** Caches the value whether told display manager that we have content. */
     private boolean mLastHasContent;
 
-    private DisplayRotationUtil mRotationUtil = new DisplayRotationUtil();
+    private static DisplayRotationUtil sRotationUtil = new DisplayRotationUtil();
 
     /**
      * The input method window for this display.
@@ -655,11 +647,6 @@
     private final InsetsStateController mInsetsStateController;
     private final InsetsPolicy mInsetsPolicy;
 
-    /** @see #getParentWindow() */
-    private WindowState mParentWindow;
-
-    private Point mLocationInParentWindow = new Point();
-
     /** Corner radius that windows should have in order to match the display. */
     private final float mWindowCornerRadius;
 
@@ -680,7 +667,7 @@
     /** All tokens used to put activities on this root task to sleep (including mOffToken) */
     final ArrayList<RootWindowContainer.SleepToken> mAllSleepTokens = new ArrayList<>();
     /** The token acquirer to put root tasks on the display to sleep */
-    private final ActivityTaskManagerInternal.SleepTokenAcquirer mOffTokenAcquirer;
+    private final ActivityTaskManagerService.SleepTokenAcquirer mOffTokenAcquirer;
 
     private boolean mSleeping;
 
@@ -712,8 +699,6 @@
     // well and thus won't change the top resumed / focused record
     boolean mDontMoveToTop;
 
-    private final ArrayList<ActivityRecord> mTmpActivityList = new ArrayList<>();
-
     private final Consumer<WindowState> mUpdateWindowsForAnimator = w -> {
         WindowStateAnimator winAnimator = w.mWinAnimator;
         final ActivityRecord activity = w.mActivityRecord;
@@ -860,7 +845,6 @@
                     w.updateLastFrames();
                 }
                 w.onResizeHandled();
-                w.updateLocationInParentDisplayIfNeeded();
             }
 
             if (w.mActivityRecord != null) {
@@ -998,8 +982,8 @@
             final boolean committed = winAnimator.commitFinishDrawingLocked();
             if (isDefaultDisplay && committed) {
                 if (w.hasWallpaper()) {
-                    if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG,
-                            "First draw done in potential wallpaper target " + w);
+                    ProtoLog.v(WM_DEBUG_WALLPAPER,
+                            "First draw done in potential wallpaper target %s", w);
                     mWallpaperMayChange = true;
                     pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
                     if (DEBUG_LAYOUT_REPEATS) {
@@ -1067,7 +1051,7 @@
 
         final InputChannel inputChannel = mWmService.mInputManager.monitorInput(
                 "PointerEventDispatcher" + mDisplayId, mDisplayId);
-        mPointerEventDispatcher = new PointerEventDispatcher(inputChannel, this);
+        mPointerEventDispatcher = new PointerEventDispatcher(inputChannel);
 
         // Tap Listeners are supported for:
         // 1. All physical displays (multi-display).
@@ -1661,7 +1645,7 @@
     }
 
     /** Returns {@code true} if the IME is possible to show on the launching activity. */
-    private boolean mayImeShowOnLaunchingActivity(@NonNull ActivityRecord r) {
+    boolean mayImeShowOnLaunchingActivity(@NonNull ActivityRecord r) {
         final WindowState win = r.findMainWindow();
         if (win == null) {
             return false;
@@ -2023,14 +2007,8 @@
         computeSizeRangesAndScreenLayout(mDisplayInfo, rotated, uiMode, dw, dh,
                 mDisplayMetrics.density, outConfig);
 
-        // We usually set the override info in DisplayManager so that we get consistent display
-        // metrics values when displays are changing and don't send out new values until WM is aware
-        // of them. However, we don't do this for displays that serve as containers for ActivityView
-        // because we don't want letter-/pillar-boxing during resize.
-        final DisplayInfo overrideDisplayInfo = mShouldOverrideDisplayConfiguration
-                ? mDisplayInfo : null;
         mWmService.mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(mDisplayId,
-                overrideDisplayInfo);
+                mDisplayInfo);
 
         mBaseDisplayRect.set(0, 0, dw, dh);
 
@@ -2048,29 +2026,35 @@
         return mDisplayCutoutCache.getOrCompute(mInitialDisplayCutout, rotation);
     }
 
-    private WmDisplayCutout calculateDisplayCutoutForRotationUncached(
-            DisplayCutout cutout, int rotation) {
+    static WmDisplayCutout calculateDisplayCutoutForRotationAndDisplaySizeUncached(
+            DisplayCutout cutout, int rotation, int displayWidth, int displayHeight) {
         if (cutout == null || cutout == DisplayCutout.NO_CUTOUT) {
             return WmDisplayCutout.NO_CUTOUT;
         }
         if (rotation == ROTATION_0) {
             return WmDisplayCutout.computeSafeInsets(
-                    cutout, mInitialDisplayWidth, mInitialDisplayHeight);
+                    cutout, displayWidth, displayHeight);
         }
         final Insets waterfallInsets =
                 RotationUtils.rotateInsets(cutout.getWaterfallInsets(), rotation);
         final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270);
-        final Rect[] newBounds = mRotationUtil.getRotatedBounds(
+        final Rect[] newBounds = sRotationUtil.getRotatedBounds(
                 cutout.getBoundingRectsAll(),
-                rotation, mInitialDisplayWidth, mInitialDisplayHeight);
+                rotation, displayWidth, displayHeight);
         final CutoutPathParserInfo info = cutout.getCutoutPathParserInfo();
         final CutoutPathParserInfo newInfo = new CutoutPathParserInfo(
                 info.getDisplayWidth(), info.getDisplayHeight(), info.getDensity(),
                 info.getCutoutSpec(), rotation, info.getScale());
         return WmDisplayCutout.computeSafeInsets(
                 DisplayCutout.constructDisplayCutout(newBounds, waterfallInsets, newInfo),
-                rotated ? mInitialDisplayHeight : mInitialDisplayWidth,
-                rotated ? mInitialDisplayWidth : mInitialDisplayHeight);
+                rotated ? displayHeight : displayWidth,
+                rotated ? displayWidth : displayHeight);
+    }
+
+    private WmDisplayCutout calculateDisplayCutoutForRotationUncached(
+            DisplayCutout cutout, int rotation) {
+        return calculateDisplayCutoutForRotationAndDisplaySizeUncached(cutout, rotation,
+                mInitialDisplayWidth, mInitialDisplayHeight);
     }
 
     RoundedCorners calculateRoundedCornersForRotation(int rotation) {
@@ -3075,9 +3059,6 @@
     void removeImmediately() {
         mDeferredRemoval = false;
         try {
-            if (mParentWindow != null) {
-                mParentWindow.removeEmbeddedDisplayContent(this);
-            }
             // Clear all transitions & screen frozen states when removing display.
             mOpeningApps.clear();
             mClosingApps.clear();
@@ -3094,6 +3075,7 @@
             mOverlayLayer.release();
             mInputMonitor.onDisplayRemoved();
             mWmService.mDisplayNotificationController.dispatchDisplayRemoved(this);
+            mWmService.mAccessibilityController.onDisplayRemoved(mDisplayId);
         } finally {
             mDisplayReady = false;
         }
@@ -3804,7 +3786,11 @@
     }
 
     boolean shouldImeAttachedToApp() {
-        return isImeControlledByApp()
+        // Force attaching IME to the display when magnifying, or it would be magnified with
+        // target app together.
+        final boolean allowAttachToApp = (mMagnificationSpec == null);
+
+        return allowAttachToApp && isImeControlledByApp()
                 && mImeLayeringTarget != null
                 && mImeLayeringTarget.mActivityRecord != null
                 && mImeLayeringTarget.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
@@ -3829,8 +3815,7 @@
 
     /**
      * Finds the window which can host IME if IME target cannot host it.
-     * e.g. IME target cannot host IME when it's display has a parent display OR when display
-     * doesn't support IME/system decorations.
+     * e.g. IME target cannot host IME  when display doesn't support IME/system decorations.
      *
      * @param target current IME target.
      * @return {@link InsetsControlTarget} that can host IME.
@@ -4143,14 +4128,10 @@
      */
     @VisibleForTesting
     SurfaceControl computeImeParent() {
-        // Force attaching IME to the display when magnifying, or it would be magnified with
-        // target app together.
-        final boolean allowAttachToApp = (mMagnificationSpec == null);
-
         // Attach it to app if the target is part of an app and such app is covering the entire
         // screen. If it's not covering the entire screen the IME might extend beyond the apps
         // bounds.
-        if (allowAttachToApp && shouldImeAttachedToApp()) {
+        if (shouldImeAttachedToApp()) {
             if (mImeLayeringTarget.mActivityRecord != mImeInputTarget.mActivityRecord) {
                 // Do not change parent if the window hasn't requested IME.
                 return null;
@@ -4596,14 +4577,6 @@
         for (int i = mExitingTokens.size() - 1; i >= 0; i--) {
             mExitingTokens.get(i).hasVisible = hasVisible;
         }
-
-        // Initialize state of exiting applications.
-        forAllRootTasks(task -> {
-            final ArrayList<ActivityRecord> activities = task.mExitingActivities;
-            for (int j = activities.size() - 1; j >= 0; --j) {
-                activities.get(j).hasVisible = hasVisible;
-            }
-        });
     }
 
     void removeExistingTokensIfPossible() {
@@ -4613,34 +4586,6 @@
                 mExitingTokens.remove(i);
             }
         }
-
-        // clear first just in case.
-        mTmpActivityList.clear();
-        // Time to remove any exiting applications?
-        forAllRootTasks(task -> {
-            final ArrayList<ActivityRecord> activities = task.mExitingActivities;
-            for (int j = activities.size() - 1; j >= 0; --j) {
-                final ActivityRecord activity = activities.get(j);
-                if (!activity.hasVisible && !mDisplayContent.mClosingApps.contains(activity)
-                        && (!activity.mIsExiting || activity.isEmpty())) {
-                    mTmpActivityList.add(activity);
-                }
-            }
-        });
-        if (!mTmpActivityList.isEmpty()) {
-            // Make sure there is no animation running on this activity, so any windows
-            // associated with it will be removed as soon as their animations are
-            // complete.
-            cancelAnimation();
-        }
-        for (int i = 0; i < mTmpActivityList.size(); ++i) {
-            final ActivityRecord activity = mTmpActivityList.get(i);
-            ProtoLog.v(WM_DEBUG_ADD_REMOVE,
-                    "performLayout: Activity exiting now removed %s", activity);
-            activity.removeIfPossible();
-        }
-        // Clear afterwards so we don't hold references.
-        mTmpActivityList.clear();
     }
 
     @Override
@@ -4680,7 +4625,7 @@
                 || mWmService.mPolicy.okToAnimate(ignoreScreenOn));
     }
 
-    static final class TaskForResizePointSearchResult {
+    static final class TaskForResizePointSearchResult implements Predicate<Task> {
         private Task taskForResize;
         private int x;
         private int y;
@@ -4693,16 +4638,13 @@
             this.y = y;
             this.delta = delta;
             mTmpRect.setEmpty();
-
-            final PooledFunction f = PooledLambda.obtainFunction(
-                    TaskForResizePointSearchResult::processTask, this, PooledLambda.__(Task.class));
-            root.forAllTasks(f);
-            f.recycle();
+            root.forAllTasks(this);
 
             return taskForResize;
         }
 
-        private boolean processTask(Task task) {
+        @Override
+        public boolean test(Task task) {
             if (!task.getRootTask().getWindowConfiguration().canResizeTask()) {
                 return true;
             }
@@ -5147,9 +5089,7 @@
         onAppTransitionDone();
 
         changes |= FINISH_LAYOUT_REDO_LAYOUT;
-        if (DEBUG_WALLPAPER_LIGHT) {
-            Slog.v(TAG_WM, "Wallpaper layer changed: assigning layers + relayout");
-        }
+        ProtoLog.v(WM_DEBUG_WALLPAPER, "Wallpaper layer changed: assigning layers + relayout");
         computeImeTarget(true /* updateImeTarget */);
         mWallpaperMayChange = true;
         // Since the window list has been rebuilt, focus might have to be recomputed since the
@@ -5184,65 +5124,6 @@
                 && isTrusted();
     }
 
-    /**
-     * Get the window which owns the surface that this DisplayContent is re-parented to.
-     *
-     * @return the parent window.
-     */
-    WindowState getParentWindow() {
-        return mParentWindow;
-    }
-
-    /**
-     * Update the location of this display in the parent window. This enables windows in this
-     * display to compute the global transformation matrix.
-     *
-     * @param win The parent window of this display.
-     * @param x The x coordinate in the parent window.
-     * @param y The y coordinate in the parent window.
-     */
-    void updateLocation(WindowState win, int x, int y) {
-        if (mParentWindow != win) {
-            throw new IllegalArgumentException(
-                    "The given window is not the parent window of this display.");
-        }
-        if (!mLocationInParentWindow.equals(x, y)) {
-            mLocationInParentWindow.set(x, y);
-            if (mWmService.mAccessibilityController.hasCallbacks()) {
-                mWmService.mAccessibilityController.onSomeWindowResizedOrMoved(mDisplayId);
-            }
-            notifyLocationInParentDisplayChanged();
-        }
-    }
-
-    Point getLocationInParentWindow() {
-        return mLocationInParentWindow;
-    }
-
-    Point getLocationInParentDisplay() {
-        final Point location = new Point();
-        if (mParentWindow != null) {
-            // LocationInParentWindow indicates the offset to (0,0) of window, but what we need is
-            // the offset to (0,0) of display.
-            DisplayContent dc = this;
-            do {
-                final WindowState displayParent = dc.getParentWindow();
-                location.x += displayParent.getFrame().left
-                        + (dc.getLocationInParentWindow().x * displayParent.mGlobalScale + 0.5f);
-                location.y += displayParent.getFrame().top
-                        + (dc.getLocationInParentWindow().y * displayParent.mGlobalScale + 0.5f);
-                dc = displayParent.getDisplayContent();
-            } while (dc != null && dc.getParentWindow() != null);
-        }
-        return location;
-    }
-
-    void notifyLocationInParentDisplayChanged() {
-        forAllWindows(w -> {
-            w.updateLocationInParentDisplayIfNeeded();
-        }, false /* traverseTopToBottom */);
-    }
-
     SurfaceControl getWindowingLayer() {
         return mWindowingLayer;
     }
@@ -5511,6 +5392,14 @@
         return mMetricsLogger;
     }
 
+    void acquireScreenOffToken(boolean acquire) {
+        if (acquire) {
+            mOffTokenAcquirer.acquire(mDisplayId);
+        } else {
+            mOffTokenAcquirer.release(mDisplayId);
+        }
+    }
+
     void onDisplayChanged() {
         mDisplay.getRealSize(mTmpDisplaySize);
         setBounds(0, 0, mTmpDisplaySize.x, mTmpDisplaySize.y);
@@ -5521,9 +5410,9 @@
         if (displayId != DEFAULT_DISPLAY) {
             final int displayState = mDisplay.getState();
             if (displayState == Display.STATE_OFF) {
-                mOffTokenAcquirer.acquire(mDisplayId);
+                acquireScreenOffToken(true /* acquire */);
             } else if (displayState == Display.STATE_ON) {
-                mOffTokenAcquirer.release(mDisplayId);
+                acquireScreenOffToken(false /* acquire */);
             }
             ProtoLog.v(WM_DEBUG_LAYER_MIRRORING,
                     "Display %d state is now (%d), so update layer mirroring?",
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 881bd35..2c285cd 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -779,6 +779,10 @@
 
     public void setAwake(boolean awake) {
         mAwake = awake;
+        // The screen off token for non-default display is controlled by DisplayContent.
+        if (mDisplayContent.isDefaultDisplay) {
+            mDisplayContent.acquireScreenOffToken(!awake);
+        }
     }
 
     public boolean isAwake() {
@@ -1547,31 +1551,51 @@
      * some temporal states, but doesn't change the window frames used to show on screen.
      */
     void simulateLayoutDisplay(DisplayFrames displayFrames, SparseArray<Rect> barContentFrames) {
-        if (mNavigationBar != null) {
-            final WindowFrames simulatedWindowFrames = new WindowFrames();
-            if (INSETS_LAYOUT_GENERALIZATION) {
-                simulateLayoutDecorWindow(mNavigationBar, displayFrames, simulatedWindowFrames,
+        if (INSETS_LAYOUT_GENERALIZATION) {
+            final InsetsStateController insetsStateController =
+                    mDisplayContent.getInsetsStateController();
+            for (int type = 0; type < InsetsState.SIZE; type++) {
+                final InsetsSourceProvider provider =
+                        insetsStateController.peekSourceProvider(type);
+                if (provider == null || !provider.hasWindow()
+                        || provider.mWin.getControllableInsetProvider() != provider) {
+                    continue;
+                }
+                final WindowFrames simulatedWindowFrames = new WindowFrames();
+                simulateLayoutDecorWindow(provider.mWin, displayFrames, simulatedWindowFrames,
                         barContentFrames,
                         contentFrame -> simulateLayoutForContentFrame(displayFrames,
-                                mNavigationBar, contentFrame));
-            } else {
+                                provider.mWin, contentFrame));
+            }
+        } else {
+            if (mNavigationBar != null) {
+                final WindowFrames simulatedWindowFrames = new WindowFrames();
                 simulateLayoutDecorWindow(mNavigationBar, displayFrames, simulatedWindowFrames,
                         barContentFrames, contentFrame -> layoutNavigationBar(displayFrames,
                                 contentFrame));
             }
-        }
-        if (mStatusBar != null) {
-            final WindowFrames simulatedWindowFrames = new WindowFrames();
-            if (INSETS_LAYOUT_GENERALIZATION) {
-                simulateLayoutDecorWindow(mStatusBar, displayFrames, simulatedWindowFrames,
-                        barContentFrames,
-                        contentFrame -> simulateLayoutForContentFrame(displayFrames,
-                                mStatusBar, contentFrame));
-            } else {
+            if (mStatusBar != null) {
+                final WindowFrames simulatedWindowFrames = new WindowFrames();
                 simulateLayoutDecorWindow(mStatusBar, displayFrames, simulatedWindowFrames,
                         barContentFrames,
                         contentFrame -> layoutStatusBar(displayFrames, contentFrame));
             }
+            if (mExtraNavBarAlt != null) {
+                // There's no pre-defined behavior for the extra navigation bar, we need to use the
+                // new flexible insets logic anyway.
+                final WindowFrames simulatedWindowFrames = new WindowFrames();
+                simulateLayoutDecorWindow(mExtraNavBarAlt, displayFrames, simulatedWindowFrames,
+                        barContentFrames,
+                        contentFrame -> simulateLayoutForContentFrame(displayFrames,
+                                mExtraNavBarAlt, contentFrame));
+            }
+            if (mClimateBarAlt != null) {
+                final WindowFrames simulatedWindowFrames = new WindowFrames();
+                simulateLayoutDecorWindow(mClimateBarAlt, displayFrames, simulatedWindowFrames,
+                        barContentFrames,
+                        contentFrame -> simulateLayoutForContentFrame(displayFrames,
+                                mClimateBarAlt, contentFrame));
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
index b08d6e1..fc317a1 100644
--- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java
+++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
@@ -127,7 +127,7 @@
         }
     }
 
-    static class EmbeddedWindow {
+    static class EmbeddedWindow implements InputTarget {
         final IWindow mClient;
         @Nullable final WindowState mHostWindowState;
         @Nullable final ActivityRecord mHostActivityRecord;
@@ -166,7 +166,8 @@
             mDisplayId = displayId;
         }
 
-        String getName() {
+        @Override
+        public String toString() {
             final String hostWindowName = (mHostWindowState != null)
                     ? mHostWindowState.getWindowTag().toString() : "Internal";
             return "EmbeddedWindow{ u" + UserHandle.getUserId(mOwnerUid) + " " + hostWindowName
@@ -183,7 +184,7 @@
         }
 
         InputChannel openInputChannel() {
-            final String name = getName();
+            final String name = toString();
             mInputChannel = mWmService.mInputManager.createInputChannel(name);
             return mInputChannel;
         }
@@ -195,5 +196,25 @@
                 mInputChannel = null;
             }
         }
+
+        @Override
+        public WindowState getWindowState() {
+            return mHostWindowState;
+        }
+
+        @Override
+        public int getDisplayId() {
+            return mDisplayId;
+        }
+
+        @Override
+        public IWindow getIWindow() {
+            return mClient;
+        }
+
+        @Override
+        public int getPid() {
+            return mOwnerPid;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/wm/InputTarget.java b/services/core/java/com/android/server/wm/InputTarget.java
new file mode 100644
index 0000000..c7d328a
--- /dev/null
+++ b/services/core/java/com/android/server/wm/InputTarget.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.view.IWindow;
+
+/**
+ * Common interface between focusable objects.
+ *
+ * Both WindowState and EmbeddedWindows can receive input. This consolidates some common properties
+ * of both targets.
+ */
+interface InputTarget {
+    /* Get the WindowState associated with the target. */
+    WindowState getWindowState();
+
+    /* Display id of the target. */
+    int getDisplayId();
+
+    /* Client IWindow for the target. */
+    IWindow getIWindow();
+
+    /* Owning pid of the target. */
+    int getPid();
+}
+
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 403df91..d202587 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -535,7 +535,7 @@
 
         @Override
         public void startAnimation(SurfaceControl animationLeash, Transaction t,
-                @AnimationType int type, OnAnimationFinishedCallback finishCallback) {
+                @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) {
             // TODO(b/166736352): Check if we still need to control the IME visibility here.
             if (mSource.getType() == ITYPE_IME) {
                 // TODO: use 0 alpha and remove t.hide() once b/138459974 is fixed.
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index c630e91..047cbbe 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -80,14 +80,14 @@
     private final SparseArray<KeyguardDisplayState> mDisplayStates = new SparseArray<>();
     private final ActivityTaskManagerService mService;
     private RootWindowContainer mRootWindowContainer;
-    private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer;
+    private final ActivityTaskManagerService.SleepTokenAcquirer mSleepTokenAcquirer;
 
 
     KeyguardController(ActivityTaskManagerService service,
             ActivityTaskSupervisor taskSupervisor) {
         mService = service;
         mTaskSupervisor = taskSupervisor;
-        mSleepTokenAcquirer = mService.new SleepTokenAcquirerImpl(KEYGUARD_SLEEP_TOKEN_TAG);
+        mSleepTokenAcquirer = mService.new SleepTokenAcquirer(KEYGUARD_SLEEP_TOKEN_TAG);
     }
 
     void setWindowManager(WindowManagerService windowManager) {
@@ -194,8 +194,7 @@
 
         if (keyguardChanged) {
             // Irrelevant to AOD.
-            dismissMultiWindowModeForTaskIfNeeded(null /* currentTaskControllsingOcclusion */,
-                    false /* turningScreenOn */);
+            dismissMultiWindowModeForTaskIfNeeded(null /* currentTaskControllsingOcclusion */);
             mKeyguardGoingAway = false;
             if (keyguardShowing) {
                 mDismissalRequested = false;
@@ -396,6 +395,8 @@
                 mService.continueWindowLayout();
             }
         }
+        dismissMultiWindowModeForTaskIfNeeded(topActivity != null
+                ? topActivity.getRootTask() : null);
     }
 
     /**
@@ -421,21 +422,6 @@
         }
     }
 
-    /**
-     * Called when somebody wants to turn screen on.
-     */
-    private void handleTurnScreenOn(int displayId) {
-        if (displayId != DEFAULT_DISPLAY) {
-            return;
-        }
-
-        mTaskSupervisor.wakeUp("handleTurnScreenOn");
-        if (mKeyguardShowing && canDismissKeyguard()) {
-            mWindowManager.dismissKeyguard(null /* callback */, null /* message */);
-            mDismissalRequested = true;
-        }
-    }
-
     boolean isDisplayOccluded(int displayId) {
         return getDisplayState(displayId).mOccluded;
     }
@@ -449,11 +435,9 @@
     }
 
     private void dismissMultiWindowModeForTaskIfNeeded(
-            @Nullable Task currentTaskControllingOcclusion, boolean turningScreenOn) {
-        // If turningScreenOn is true, it means that the visibility state has changed from
-        // currentTaskControllingOcclusion and we should update windowing mode.
+            @Nullable Task currentTaskControllingOcclusion) {
         // TODO(b/113840485): Handle docked stack for individual display.
-        if (!turningScreenOn && (!mKeyguardShowing || !isDisplayOccluded(DEFAULT_DISPLAY))) {
+        if (!mKeyguardShowing || !isDisplayOccluded(DEFAULT_DISPLAY)) {
             return;
         }
 
@@ -521,10 +505,10 @@
 
         private boolean mRequestDismissKeyguard;
         private final ActivityTaskManagerService mService;
-        private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer;
+        private final ActivityTaskManagerService.SleepTokenAcquirer mSleepTokenAcquirer;
 
         KeyguardDisplayState(ActivityTaskManagerService service, int displayId,
-                ActivityTaskManagerInternal.SleepTokenAcquirer acquirer) {
+                ActivityTaskManagerService.SleepTokenAcquirer acquirer) {
             mService = service;
             mDisplayId = displayId;
             mSleepTokenAcquirer = acquirer;
@@ -592,26 +576,17 @@
                     && controller.mWindowManager.isKeyguardSecure(
                     controller.mService.getCurrentUserId());
 
-            boolean occludingChange = false;
-            boolean turningScreenOn = false;
             if (mTopTurnScreenOnActivity != lastTurnScreenOnActivity
                     && mTopTurnScreenOnActivity != null
                     && !mService.mWindowManager.mPowerManager.isInteractive()
-                    && (mRequestDismissKeyguard || occludedByActivity
-                        || controller.canDismissKeyguard())) {
-                turningScreenOn = true;
-                controller.handleTurnScreenOn(mDisplayId);
+                    && (mRequestDismissKeyguard || occludedByActivity)) {
+                controller.mTaskSupervisor.wakeUp("handleTurnScreenOn");
                 mTopTurnScreenOnActivity.setCurrentLaunchCanTurnScreenOn(false);
             }
 
             if (lastOccluded != mOccluded) {
-                occludingChange = true;
                 controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity);
             }
-
-            if (occludingChange || turningScreenOn) {
-                controller.dismissMultiWindowModeForTaskIfNeeded(task, turningScreenOn);
-            }
         }
 
         /**
diff --git a/services/core/java/com/android/server/wm/LocalAnimationAdapter.java b/services/core/java/com/android/server/wm/LocalAnimationAdapter.java
index 520bd8b..a3eb980 100644
--- a/services/core/java/com/android/server/wm/LocalAnimationAdapter.java
+++ b/services/core/java/com/android/server/wm/LocalAnimationAdapter.java
@@ -19,6 +19,7 @@
 import static com.android.server.wm.AnimationAdapterProto.LOCAL;
 import static com.android.server.wm.LocalAnimationAdapterProto.ANIMATION_SPEC;
 
+import android.annotation.NonNull;
 import android.os.SystemClock;
 import android.util.proto.ProtoOutputStream;
 import android.view.SurfaceControl;
@@ -51,7 +52,7 @@
 
     @Override
     public void startAnimation(SurfaceControl animationLeash, Transaction t,
-            @AnimationType int type, OnAnimationFinishedCallback finishCallback) {
+            @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) {
         mAnimator.startAnimation(mSpec, animationLeash, t,
                 () -> finishCallback.onAnimationFinished(type, this));
     }
diff --git a/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java b/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java
index e50dc51..7abf3b8 100644
--- a/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java
+++ b/services/core/java/com/android/server/wm/NavBarFadeAnimationController.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 
+import android.annotation.NonNull;
 import android.view.SurfaceControl;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
@@ -144,7 +145,7 @@
 
         @Override
         public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t,
-                int type, SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
+                int type, @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
             super.startAnimation(animationLeash, t, type, finishCallback);
             if (mParent != null && mParent.isValid()) {
                 t.reparent(animationLeash, mParent);
diff --git a/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java b/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java
index 88941eb..9f28509 100644
--- a/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java
+++ b/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java
@@ -27,6 +27,7 @@
 import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
 
+import android.annotation.NonNull;
 import android.graphics.Rect;
 import android.os.SystemClock;
 import android.util.proto.ProtoOutputStream;
@@ -145,7 +146,7 @@
 
     @Override
     public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t,
-            int type, SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
+            int type, @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
         ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation");
         mCapturedLeash = animationLeash;
         mCapturedLeashFinishCallback = finishCallback;
diff --git a/services/core/java/com/android/server/wm/PointerEventDispatcher.java b/services/core/java/com/android/server/wm/PointerEventDispatcher.java
index 9bc7e93..4f8ec63 100644
--- a/services/core/java/com/android/server/wm/PointerEventDispatcher.java
+++ b/services/core/java/com/android/server/wm/PointerEventDispatcher.java
@@ -16,15 +16,11 @@
 
 package com.android.server.wm;
 
-import static com.android.server.input.InputManagerService.ENABLE_PER_WINDOW_INPUT_ROTATION;
-
-import android.graphics.Point;
 import android.view.InputChannel;
 import android.view.InputDevice;
 import android.view.InputEvent;
 import android.view.InputEventReceiver;
 import android.view.MotionEvent;
-import android.view.Surface;
 import android.view.WindowManagerPolicyConstants.PointerEventListener;
 
 import com.android.server.UiThread;
@@ -35,12 +31,8 @@
     private final ArrayList<PointerEventListener> mListeners = new ArrayList<>();
     private PointerEventListener[] mListenersArray = new PointerEventListener[0];
 
-    private final DisplayContent mDisplayContent;
-    private final Point mTmpSize = new Point();
-
-    public PointerEventDispatcher(InputChannel inputChannel, DisplayContent dc) {
+    public PointerEventDispatcher(InputChannel inputChannel) {
         super(inputChannel, UiThread.getHandler().getLooper());
-        mDisplayContent = dc;
     }
 
     @Override
@@ -49,15 +41,6 @@
             if (event instanceof MotionEvent
                     && (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                 MotionEvent motionEvent = (MotionEvent) event;
-                if (ENABLE_PER_WINDOW_INPUT_ROTATION) {
-                    final int rotation = mDisplayContent.getRotation();
-                    if (rotation != Surface.ROTATION_0) {
-                        mDisplayContent.getDisplay().getRealSize(mTmpSize);
-                        motionEvent = MotionEvent.obtain(motionEvent);
-                        motionEvent.transform(MotionEvent.createRotateMatrix(
-                                rotation, mTmpSize.x, mTmpSize.y));
-                    }
-                }
                 PointerEventListener[] listeners;
                 synchronized (mListeners) {
                     if (mListenersArray == null) {
diff --git a/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java b/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java
index ef8dee4..11a27c5 100644
--- a/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java
+++ b/services/core/java/com/android/server/wm/PossibleDisplayInfoMapper.java
@@ -120,8 +120,15 @@
             @Surface.Rotation int rotation) {
         DisplayInfo updatedDisplayInfo = new DisplayInfo();
         updatedDisplayInfo.copyFrom(displayInfo);
-        updatedDisplayInfo.rotation = rotation;
+        // Apply rotations before updating width and height
+        updatedDisplayInfo.roundedCorners = updatedDisplayInfo.roundedCorners.rotate(rotation,
+                updatedDisplayInfo.logicalWidth, updatedDisplayInfo.logicalHeight);
+        updatedDisplayInfo.displayCutout =
+                DisplayContent.calculateDisplayCutoutForRotationAndDisplaySizeUncached(
+                        updatedDisplayInfo.displayCutout, rotation, updatedDisplayInfo.logicalWidth,
+                        updatedDisplayInfo.logicalHeight).getDisplayCutout();
 
+        updatedDisplayInfo.rotation = rotation;
         final int naturalWidth = updatedDisplayInfo.getNaturalWidth();
         final int naturalHeight = updatedDisplayInfo.getNaturalHeight();
         updatedDisplayInfo.logicalWidth = naturalWidth;
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index ba1cf8a..ee05523 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -170,6 +170,13 @@
         ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "startRecentsActivity(): intent=%s", mTargetIntent);
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "RecentsAnimation#startRecentsActivity");
 
+        // Cancel any existing recents animation running synchronously (do not hold the
+        // WM lock) before starting the newly requested recents animation as they can not coexist
+        if (mWindowManager.getRecentsAnimationController() != null) {
+            mWindowManager.getRecentsAnimationController().forceCancelAnimation(
+                    REORDER_MOVE_TO_ORIGINAL_POSITION, "startRecentsActivity");
+        }
+
         // If the activity is associated with the root recents task, then try and get that first
         Task targetRootTask = mDefaultTaskDisplayArea.getRootTask(WINDOWING_MODE_UNDEFINED,
                 mTargetActivityType);
@@ -243,12 +250,7 @@
             targetActivity.intent.replaceExtras(mTargetIntent);
 
             // Fetch all the surface controls and pass them to the client to get the animation
-            // started. Cancel any existing recents animation running synchronously (do not hold the
-            // WM lock)
-            if (mWindowManager.getRecentsAnimationController() != null) {
-                mWindowManager.getRecentsAnimationController().forceCancelAnimation(
-                        REORDER_MOVE_TO_ORIGINAL_POSITION, "startRecentsActivity");
-            }
+            // started
             mWindowManager.initializeRecentsAnimation(mTargetActivityType, recentsAnimationRunner,
                     this, mDefaultTaskDisplayArea.getDisplayId(),
                     mTaskSupervisor.mRecentTasks.getRecentTaskIds(), targetActivity);
diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java
index a663c62..9202eda 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimationController.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimationController.java
@@ -68,7 +68,6 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.LatencyTracker;
 import com.android.internal.util.function.pooled.PooledConsumer;
-import com.android.internal.util.function.pooled.PooledFunction;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.LocalServices;
 import com.android.server.inputmethod.InputMethodManagerInternal;
@@ -793,7 +792,7 @@
 
     private RemoteAnimationTarget[] createWallpaperAnimations() {
         ProtoLog.d(WM_DEBUG_RECENTS_ANIMATIONS, "createWallpaperAnimations()");
-        return WallpaperAnimationAdapter.startWallpaperAnimations(mService, 0L, 0L,
+        return WallpaperAnimationAdapter.startWallpaperAnimations(mDisplayContent, 0L, 0L,
                 adapter -> {
                     synchronized (mService.mGlobalLock) {
                         // If the wallpaper animation is canceled, continue with the recents
@@ -1152,13 +1151,7 @@
 
     private boolean isAnimatingApp(ActivityRecord activity) {
         for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
-            final Task task = mPendingAnimations.get(i).mTask;
-            final PooledFunction f = PooledLambda.obtainFunction(
-                    (a, b) -> a == b, activity,
-                    PooledLambda.__(ActivityRecord.class));
-            boolean isAnimatingApp = task.forAllActivities(f);
-            f.recycle();
-            if (isAnimatingApp) {
+            if (activity.isDescendantOf(mPendingAnimations.get(i).mTask)) {
                 return true;
             }
         }
@@ -1320,7 +1313,7 @@
 
         @Override
         public void startAnimation(SurfaceControl animationLeash, Transaction t,
-                @AnimationType int type, OnAnimationFinishedCallback finishCallback) {
+                @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) {
             // Restore position and root task crop until client has a chance to modify it.
             t.setPosition(animationLeash, mLocalBounds.left, mLocalBounds.top);
             mTmpRect.set(mLocalBounds);
diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java
index 16a45fe..eeac230 100644
--- a/services/core/java/com/android/server/wm/RemoteAnimationController.java
+++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java
@@ -22,6 +22,7 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
+import android.annotation.NonNull;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Binder;
@@ -207,7 +208,7 @@
                 if (wrappers.mThumbnailAdapter != null
                         && wrappers.mThumbnailAdapter.mCapturedFinishCallback != null) {
                     wrappers.mThumbnailAdapter.mCapturedFinishCallback
-                            .onAnimationFinished(wrappers.mAdapter.mAnimationType,
+                            .onAnimationFinished(wrappers.mThumbnailAdapter.mAnimationType,
                                     wrappers.mThumbnailAdapter);
                 }
                 mPendingAnimations.remove(i);
@@ -218,7 +219,7 @@
 
     private RemoteAnimationTarget[] createWallpaperAnimations() {
         ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "createWallpaperAnimations()");
-        return WallpaperAnimationAdapter.startWallpaperAnimations(mService,
+        return WallpaperAnimationAdapter.startWallpaperAnimations(mDisplayContent,
                 mRemoteAnimationAdapter.getDuration(),
                 mRemoteAnimationAdapter.getStatusBarTransitionDelay(),
                 adapter -> {
@@ -260,7 +261,7 @@
                     }
                     if (adapters.mThumbnailAdapter != null) {
                         adapters.mThumbnailAdapter.mCapturedFinishCallback
-                                .onAnimationFinished(adapters.mAdapter.mAnimationType,
+                                .onAnimationFinished(adapters.mThumbnailAdapter.mAnimationType,
                                         adapters.mThumbnailAdapter);
                     }
                     mPendingAnimations.remove(i);
@@ -477,7 +478,7 @@
 
         @Override
         public void startAnimation(SurfaceControl animationLeash, Transaction t,
-                @AnimationType int type, OnAnimationFinishedCallback finishCallback) {
+                @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) {
             ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation");
 
             if (mStartBounds.isEmpty()) {
diff --git a/services/core/java/com/android/server/wm/ResetTargetTaskHelper.java b/services/core/java/com/android/server/wm/ResetTargetTaskHelper.java
index 2626b87..1fd66fc 100644
--- a/services/core/java/com/android/server/wm/ResetTargetTaskHelper.java
+++ b/services/core/java/com/android/server/wm/ResetTargetTaskHelper.java
@@ -25,14 +25,13 @@
 import android.os.Debug;
 
 import com.android.internal.protolog.common.ProtoLog;
-import com.android.internal.util.function.pooled.PooledConsumer;
-import com.android.internal.util.function.pooled.PooledFunction;
-import com.android.internal.util.function.pooled.PooledLambda;
 
 import java.util.ArrayList;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /** Helper class for processing the reset of a task. */
-class ResetTargetTaskHelper {
+class ResetTargetTaskHelper implements Consumer<Task>, Predicate<ActivityRecord> {
     private Task mTask;
     private Task mTargetTask;
     private Task mTargetRootTask;
@@ -40,6 +39,7 @@
     private boolean mForceReset;
     private boolean mCanMoveOptions;
     private boolean mTargetTaskFound;
+    private boolean mIsTargetTask;
     private int mActivityReparentPosition;
     private ActivityOptions mTopOptions;
     private ArrayList<ActivityRecord> mResultActivities = new ArrayList<>();
@@ -62,32 +62,27 @@
         mTargetRootTask = targetTask.getRootTask();
         mActivityReparentPosition = -1;
 
-        final PooledConsumer c = PooledLambda.obtainConsumer(
-                ResetTargetTaskHelper::processTask, this, PooledLambda.__(Task.class));
-        targetTask.mWmService.mRoot.forAllLeafTasks(c, true /*traverseTopToBottom*/);
-        c.recycle();
+        targetTask.mWmService.mRoot.forAllLeafTasks(this, true /* traverseTopToBottom */);
 
         processPendingReparentActivities();
         reset(null);
         return mTopOptions;
     }
 
-    private void processTask(Task task) {
+    @Override
+    public void accept(Task task) {
         reset(task);
         mRoot = task.getRootActivity(true);
         if (mRoot == null) return;
 
-        final boolean isTargetTask = task == mTargetTask;
-        if (isTargetTask) mTargetTaskFound = true;
+        mIsTargetTask = task == mTargetTask;
+        if (mIsTargetTask) mTargetTaskFound = true;
 
-        final PooledFunction f = PooledLambda.obtainFunction(
-                ResetTargetTaskHelper::processActivity, this,
-                PooledLambda.__(ActivityRecord.class), isTargetTask);
-        task.forAllActivities(f);
-        f.recycle();
+        task.forAllActivities(this);
     }
 
-    private boolean processActivity(ActivityRecord r, boolean isTargetTask) {
+    @Override
+    public boolean test(ActivityRecord r) {
         // End processing if we have reached the root.
         if (r == mRoot) return true;
 
@@ -100,7 +95,7 @@
         final boolean clearWhenTaskReset =
                 (r.intent.getFlags() & Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) != 0;
 
-        if (isTargetTask) {
+        if (mIsTargetTask) {
             if (!finishOnTaskLaunch && !clearWhenTaskReset) {
                 if (r.resultTo != null) {
                     // If this activity is sending a reply to a previous activity, we can't do
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 97ea41c..9c3b756 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -48,6 +48,7 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WALLPAPER;
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_SURFACE_ALLOC;
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
 import static com.android.server.policy.PhoneWindowManager.SYSTEM_DIALOG_REASON_ASSIST;
@@ -72,6 +73,7 @@
 import static com.android.server.wm.ActivityTaskSupervisor.dumpHistoryList;
 import static com.android.server.wm.ActivityTaskSupervisor.printThisActivity;
 import static com.android.server.wm.KeyguardController.KEYGUARD_SLEEP_TOKEN_TAG;
+import static com.android.server.wm.RootWindowContainerProto.DEFAULT_MIN_SIZE_RESIZABLE_TASK;
 import static com.android.server.wm.RootWindowContainerProto.IS_HOME_RECENTS_COMPONENT;
 import static com.android.server.wm.RootWindowContainerProto.KEYGUARD_CONTROLLER;
 import static com.android.server.wm.RootWindowContainerProto.WINDOW_CONTAINER;
@@ -79,7 +81,6 @@
 import static com.android.server.wm.Task.REPARENT_MOVE_ROOT_TASK_TO_FRONT;
 import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_INVISIBLE;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_TRACE;
 import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -151,7 +152,6 @@
 import com.android.internal.app.ResolverActivity;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.function.pooled.PooledConsumer;
-import com.android.internal.util.function.pooled.PooledFunction;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.internal.util.function.pooled.PooledPredicate;
 import com.android.server.LocalServices;
@@ -169,7 +169,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.function.Consumer;
-import java.util.function.Function;
+import java.util.function.Predicate;
 
 /** Root {@link WindowContainer} for the device. */
 class RootWindowContainer extends WindowContainer<DisplayContent>
@@ -225,7 +225,7 @@
     private static final String DISPLAY_OFF_SLEEP_TOKEN_TAG = "Display-off";
 
     /** The token acquirer to put root tasks on the displays to sleep */
-    final ActivityTaskManagerInternal.SleepTokenAcquirer mDisplayOffTokenAcquirer;
+    final ActivityTaskManagerService.SleepTokenAcquirer mDisplayOffTokenAcquirer;
 
     /**
      * The modes which affect which tasks are returned when calling
@@ -278,8 +278,7 @@
     private int mTmpTaskLayerRank;
     private final RankTaskLayersRunnable mRankTaskLayersRunnable = new RankTaskLayersRunnable();
 
-    private boolean mTmpBoolean;
-    private RemoteException mTmpRemoteException;
+    private final AttachApplicationHelper mAttachApplicationHelper = new AttachApplicationHelper();
 
     private String mDestroyAllActivitiesReason;
     private final Runnable mDestroyAllActivitiesRunnable = new Runnable() {
@@ -305,7 +304,7 @@
 
     private final FindTaskResult mTmpFindTaskResult = new FindTaskResult();
 
-    static class FindTaskResult implements Function<Task, Boolean> {
+    static class FindTaskResult implements Predicate<Task> {
         ActivityRecord mIdealRecord;
         ActivityRecord mCandidateRecord;
 
@@ -347,7 +346,7 @@
         }
 
         @Override
-        public Boolean apply(Task task) {
+        public boolean test(Task task) {
             if (!ConfigurationContainer.isCompatibleActivityType(mActivityType,
                     task.getActivityType())) {
                 ProtoLog.d(WM_DEBUG_TASKS, "Skipping task: (mismatch activity/task) %s", task);
@@ -471,7 +470,7 @@
         mService = service.mAtmService;
         mTaskSupervisor = mService.mTaskSupervisor;
         mTaskSupervisor.mRootWindowContainer = this;
-        mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirerImpl(DISPLAY_OFF_SLEEP_TOKEN_TAG);
+        mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirer(DISPLAY_OFF_SLEEP_TOKEN_TAG);
     }
 
     boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) {
@@ -504,6 +503,7 @@
             mTopFocusedDisplayId = topFocusedDisplayId;
             mWmService.mInputManager.setFocusedDisplay(topFocusedDisplayId);
             mWmService.mPolicy.setTopFocusedDisplay(topFocusedDisplayId);
+            mWmService.mAccessibilityController.setFocusedDisplay(topFocusedDisplayId);
             ProtoLog.d(WM_DEBUG_FOCUS_LIGHT, "New topFocusedDisplayId=%d", topFocusedDisplayId);
         }
         return changed;
@@ -893,7 +893,7 @@
         for (int displayNdx = 0; displayNdx < mChildren.size(); ++displayNdx) {
             final DisplayContent displayContent = mChildren.get(displayNdx);
             if (displayContent.mWallpaperMayChange) {
-                if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG, "Wallpaper may change!  Adjusting");
+                ProtoLog.v(WM_DEBUG_WALLPAPER, "Wallpaper may change!  Adjusting");
                 displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
                 if (DEBUG_LAYOUT_REPEATS) {
                     surfacePlacer.debugLayoutRepeats("WallpaperMayChange",
@@ -1253,6 +1253,11 @@
         pw.println(mTopFocusedDisplayId);
     }
 
+    void dumpDefaultMinSizeOfResizableTask(PrintWriter pw) {
+        pw.print("  mDefaultMinSizeOfResizeableTaskDp=");
+        pw.println(mDefaultMinSizeOfResizeableTaskDp);
+    }
+
     void dumpLayoutNeededDisplayIds(PrintWriter pw) {
         if (!isLayoutNeeded()) {
             return;
@@ -1299,7 +1304,7 @@
         mTaskSupervisor.getKeyguardController().dumpDebug(proto, KEYGUARD_CONTROLLER);
         proto.write(IS_HOME_RECENTS_COMPONENT,
                 mTaskSupervisor.mRecentTasks.isRecentsComponentHomeActivity(mCurrentUser));
-
+        proto.write(DEFAULT_MIN_SIZE_RESIZABLE_TASK, mDefaultMinSizeOfResizeableTaskDp);
         proto.end(token);
     }
 
@@ -1930,58 +1935,11 @@
     }
 
     boolean attachApplication(WindowProcessController app) throws RemoteException {
-        boolean didSomething = false;
-        for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
-            mTmpRemoteException = null;
-            mTmpBoolean = false; // Set to true if an activity was started.
-            final DisplayContent display = getChildAt(displayNdx);
-            display.forAllRootTasks(rootTask -> {
-                if (mTmpRemoteException != null) {
-                    return;
-                }
-
-                if (rootTask.getVisibility(null /* starting */)
-                        == TASK_FRAGMENT_VISIBILITY_INVISIBLE) {
-                    return;
-                }
-
-                final PooledFunction c = PooledLambda.obtainFunction(
-                        RootWindowContainer::startActivityForAttachedApplicationIfNeeded, this,
-                        PooledLambda.__(ActivityRecord.class), app,
-                        rootTask.topRunningActivity());
-                rootTask.forAllActivities(c);
-                c.recycle();
-            });
-            if (mTmpRemoteException != null) {
-                throw mTmpRemoteException;
-            }
-            didSomething |= mTmpBoolean;
-        }
-        if (!didSomething) {
-            ensureActivitiesVisible(null, 0, false /* preserve_windows */);
-        }
-        return didSomething;
-    }
-
-    private boolean startActivityForAttachedApplicationIfNeeded(ActivityRecord r,
-            WindowProcessController app, ActivityRecord top) {
-        if (r.finishing || !r.showToCurrentUser() || !r.visibleIgnoringKeyguard || r.app != null
-                || app.mUid != r.info.applicationInfo.uid || !app.mName.equals(r.processName)) {
-            return false;
-        }
-
         try {
-            if (mTaskSupervisor.realStartActivityLocked(r, app,
-                    top == r && r.isFocusable() /*andResume*/, true /*checkConfig*/)) {
-                mTmpBoolean = true;
-            }
-        } catch (RemoteException e) {
-            Slog.w(TAG, "Exception in new application when starting activity "
-                    + top.intent.getComponent().flattenToShortString(), e);
-            mTmpRemoteException = e;
-            return true;
+            return mAttachApplicationHelper.process(app);
+        } finally {
+            mAttachApplicationHelper.reset();
         }
-        return false;
     }
 
     /**
@@ -2150,6 +2108,9 @@
             final Task rootTask;
             if (singleActivity) {
                 rootTask = task;
+
+                // Apply the last recents animation leash transform to the task entering PIP
+                rootTask.maybeApplyLastRecentsAnimationTransaction();
             } else {
                 // In the case of multiple activities, we will create a new task for it and then
                 // move the PIP activity into the task. Note that we explicitly defer the task
@@ -3238,7 +3199,7 @@
     FinishDisabledPackageActivitiesHelper mFinishDisabledPackageActivitiesHelper =
             new FinishDisabledPackageActivitiesHelper();
 
-    class FinishDisabledPackageActivitiesHelper {
+    class FinishDisabledPackageActivitiesHelper implements Predicate<ActivityRecord> {
         private String mPackageName;
         private Set<String> mFilterByClasses;
         private boolean mDoit;
@@ -3262,12 +3223,7 @@
         boolean process(String packageName, Set<String> filterByClasses,
                 boolean doit, boolean evenPersistent, int userId, boolean onlyRemoveNoProcess) {
             reset(packageName, filterByClasses, doit, evenPersistent, userId, onlyRemoveNoProcess);
-
-            final PooledFunction f = PooledLambda.obtainFunction(
-                    FinishDisabledPackageActivitiesHelper::collectActivity, this,
-                    PooledLambda.__(ActivityRecord.class));
-            forAllActivities(f);
-            f.recycle();
+            forAllActivities(this);
 
             boolean didSomething = false;
             final int size = mCollectedActivities.size();
@@ -3292,7 +3248,8 @@
             return didSomething;
         }
 
-        private boolean collectActivity(ActivityRecord r) {
+        @Override
+        public boolean test(ActivityRecord r) {
             final boolean sameComponent =
                     (r.packageName.equals(mPackageName) && (mFilterByClasses == null
                             || mFilterByClasses.contains(r.mActivityComponent.getClassName())))
@@ -3732,4 +3689,67 @@
             }
         }
     }
+
+    private class AttachApplicationHelper implements Consumer<Task>, Predicate<ActivityRecord> {
+        private boolean mHasActivityStarted;
+        private RemoteException mRemoteException;
+        private WindowProcessController mApp;
+        private ActivityRecord mTop;
+
+        void reset() {
+            mHasActivityStarted = false;
+            mRemoteException = null;
+            mApp = null;
+            mTop = null;
+        }
+
+        boolean process(WindowProcessController app) throws RemoteException {
+            mApp = app;
+            for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
+                getChildAt(displayNdx).forAllRootTasks(this);
+                if (mRemoteException != null) {
+                    throw mRemoteException;
+                }
+            }
+            if (!mHasActivityStarted) {
+                ensureActivitiesVisible(null /* starting */, 0 /* configChanges */,
+                        false /* preserveWindows */);
+            }
+            return mHasActivityStarted;
+        }
+
+        @Override
+        public void accept(Task rootTask) {
+            if (mRemoteException != null) {
+                return;
+            }
+            if (rootTask.getVisibility(null /* starting */)
+                    == TASK_FRAGMENT_VISIBILITY_INVISIBLE) {
+                return;
+            }
+            mTop = rootTask.topRunningActivity();
+            rootTask.forAllActivities(this);
+        }
+
+        @Override
+        public boolean test(ActivityRecord r) {
+            if (r.finishing || !r.showToCurrentUser() || !r.visibleIgnoringKeyguard
+                    || r.app != null || mApp.mUid != r.info.applicationInfo.uid
+                    || !mApp.mName.equals(r.processName)) {
+                return false;
+            }
+
+            try {
+                if (mTaskSupervisor.realStartActivityLocked(r, mApp,
+                        mTop == r && r.isFocusable() /* andResume */, true /* checkConfig */)) {
+                    mHasActivityStarted = true;
+                }
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Exception in new application when starting activity " + mTop, e);
+                mRemoteException = e;
+                return true;
+            }
+            return false;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 8c056b2..6b7f92a 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -620,11 +620,6 @@
     }
 
     @Override
-    public void updateDisplayContentLocation(IWindow window, int x, int y, int displayId) {
-        mService.updateDisplayContentLocation(window, x, y, displayId);
-    }
-
-    @Override
     public void updateTapExcludeRegion(IWindow window, Region region) {
         final long identity = Binder.clearCallingIdentity();
         try {
diff --git a/services/core/java/com/android/server/wm/StartingSurfaceController.java b/services/core/java/com/android/server/wm/StartingSurfaceController.java
index 26f0384..8b2a425 100644
--- a/services/core/java/com/android/server/wm/StartingSurfaceController.java
+++ b/services/core/java/com/android/server/wm/StartingSurfaceController.java
@@ -17,6 +17,7 @@
 package com.android.server.wm;
 
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED;
+import static android.window.StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_DRAWN;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_LEGACY_SPLASH_SCREEN;
 import static android.window.StartingWindowInfo.TYPE_PARAMETER_NEW_TASK;
@@ -86,7 +87,7 @@
 
     int makeStartingWindowTypeParameter(boolean newTask, boolean taskSwitch,
             boolean processRunning, boolean allowTaskSnapshot, boolean activityCreated,
-            boolean useEmpty, boolean useLegacy) {
+            boolean useEmpty, boolean useLegacy, boolean activityDrawn) {
         int parameter = 0;
         if (newTask) {
             parameter |= TYPE_PARAMETER_NEW_TASK;
@@ -109,6 +110,9 @@
         if (useLegacy) {
             parameter |= TYPE_PARAMETER_LEGACY_SPLASH_SCREEN;
         }
+        if (activityDrawn) {
+            parameter |= TYPE_PARAMETER_ACTIVITY_DRAWN;
+        }
         return parameter;
     }
 
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index c7bf8ec..d712bbf 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -57,8 +57,11 @@
     @VisibleForTesting
     SurfaceControl mLeash;
     @VisibleForTesting
+    SurfaceFreezer.Snapshot mSnapshot;
+    @VisibleForTesting
     final Animatable mAnimatable;
-    private final OnAnimationFinishedCallback mInnerAnimationFinishedCallback;
+    @VisibleForTesting
+    final OnAnimationFinishedCallback mInnerAnimationFinishedCallback;
 
     /**
      * Static callback to run on all animations started through this SurfaceAnimator
@@ -151,12 +154,14 @@
      * @param animationFinishedCallback The callback being triggered when the animation finishes.
      * @param animationCancelledCallback The callback is triggered after the SurfaceAnimator sends a
      *                                   cancel call to the underlying AnimationAdapter.
+     * @param snapshotAnim The animation to run for the snapshot. {@code null} if there is no
+     *                     snapshot.
      */
     void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
             @AnimationType int type,
             @Nullable OnAnimationFinishedCallback animationFinishedCallback,
             @Nullable Runnable animationCancelledCallback,
-            @Nullable SurfaceFreezer freezer) {
+            @Nullable AnimationAdapter snapshotAnim, @Nullable SurfaceFreezer freezer) {
         cancelAnimation(t, true /* restarting */, true /* forwardCancel */);
         mAnimation = anim;
         mAnimationType = type;
@@ -181,12 +186,16 @@
             return;
         }
         mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback);
+        if (snapshotAnim != null) {
+            mSnapshot = freezer.takeSnapshotForAnimation();
+            mSnapshot.startAnimation(t, snapshotAnim, type);
+        }
     }
 
     void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
             @AnimationType int type) {
         startAnimation(t, anim, hidden, type, null /* animationFinishedCallback */,
-                null /* animationCancelledCallback */, null /* freezer */);
+                null /* animationCancelledCallback */, null /* snapshotAnim */, null /* freezer */);
     }
 
     /**
@@ -328,6 +337,7 @@
         final OnAnimationFinishedCallback animationFinishedCallback =
                 mSurfaceAnimationFinishedCallback;
         final Runnable animationCancelledCallback = mAnimationCancelledCallback;
+        final SurfaceFreezer.Snapshot snapshot = mSnapshot;
         reset(t, false);
         if (animation != null) {
             if (!mAnimationStartDelayed && forwardCancel) {
@@ -346,9 +356,14 @@
             }
         }
 
-        if (forwardCancel && leash != null) {
-            t.remove(leash);
-            mService.scheduleAnimationLocked();
+        if (forwardCancel) {
+            if (snapshot != null) {
+                snapshot.cancelAnimation(t, false /* restarting */);
+            }
+            if (leash != null) {
+                t.remove(leash);
+                mService.scheduleAnimationLocked();
+            }
         }
 
         if (!restarting) {
@@ -361,6 +376,12 @@
         mAnimation = null;
         mSurfaceAnimationFinishedCallback = null;
         mAnimationType = ANIMATION_TYPE_NONE;
+        final SurfaceFreezer.Snapshot snapshot = mSnapshot;
+        mSnapshot = null;
+        if (snapshot != null) {
+            // Reset the mSnapshot reference before calling the callback to prevent circular reset.
+            snapshot.cancelAnimation(t, !destroyLeash);
+        }
         if (mLeash == null) {
             return;
         }
@@ -377,11 +398,15 @@
         boolean scheduleAnim = false;
         final SurfaceControl surface = animatable.getSurfaceControl();
         final SurfaceControl parent = animatable.getParentSurfaceControl();
+        final SurfaceControl curAnimationLeash = animatable.getAnimationLeash();
 
         // If the surface was destroyed or the leash is invalid, we don't care to reparent it back.
         // Note that we also set this variable to true even if the parent isn't valid anymore, in
         // order to ensure onAnimationLeashLost still gets called in this case.
-        final boolean reparent = surface != null;
+        // If the animation leash is set, and it is different from the removing leash, it means the
+        // surface now has a new animation surface. We don't want to reparent for that.
+        final boolean reparent = surface != null && (curAnimationLeash == null
+                || curAnimationLeash.equals(leash));
         if (reparent) {
             if (DEBUG_ANIM) Slog.i(TAG, "Reparenting to original parent: " + parent);
             // We shouldn't really need these isValid checks but we do
@@ -608,6 +633,14 @@
         void onAnimationLeashLost(Transaction t);
 
         /**
+         * Gets the last created animation leash that has not lost yet.
+         */
+        @Nullable
+        default SurfaceControl getAnimationLeash() {
+            return null;
+        }
+
+        /**
          * @return A new surface to be used for the animation leash, inserted at the correct
          *         position in the hierarchy.
          */
diff --git a/services/core/java/com/android/server/wm/SurfaceFreezer.java b/services/core/java/com/android/server/wm/SurfaceFreezer.java
index 89986ce..c667db8 100644
--- a/services/core/java/com/android/server/wm/SurfaceFreezer.java
+++ b/services/core/java/com/android/server/wm/SurfaceFreezer.java
@@ -17,7 +17,6 @@
 package com.android.server.wm;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATION;
 
 import android.annotation.NonNull;
@@ -27,13 +26,11 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
-import android.view.Surface;
 import android.view.SurfaceControl;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
 
-import java.util.function.Supplier;
-
 /**
  * This class handles "freezing" of an Animatable. The Animatable in question should implement
  * Freezable.
@@ -54,7 +51,8 @@
 
     private final Freezable mAnimatable;
     private final WindowManagerService mWmService;
-    private SurfaceControl mLeash;
+    @VisibleForTesting
+    SurfaceControl mLeash;
     Snapshot mSnapshot = null;
     final Rect mFreezeBounds = new Rect();
 
@@ -94,7 +92,7 @@
             if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
                 return;
             }
-            mSnapshot = new Snapshot(mWmService.mSurfaceFactory, t, screenshotBuffer, mLeash);
+            mSnapshot = new Snapshot(t, screenshotBuffer, mLeash);
         }
     }
 
@@ -109,12 +107,25 @@
     }
 
     /**
+     * Used by {@link SurfaceAnimator}. This "transfers" the snapshot leash to be used for
+     * animation. By transferring the leash, this will no longer try to clean-up the leash when
+     * finished.
+     */
+    @Nullable
+    Snapshot takeSnapshotForAnimation() {
+        final Snapshot out = mSnapshot;
+        mSnapshot = null;
+        return out;
+    }
+
+    /**
      * Clean-up the snapshot and remove leash. If the leash was taken, this just cleans-up the
      * snapshot.
      */
     void unfreeze(SurfaceControl.Transaction t) {
         if (mSnapshot != null) {
             mSnapshot.cancelAnimation(t, false /* restarting */);
+            mSnapshot = null;
         }
         if (mLeash == null) {
             return;
@@ -163,13 +174,12 @@
     class Snapshot {
         private SurfaceControl mSurfaceControl;
         private AnimationAdapter mAnimation;
-        private SurfaceAnimator.OnAnimationFinishedCallback mFinishedCallback;
 
         /**
          * @param t Transaction to create the thumbnail in.
          * @param screenshotBuffer A thumbnail or placeholder for thumbnail to initialize with.
          */
-        Snapshot(Supplier<Surface> surfaceFactory, SurfaceControl.Transaction t,
+        Snapshot(SurfaceControl.Transaction t,
                 SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer, SurfaceControl parent) {
             // We can't use a delegating constructor since we need to
             // reference this::onAnimationFinished
@@ -211,19 +221,15 @@
          *             component responsible for running the animation. It runs the animation with
          *             {@link AnimationAdapter#startAnimation} once the hierarchy with
          *             the Leash has been set up.
-         * @param animationFinishedCallback The callback being triggered when the animation
-         *                                  finishes.
          */
-        void startAnimation(SurfaceControl.Transaction t, AnimationAdapter anim, int type,
-                @Nullable SurfaceAnimator.OnAnimationFinishedCallback animationFinishedCallback) {
+        void startAnimation(SurfaceControl.Transaction t, AnimationAdapter anim, int type) {
             cancelAnimation(t, true /* restarting */);
             mAnimation = anim;
-            mFinishedCallback = animationFinishedCallback;
             if (mSurfaceControl == null) {
                 cancelAnimation(t, false /* restarting */);
                 return;
             }
-            mAnimation.startAnimation(mSurfaceControl, t, type, animationFinishedCallback);
+            mAnimation.startAnimation(mSurfaceControl, t, type, (typ, ani) -> { });
         }
 
         /**
@@ -235,18 +241,9 @@
         void cancelAnimation(SurfaceControl.Transaction t, boolean restarting) {
             final SurfaceControl leash = mSurfaceControl;
             final AnimationAdapter animation = mAnimation;
-            final SurfaceAnimator.OnAnimationFinishedCallback animationFinishedCallback =
-                    mFinishedCallback;
             mAnimation = null;
-            mFinishedCallback = null;
             if (animation != null) {
                 animation.onAnimationCancelled(leash);
-                if (!restarting) {
-                    if (animationFinishedCallback != null) {
-                        animationFinishedCallback.onAnimationFinished(
-                                ANIMATION_TYPE_APP_TRANSITION, animation);
-                    }
-                }
             }
             if (!restarting) {
                 destroy(t);
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 0819549..6987cdb 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -195,7 +195,6 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.pooled.PooledConsumer;
-import com.android.internal.util.function.pooled.PooledFunction;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.internal.util.function.pooled.PooledPredicate;
 import com.android.server.Watchdog;
@@ -486,9 +485,6 @@
 
     private static Exception sTmpException;
 
-    /** ActivityRecords that are exiting, but still on screen for animations. */
-    final ArrayList<ActivityRecord> mExitingActivities = new ArrayList<>();
-
     private boolean mForceShowForAllUsers;
 
     /** When set, will force the task to report as invisible. */
@@ -538,26 +534,23 @@
     private static final ResetTargetTaskHelper sResetTargetTaskHelper = new ResetTargetTaskHelper();
 
     private final FindRootHelper mFindRootHelper = new FindRootHelper();
-    private class FindRootHelper {
+    private class FindRootHelper implements Predicate<ActivityRecord> {
         private ActivityRecord mRoot;
-
-        private void clear() {
-            mRoot = null;
-        }
+        private boolean mIgnoreRelinquishIdentity;
+        private boolean mSetToBottomIfNone;
 
         ActivityRecord findRoot(boolean ignoreRelinquishIdentity, boolean setToBottomIfNone) {
-            final PooledFunction f = PooledLambda.obtainFunction(FindRootHelper::processActivity,
-                    this, PooledLambda.__(ActivityRecord.class), ignoreRelinquishIdentity,
-                    setToBottomIfNone);
-            clear();
-            forAllActivities(f, false /*traverseTopToBottom*/);
-            f.recycle();
-            return mRoot;
+            mIgnoreRelinquishIdentity = ignoreRelinquishIdentity;
+            mSetToBottomIfNone = setToBottomIfNone;
+            forAllActivities(this, false /* traverseTopToBottom */);
+            final ActivityRecord root = mRoot;
+            mRoot = null;
+            return root;
         }
 
-        private boolean processActivity(ActivityRecord r,
-                boolean ignoreRelinquishIdentity, boolean setToBottomIfNone) {
-            if (mRoot == null && setToBottomIfNone) {
+        @Override
+        public boolean test(ActivityRecord r) {
+            if (mRoot == null && mSetToBottomIfNone) {
                 // This is the first activity we are process. Set it as the candidate root in case
                 // we don't find a better one.
                 mRoot = r;
@@ -569,7 +562,7 @@
             mRoot = r;
 
             // Only end search if we are ignore relinquishing identity or we are not relinquishing.
-            return ignoreRelinquishIdentity
+            return mIgnoreRelinquishIdentity
                     || mNeverRelinquishIdentity
                     || (r.info.flags & FLAG_RELINQUISH_TASK_IDENTITY) == 0;
         }
@@ -879,7 +872,7 @@
             // entrance of the new window to be properly animated.
             // Note here we always set the replacing window first, as the flags might be needed
             // during the relaunch. If we end up not doing any relaunch, we clear the flags later.
-            windowManager.setWillReplaceWindow(topActivity.appToken, animate);
+            windowManager.setWillReplaceWindow(topActivity.token, animate);
         }
 
         mAtmService.deferWindowLayout();
@@ -936,7 +929,7 @@
             // If we didn't actual do a relaunch (indicated by kept==true meaning we kept the old
             // window), we need to clear the replace window settings. Otherwise, we schedule a
             // timeout to remove the old window if the replacing window is not coming in time.
-            windowManager.scheduleClearWillReplaceWindows(topActivity.appToken, !kept);
+            windowManager.scheduleClearWillReplaceWindows(topActivity.token, !kept);
         }
 
         if (!deferResume) {
@@ -1001,7 +994,6 @@
             mCallingPackage = r.launchedFromPackage;
             mCallingFeatureId = r.launchedFromFeatureId;
             setIntent(intent != null ? intent : r.intent, info != null ? info : r.info);
-            return;
         }
         setLockTaskAuth(r);
     }
@@ -1641,7 +1633,7 @@
         final ActivityRecord r = findActivityInHistory(newR.mActivityComponent);
         if (r == null) return null;
 
-        final PooledFunction f = PooledLambda.obtainFunction(Task::finishActivityAbove,
+        final PooledPredicate f = PooledLambda.obtainPredicate(Task::finishActivityAbove,
                 PooledLambda.__(ActivityRecord.class), r);
         forAllActivities(f);
         f.recycle();
@@ -1791,7 +1783,7 @@
         if (root == null) return;
 
         final TaskDescription taskDescription = new TaskDescription();
-        final PooledFunction f = PooledLambda.obtainFunction(
+        final PooledPredicate f = PooledLambda.obtainPredicate(
                 Task::setTaskDescriptionFromActivityAboveRoot,
                 PooledLambda.__(ActivityRecord.class), root, taskDescription);
         forAllActivities(f);
@@ -1926,6 +1918,7 @@
             final ActivityRecord r = topRunningActivity();
             if (r != null && mDisplayContent.isFixedRotationLaunchingApp(r)) {
                 getSyncTransaction().setWindowCrop(mSurfaceControl, null)
+                        .setCornerRadius(mSurfaceControl, 0f)
                         .setMatrix(mSurfaceControl, Matrix.IDENTITY_MATRIX, new float[9]);
             }
         }
@@ -2972,11 +2965,6 @@
         return super.makeAnimationLeash().setMetadata(METADATA_TASK_ID, mTaskId);
     }
 
-    @Override
-    void resetSurfacePositionForAnimationLeash(SurfaceControl.Transaction t) {
-        super.resetSurfacePositionForAnimationLeash(t);
-    }
-
     boolean shouldAnimate() {
         /**
          * Animations are handled by the TaskOrganizer implementation.
@@ -3041,7 +3029,7 @@
     }
 
     private static boolean isTopRunning(ActivityRecord r, int taskId, IBinder notTop) {
-        return r.getTask().mTaskId != taskId && r.appToken != notTop && r.canBeTopRunning();
+        return r.getTask().mTaskId != taskId && r.token != notTop && r.canBeTopRunning();
     }
 
     ActivityRecord getTopFullscreenActivity() {
@@ -3153,13 +3141,13 @@
     }
 
     @Override
-    boolean forAllTasks(Function<Task, Boolean> callback) {
+    boolean forAllTasks(Predicate<Task> callback) {
         if (super.forAllTasks(callback)) return true;
-        return callback.apply(this);
+        return callback.test(this);
     }
 
     @Override
-    boolean forAllLeafTasks(Function<Task, Boolean> callback) {
+    boolean forAllLeafTasks(Predicate<Task> callback) {
         boolean isLeafTask = true;
         for (int i = mChildren.size() - 1; i >= 0; --i) {
             final Task child = mChildren.get(i).asTask();
@@ -3171,7 +3159,7 @@
             }
         }
         if (isLeafTask) {
-            return callback.apply(this);
+            return callback.test(this);
         }
         return false;
     }
@@ -3212,8 +3200,8 @@
     }
 
     @Override
-    boolean forAllRootTasks(Function<Task, Boolean> callback, boolean traverseTopToBottom) {
-        return isRootTask() ? callback.apply(this) : false;
+    boolean forAllRootTasks(Predicate<Task> callback, boolean traverseTopToBottom) {
+        return isRootTask() ? callback.test(this) : false;
     }
 
     @Override
@@ -3330,20 +3318,6 @@
     @Override
     void dump(PrintWriter pw, String prefix, boolean dumpAll) {
         super.dump(pw, prefix, dumpAll);
-
-        if (!mExitingActivities.isEmpty()) {
-            final String doublePrefix = prefix + "  ";
-            pw.println();
-            pw.println(prefix + "Exiting application tokens:");
-            for (int i = mExitingActivities.size() - 1; i >= 0; i--) {
-                WindowToken token = mExitingActivities.get(i);
-                pw.print(doublePrefix + "Exiting App #" + i);
-                pw.print(' '); pw.print(token);
-                pw.println(':');
-                token.dump(pw, doublePrefix, dumpAll);
-            }
-            pw.println();
-        }
         mAnimatingActivityRegistry.dump(pw, "AnimatingApps:", prefix);
     }
 
@@ -3794,7 +3768,7 @@
         }
 
         sTmpException = null;
-        final PooledFunction f = PooledLambda.obtainFunction(Task::saveActivityToXml,
+        final PooledPredicate f = PooledLambda.obtainPredicate(Task::saveActivityToXml,
                 PooledLambda.__(ActivityRecord.class), getBottomMostActivity(), out);
         forAllActivities(f);
         f.recycle();
@@ -4270,7 +4244,7 @@
     /**
      * @return true if the task is currently focused.
      */
-    boolean isFocused() {
+    private boolean isFocused() {
         if (mDisplayContent == null || mDisplayContent.mFocusedApp == null) {
             return false;
         }
@@ -4328,14 +4302,10 @@
     }
 
     /**
-     * Called on the task of a window which gained or lost focus.
+     * Called on the task when it gained or lost focus.
      * @param hasFocus
      */
     void onAppFocusChanged(boolean hasFocus) {
-        final ActivityRecord topAct = getTopVisibleActivity();
-        if (topAct != null && (topAct.mStartingData instanceof SnapshotStartingData)) {
-            topAct.removeStartingWindowIfNeeded();
-        }
         updateShadowsRadius(hasFocus, getSyncTransaction());
         dispatchTaskInfoChangedIfNeeded(false /* force */);
     }
@@ -4865,11 +4835,11 @@
             mHandler.removeMessages(TRANSLUCENT_TIMEOUT_MSG);
 
             if (waitingActivity != null) {
-                mWmService.setWindowOpaqueLocked(waitingActivity.appToken, false);
+                mWmService.setWindowOpaqueLocked(waitingActivity.token, false);
                 if (waitingActivity.attachedToProcess()) {
                     try {
                         waitingActivity.app.getThread().scheduleTranslucentConversionComplete(
-                                waitingActivity.appToken, r != null);
+                                waitingActivity.token, r != null);
                     } catch (RemoteException e) {
                     }
                 }
@@ -5262,7 +5232,7 @@
             });
         } else {
             // Check if any of the activities are using voice
-            final PooledFunction f = PooledLambda.obtainFunction(
+            final PooledPredicate f = PooledLambda.obtainPredicate(
                     Task::finishIfVoiceActivity, PooledLambda.__(ActivityRecord.class),
                     binder);
             tr.forAllActivities(f);
@@ -5275,7 +5245,7 @@
         // Inform of cancellation
         r.clearVoiceSessionLocked();
         try {
-            r.app.getThread().scheduleLocalVoiceInteractionStarted(r.appToken, null);
+            r.app.getThread().scheduleLocalVoiceInteractionStarted(r.token, null);
         } catch (RemoteException re) {
             // Ok Boomer...
         }
@@ -5371,7 +5341,7 @@
         // We should consolidate.
         IActivityController controller = mAtmService.mController;
         if (controller != null) {
-            ActivityRecord next = topRunningActivity(srec.appToken, INVALID_TASK_ID);
+            ActivityRecord next = topRunningActivity(srec.token, INVALID_TASK_ID);
             if (next != null) {
                 // ask watcher if this is allowed
                 boolean resumeOK = true;
@@ -5428,7 +5398,7 @@
                             .obtainStarter(destIntent, "navigateUpTo")
                             .setCaller(srec.app.getThread())
                             .setActivityInfo(aInfo)
-                            .setResultTo(parent.appToken)
+                            .setResultTo(parent.token)
                             .setCallingPid(-1)
                             .setCallingUid(callingUid)
                             .setCallingPackage(srec.packageName)
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 66f2dbc..16e77f0 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -60,7 +60,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.ToBooleanFunction;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.internal.util.function.pooled.PooledPredicate;
 import com.android.server.wm.LaunchParamsController.LaunchParams;
@@ -68,10 +67,10 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 /**
  * {@link DisplayArea} that represents a section of a screen that contains app window containers.
@@ -99,13 +98,6 @@
     private int mColorLayerCounter = 0;
 
     /**
-     * A control placed at the appropriate level for transitions to occur.
-     */
-    private SurfaceControl mAppAnimationLayer;
-    private SurfaceControl mBoostedAppAnimationLayer;
-    private SurfaceControl mHomeAppAnimationLayer;
-
-    /**
      * Given that the split-screen divider does not have an AppWindowToken, it
      * will have to live inside of a "NonAppWindowContainer". However, in visual Z order
      * it will need to be interleaved with some of our children, appearing on top of
@@ -132,7 +124,6 @@
     private final ArrayList<WindowContainer> mTmpNormalChildren = new ArrayList<>();
     private final ArrayList<WindowContainer> mTmpHomeChildren = new ArrayList<>();
     private final IntArray mTmpNeedsZBoostIndexes = new IntArray();
-    private int mTmpLayerForAnimationLayer;
 
     private ArrayList<Task> mTmpTasks = new ArrayList<>();
 
@@ -528,16 +519,16 @@
     }
 
     @Override
-    boolean forAllTaskDisplayAreas(Function<TaskDisplayArea, Boolean> callback,
+    boolean forAllTaskDisplayAreas(Predicate<TaskDisplayArea> callback,
             boolean traverseTopToBottom) {
         // Apply the callback to all TDAs at or below this container. If the callback returns true,
         // stop early.
         if (traverseTopToBottom) {
             // When it is top to bottom, run on child TDA first as they are on top of the parent.
             return super.forAllTaskDisplayAreas(callback, traverseTopToBottom)
-                    || callback.apply(this);
+                    || callback.test(this);
         }
-        return callback.apply(this) || super.forAllTaskDisplayAreas(callback, traverseTopToBottom);
+        return callback.test(this) || super.forAllTaskDisplayAreas(callback, traverseTopToBottom);
     }
 
     @Override
@@ -694,71 +685,6 @@
     }
 
     @Override
-    boolean forAllWindows(ToBooleanFunction<WindowState> callback,
-            boolean traverseTopToBottom) {
-        if (traverseTopToBottom) {
-            if (super.forAllWindows(callback, traverseTopToBottom)) {
-                return true;
-            }
-            if (forAllExitingAppTokenWindows(callback, traverseTopToBottom)) {
-                return true;
-            }
-        } else {
-            if (forAllExitingAppTokenWindows(callback, traverseTopToBottom)) {
-                return true;
-            }
-            if (super.forAllWindows(callback, traverseTopToBottom)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private boolean forAllExitingAppTokenWindows(ToBooleanFunction<WindowState> callback,
-            boolean traverseTopToBottom) {
-        // For legacy reasons we process the RootTask.mExitingActivities first here before the
-        // app tokens.
-        // TODO: Investigate if we need to continue to do this or if we can just process them
-        // in-order.
-        if (traverseTopToBottom) {
-            for (int i = mChildren.size() - 1; i >= 0; --i) {
-                // Only run on those of direct Task child, because child TaskDisplayArea has run on
-                // its child in #forAllWindows()
-                if (mChildren.get(i).asTask() == null) {
-                    continue;
-                }
-                final List<ActivityRecord> activities =
-                        mChildren.get(i).asTask().mExitingActivities;
-                for (int j = activities.size() - 1; j >= 0; --j) {
-                    if (activities.get(j).forAllWindowsUnchecked(callback,
-                            traverseTopToBottom)) {
-                        return true;
-                    }
-                }
-            }
-        } else {
-            final int count = mChildren.size();
-            for (int i = 0; i < count; ++i) {
-                // Only run on those of direct Task child, because child TaskDisplayArea has run on
-                // its child in #forAllWindows()
-                if (mChildren.get(i).asTask() == null) {
-                    continue;
-                }
-                final List<ActivityRecord> activities =
-                        mChildren.get(i).asTask().mExitingActivities;
-                final int appTokensCount = activities.size();
-                for (int j = 0; j < appTokensCount; j++) {
-                    if (activities.get(j).forAllWindowsUnchecked(callback,
-                            traverseTopToBottom)) {
-                        return true;
-                    }
-                }
-            }
-        }
-        return false;
-    }
-
-    @Override
     int getOrientation(int candidate) {
         mLastOrientationSource = null;
         if (mIgnoreOrientationRequest) {
@@ -871,33 +797,13 @@
 
         int layer = 0;
         // Place root home tasks to the bottom.
-        layer = adjustRootTaskLayer(t, mTmpHomeChildren, layer, false /* normalRootTasks */);
-        // The home animation layer is between the root home tasks and the normal root tasks.
-        final int layerForHomeAnimationLayer = layer++;
-        mTmpLayerForAnimationLayer = layer++;
-        layer = adjustRootTaskLayer(t, mTmpNormalChildren, layer, true /* normalRootTasks */);
+        layer = adjustRootTaskLayer(t, mTmpHomeChildren, layer);
+        adjustRootTaskLayer(t, mTmpNormalChildren, layer);
 
-        // The boosted animation layer is between the normal root tasks and the always on top
-        // root tasks.
-        final int layerForBoostedAnimationLayer = layer++;
         // Always on top tasks layer should higher than split divider layer so set it as start.
-        layer = SPLIT_DIVIDER_LAYER + 1;
-        adjustRootTaskLayer(t, mTmpAlwaysOnTopChildren, layer, false /* normalRootTasks */);
-
-        t.setLayer(mHomeAppAnimationLayer, layerForHomeAnimationLayer);
-        t.setLayer(mAppAnimationLayer, mTmpLayerForAnimationLayer);
         t.setLayer(mSplitScreenDividerAnchor, SPLIT_DIVIDER_LAYER);
-        t.setLayer(mBoostedAppAnimationLayer, layerForBoostedAnimationLayer);
-    }
-
-    private int adjustNormalRootTaskLayer(WindowContainer child, int layer) {
-        if ((child.asTask() != null && child.asTask().isAnimatingByRecents())
-                || child.isAppTransitioning()) {
-            // The animation layer is located above the highest animating root task and no
-            // higher.
-            mTmpLayerForAnimationLayer = layer++;
-        }
-        return layer;
+        layer = SPLIT_DIVIDER_LAYER + 1;
+        adjustRootTaskLayer(t, mTmpAlwaysOnTopChildren, layer);
     }
 
     /**
@@ -906,11 +812,10 @@
      * normal rootTasks.
      *
      * @param startLayer   The beginning layer of this group of rootTasks.
-     * @param normalRootTasks Set {@code true} if this group is neither home nor always on top.
      * @return The adjusted layer value.
      */
     private int adjustRootTaskLayer(SurfaceControl.Transaction t,
-            ArrayList<WindowContainer> children, int startLayer, boolean normalRootTasks) {
+            ArrayList<WindowContainer> children, int startLayer) {
         mTmpNeedsZBoostIndexes.clear();
         final int childCount = children.size();
         for (int i = 0; i < childCount; i++) {
@@ -923,9 +828,6 @@
 
             if (!childNeedsZBoost) {
                 child.assignLayer(t, startLayer++);
-                if (normalRootTasks) {
-                    startLayer = adjustNormalRootTaskLayer(child, startLayer);
-                }
             } else {
                 mTmpNeedsZBoostIndexes.add(i);
             }
@@ -935,9 +837,6 @@
         for (int i = 0; i < zBoostSize; i++) {
             final WindowContainer child = children.get(mTmpNeedsZBoostIndexes.get(i));
             child.assignLayer(t, startLayer++);
-            if (normalRootTasks) {
-                startLayer = adjustNormalRootTaskLayer(child, startLayer);
-            }
         }
         return startLayer;
     }
@@ -951,19 +850,6 @@
     }
 
     @Override
-    SurfaceControl getAppAnimationLayer(@AnimationLayer int animationLayer) {
-        switch (animationLayer) {
-            case ANIMATION_LAYER_BOOSTED:
-                return mBoostedAppAnimationLayer;
-            case ANIMATION_LAYER_HOME:
-                return mHomeAppAnimationLayer;
-            case ANIMATION_LAYER_STANDARD:
-            default:
-                return mAppAnimationLayer;
-        }
-    }
-
-    @Override
     RemoteAnimationTarget createRemoteAnimationTarget(
             RemoteAnimationController.RemoteAnimationRecord record) {
         final ActivityRecord activity = getTopMostActivity();
@@ -983,42 +869,21 @@
                         .setName("colorBackgroundLayer")
                         .setCallsite("TaskDisplayArea.onParentChanged")
                         .build();
-                mAppAnimationLayer = makeChildSurface(null)
-                        .setName("animationLayer")
-                        .setCallsite("TaskDisplayArea.onParentChanged")
-                        .build();
-                mBoostedAppAnimationLayer = makeChildSurface(null)
-                        .setName("boostedAnimationLayer")
-                        .setCallsite("TaskDisplayArea.onParentChanged")
-                        .build();
-                mHomeAppAnimationLayer = makeChildSurface(null)
-                        .setName("homeAnimationLayer")
-                        .setCallsite("TaskDisplayArea.onParentChanged")
-                        .build();
                 mSplitScreenDividerAnchor = makeChildSurface(null)
                         .setName("splitScreenDividerAnchor")
                         .setCallsite("TaskDisplayArea.onParentChanged")
                         .build();
 
                 getSyncTransaction()
-                        .show(mAppAnimationLayer)
-                        .show(mBoostedAppAnimationLayer)
-                        .show(mHomeAppAnimationLayer)
                         .show(mSplitScreenDividerAnchor);
             });
         } else {
             super.onParentChanged(newParent, oldParent);
             mWmService.mTransactionFactory.get()
                     .remove(mColorBackgroundLayer)
-                    .remove(mAppAnimationLayer)
-                    .remove(mBoostedAppAnimationLayer)
-                    .remove(mHomeAppAnimationLayer)
                     .remove(mSplitScreenDividerAnchor)
                     .apply();
             mColorBackgroundLayer = null;
-            mAppAnimationLayer = null;
-            mBoostedAppAnimationLayer = null;
-            mHomeAppAnimationLayer = null;
             mSplitScreenDividerAnchor = null;
         }
     }
@@ -1059,15 +924,12 @@
     @Override
     void migrateToNewSurfaceControl(SurfaceControl.Transaction t) {
         super.migrateToNewSurfaceControl(t);
-        if (mAppAnimationLayer == null) {
+        if (mColorBackgroundLayer == null) {
             return;
         }
 
         // As TaskDisplayArea is getting a new surface, reparent and reorder the child surfaces.
         t.reparent(mColorBackgroundLayer, mSurfaceControl);
-        t.reparent(mAppAnimationLayer, mSurfaceControl);
-        t.reparent(mBoostedAppAnimationLayer, mSurfaceControl);
-        t.reparent(mHomeAppAnimationLayer, mSurfaceControl);
         t.reparent(mSplitScreenDividerAnchor, mSurfaceControl);
         reassignLayer(t);
         scheduleAnimation();
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 44b22c6..b8393f6 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -93,7 +93,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
-import com.android.internal.util.function.pooled.PooledFunction;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.internal.util.function.pooled.PooledPredicate;
 
@@ -102,7 +101,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
-import java.util.function.Function;
+import java.util.function.Predicate;
 
 /**
  * A basic container that can be used to contain activities or other {@link TaskFragment}, which
@@ -252,7 +251,7 @@
             new EnsureActivitiesVisibleHelper(this);
     private final EnsureVisibleActivitiesConfigHelper mEnsureVisibleActivitiesConfigHelper =
             new EnsureVisibleActivitiesConfigHelper();
-    private class EnsureVisibleActivitiesConfigHelper {
+    private class EnsureVisibleActivitiesConfigHelper implements Predicate<ActivityRecord> {
         private boolean mUpdateConfig;
         private boolean mPreserveWindow;
         private boolean mBehindFullscreen;
@@ -268,12 +267,8 @@
                 return;
             }
             reset(preserveWindow);
-
-            final PooledFunction f = PooledLambda.obtainFunction(
-                    EnsureVisibleActivitiesConfigHelper::processActivity, this,
-                    PooledLambda.__(ActivityRecord.class));
-            forAllActivities(f, start, true /*includeBoundary*/, true /*traverseTopToBottom*/);
-            f.recycle();
+            forAllActivities(this, start, true /* includeBoundary */,
+                    true /* traverseTopToBottom */);
 
             if (mUpdateConfig) {
                 // Ensure the resumed state of the focus activity if we updated the configuration of
@@ -282,7 +277,8 @@
             }
         }
 
-        boolean processActivity(ActivityRecord r) {
+        @Override
+        public boolean test(ActivityRecord r) {
             mUpdateConfig |= r.ensureActivityConfiguration(0 /*globalChanges*/, mPreserveWindow);
             mBehindFullscreen |= r.occludesParent();
             return mBehindFullscreen;
@@ -1238,7 +1234,7 @@
 
             try {
                 final ClientTransaction transaction =
-                        ClientTransaction.obtain(next.app.getThread(), next.appToken);
+                        ClientTransaction.obtain(next.app.getThread(), next.token);
                 // Deliver all pending results.
                 ArrayList<ResultInfo> a = next.results;
                 if (a != null) {
@@ -1428,23 +1424,8 @@
                         + "directly: %s", prev);
 
                 didAutoPip = mAtmService.enterPictureInPictureMode(prev, prev.pictureInPictureArgs);
-                mPausingActivity = null;
             } else {
-                ProtoLog.v(WM_DEBUG_STATES, "Enqueueing pending pause: %s", prev);
-                try {
-                    EventLogTags.writeWmPauseActivity(prev.mUserId, System.identityHashCode(prev),
-                            prev.shortComponentName, "userLeaving=" + userLeaving, reason);
-
-                    mAtmService.getLifecycleManager().scheduleTransaction(prev.app.getThread(),
-                            prev.appToken, PauseActivityItem.obtain(prev.finishing, userLeaving,
-                                    prev.configChangeFlags, pauseImmediately));
-                } catch (Exception e) {
-                    // Ignore exception, if process died other code will cleanup.
-                    Slog.w(TAG, "Exception thrown during pause", e);
-                    mPausingActivity = null;
-                    mLastPausedActivity = null;
-                    mTaskSupervisor.mNoHistoryActivities.remove(prev);
-                }
+                schedulePauseActivity(prev, userLeaving, pauseImmediately, reason);
             }
         } else {
             mPausingActivity = null;
@@ -1459,7 +1440,7 @@
         }
 
         // If already entered PIP mode, no need to keep pausing.
-        if (mPausingActivity != null && !didAutoPip) {
+        if (mPausingActivity != null) {
             // Have the window manager pause its key dispatching until the new
             // activity has started.  If we're pausing the activity just because
             // the screen is being turned off and the UI is sleeping, don't interrupt
@@ -1494,6 +1475,25 @@
         }
     }
 
+    void schedulePauseActivity(ActivityRecord prev, boolean userLeaving,
+            boolean pauseImmediately, String reason) {
+        ProtoLog.v(WM_DEBUG_STATES, "Enqueueing pending pause: %s", prev);
+        try {
+            EventLogTags.writeWmPauseActivity(prev.mUserId, System.identityHashCode(prev),
+                    prev.shortComponentName, "userLeaving=" + userLeaving, reason);
+
+            mAtmService.getLifecycleManager().scheduleTransaction(prev.app.getThread(),
+                    prev.token, PauseActivityItem.obtain(prev.finishing, userLeaving,
+                            prev.configChangeFlags, pauseImmediately));
+        } catch (Exception e) {
+            // Ignore exception, if process died other code will cleanup.
+            Slog.w(TAG, "Exception thrown during pause", e);
+            mPausingActivity = null;
+            mLastPausedActivity = null;
+            mTaskSupervisor.mNoHistoryActivities.remove(prev);
+        }
+    }
+
     @VisibleForTesting
     void completePause(boolean resumeNext, ActivityRecord resuming) {
         // Complete the pausing process of a pausing activity, so it doesn't make sense to
@@ -1615,7 +1615,7 @@
     }
 
     @Override
-    boolean forAllLeafTaskFragments(Function<TaskFragment, Boolean> callback) {
+    boolean forAllLeafTaskFragments(Predicate<TaskFragment> callback) {
         boolean isLeafTaskFrag = true;
         for (int i = mChildren.size() - 1; i >= 0; --i) {
             final TaskFragment child = mChildren.get(i).asTaskFragment();
@@ -1627,7 +1627,7 @@
             }
         }
         if (isLeafTaskFrag) {
-            return callback.apply(this);
+            return callback.test(this);
         }
         return false;
     }
@@ -2164,7 +2164,7 @@
                     && wc.asActivityRecord() != null
                     && wc.asActivityRecord().getPid() == mTaskFragmentOrganizerPid) {
                 // Only includes Activities that belong to the organizer process for security.
-                childActivities.add(wc.asActivityRecord().appToken);
+                childActivities.add(wc.asActivityRecord().token);
             }
         }
         final Point positionInParent = new Point();
diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
index 9cc24e2..d543c1f 100644
--- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
@@ -261,8 +261,7 @@
             if (launchMode == WINDOWING_MODE_PINNED) {
                 if (DEBUG) appendLog("picture-in-picture");
             } else if (!root.isResizeable()) {
-                if (shouldLaunchUnresizableAppInFreeform(root, suggestedDisplayArea,
-                        options.getLaunchWindowingMode())) {
+                if (shouldLaunchUnresizableAppInFreeform(root, suggestedDisplayArea, options)) {
                     launchMode = WINDOWING_MODE_FREEFORM;
                     if (outParams.mBounds.isEmpty()) {
                         getTaskBounds(root, suggestedDisplayArea, layout, launchMode,
@@ -618,8 +617,8 @@
     }
 
     private boolean shouldLaunchUnresizableAppInFreeform(ActivityRecord activity,
-            TaskDisplayArea displayArea, int launchWindowingMode) {
-        if (launchWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+            TaskDisplayArea displayArea, @Nullable ActivityOptions options) {
+        if (options != null && options.getLaunchWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
             // Do not launch the activity in freeform if it explicitly requested fullscreen mode.
             return false;
         }
@@ -718,7 +717,7 @@
         }
 
         // First we get the default size we want.
-        getDefaultFreeformSize(displayArea, layout, orientation, mTmpBounds);
+        getDefaultFreeformSize(root.info, displayArea, layout, orientation, mTmpBounds);
         if (hasInitialBounds || sizeMatches(inOutBounds, mTmpBounds)) {
             // We're here because either input parameters specified initial bounds, or the suggested
             // bounds have the same size of the default freeform size. We should use the suggested
@@ -786,7 +785,8 @@
         return orientation;
     }
 
-    private void getDefaultFreeformSize(@NonNull TaskDisplayArea displayArea,
+    private void getDefaultFreeformSize(@NonNull ActivityInfo info,
+            @NonNull TaskDisplayArea displayArea,
             @NonNull ActivityInfo.WindowLayout layout, int orientation, @NonNull Rect bounds) {
         // Default size, which is letterboxing/pillarboxing in displayArea. That's to say the large
         // dimension of default size is the small dimension of displayArea size, and the small
@@ -817,11 +817,38 @@
         final int layoutMinWidth = (layout == null) ? -1 : layout.minWidth;
         final int layoutMinHeight = (layout == null) ? -1 : layout.minHeight;
 
-        // Final result.
+        // Aspect ratio requirements.
+        final float minAspectRatio = info.getMinAspectRatio();
+        final float maxAspectRatio = info.getMaxAspectRatio();
+
         final int width = Math.min(defaultWidth, Math.max(phoneWidth, layoutMinWidth));
         final int height = Math.min(defaultHeight, Math.max(phoneHeight, layoutMinHeight));
+        final float aspectRatio = (float) Math.max(width, height) / (float) Math.min(width, height);
 
-        bounds.set(0, 0, width, height);
+        // Adjust the width and height to the aspect ratio requirements.
+        int adjWidth = width;
+        int adjHeight = height;
+        if (minAspectRatio >= 1 && aspectRatio < minAspectRatio) {
+            // The aspect ratio is below the minimum, adjust it to the minimum.
+            if (orientation == SCREEN_ORIENTATION_LANDSCAPE) {
+                // Fix the width, scale the height.
+                adjHeight = (int) (adjWidth / minAspectRatio + 0.5f);
+            } else {
+                // Fix the height, scale the width.
+                adjWidth = (int) (adjHeight / minAspectRatio + 0.5f);
+            }
+        } else if (maxAspectRatio >= 1 && aspectRatio > maxAspectRatio) {
+            // The aspect ratio exceeds the maximum, adjust it to the maximum.
+            if (orientation == SCREEN_ORIENTATION_LANDSCAPE) {
+                // Fix the width, scale the height.
+                adjHeight = (int) (adjWidth / maxAspectRatio + 0.5f);
+            } else {
+                // Fix the height, scale the width.
+                adjWidth = (int) (adjHeight / maxAspectRatio + 0.5f);
+            }
+        }
+
+        bounds.set(0, 0, adjWidth, adjHeight);
         bounds.offset(stableBounds.left, stableBounds.top);
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index ccda126..3d5f988 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -22,13 +22,13 @@
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL;
 import static com.android.server.wm.WindowOrganizerController.configurationsAreEqualForOrganizer;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.WindowConfiguration;
 import android.content.Intent;
 import android.content.pm.ParceledListSlice;
-import android.graphics.Rect;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.Parcel;
@@ -40,6 +40,7 @@
 import android.window.ITaskOrganizerController;
 import android.window.SplashScreenView;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskAppearedInfo;
 import android.window.TaskSnapshot;
 import android.window.WindowContainerToken;
@@ -422,8 +423,8 @@
     }
 
     // Capture the animation surface control for activity's main window
-    private static class StartingWindowAnimationAdaptor implements AnimationAdapter {
-        private SurfaceControl mAnimationLeash;
+    static class StartingWindowAnimationAdaptor implements AnimationAdapter {
+        SurfaceControl mAnimationLeash;
         @Override
         public boolean getShowWallpaper() {
             return false;
@@ -431,7 +432,7 @@
 
         @Override
         public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t,
-                int type, SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
+                int type, @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
             mAnimationLeash = animationLeash;
         }
 
@@ -464,6 +465,13 @@
         }
     }
 
+    static SurfaceControl applyStartingWindowAnimation(WindowContainer window) {
+        final StartingWindowAnimationAdaptor adaptor = new StartingWindowAnimationAdaptor();
+        window.startAnimation(window.getPendingTransaction(), adaptor, false,
+                ANIMATION_TYPE_STARTING_REVEAL);
+        return adaptor.mAnimationLeash;
+    }
+
     boolean addStartingWindow(Task task, ActivityRecord activity, int launchTheme,
             TaskSnapshot taskSnapshot) {
         final Task rootTask = task.getRootTask();
@@ -498,29 +506,28 @@
         if (lastOrganizer == null) {
             return;
         }
-        SurfaceControl windowAnimationLeash = null;
-        Rect mainFrame = null;
+        final StartingWindowRemovalInfo removalInfo = new StartingWindowRemovalInfo();
+        removalInfo.taskId = task.mTaskId;
+        removalInfo.playRevealAnimation = prepareAnimation;
         final boolean playShiftUpAnimation = !task.inMultiWindowMode();
-        if (prepareAnimation && playShiftUpAnimation) {
-            final ActivityRecord topActivity = task.topActivityContainsStartingWindow();
-            if (topActivity != null) {
+        final ActivityRecord topActivity = task.topActivityContainsStartingWindow();
+        if (topActivity != null) {
+            removalInfo.deferRemoveForIme = topActivity.mDisplayContent
+                    .mayImeShowOnLaunchingActivity(topActivity);
+            if (prepareAnimation && playShiftUpAnimation) {
                 final WindowState mainWindow =
                         topActivity.findMainWindow(false/* includeStartingApp */);
                 if (mainWindow != null) {
-                    final StartingWindowAnimationAdaptor adaptor =
-                            new StartingWindowAnimationAdaptor();
                     final SurfaceControl.Transaction t = mainWindow.getPendingTransaction();
-                    mainWindow.startAnimation(t, adaptor, false,
-                            ANIMATION_TYPE_STARTING_REVEAL);
-                    windowAnimationLeash = adaptor.mAnimationLeash;
-                    mainFrame = mainWindow.getRelativeFrame();
-                    t.setPosition(windowAnimationLeash, mainFrame.left, mainFrame.top);
+                    removalInfo.windowAnimationLeash = applyStartingWindowAnimation(mainWindow);
+                    removalInfo.mainFrame = mainWindow.getRelativeFrame();
+                    t.setPosition(removalInfo.windowAnimationLeash,
+                            removalInfo.mainFrame.left, removalInfo.mainFrame.top);
                 }
             }
         }
         try {
-            lastOrganizer.removeStartingWindow(task.mTaskId, windowAnimationLeash,
-                    mainFrame, prepareAnimation);
+            lastOrganizer.removeStartingWindow(removalInfo);
         } catch (RemoteException e) {
             Slog.e(TAG, "Exception sending onStartTaskFinished callback", e);
         }
diff --git a/services/core/java/com/android/server/wm/TaskPositioningController.java b/services/core/java/com/android/server/wm/TaskPositioningController.java
index 525420e..4d3219d 100644
--- a/services/core/java/com/android/server/wm/TaskPositioningController.java
+++ b/services/core/java/com/android/server/wm/TaskPositioningController.java
@@ -158,7 +158,7 @@
                     + "win=" + win + ", resize=" + resize + ", preserveOrientation="
                     + preserveOrientation + ", {" + startX + ", " + startY + "}");
 
-        if (win == null || win.getAppToken() == null) {
+        if (win == null || win.mActivityRecord == null) {
             Slog.w(TAG_WM, "startPositioningLocked: Bad window " + win);
             return false;
         }
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 795286d..96c935a 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -180,43 +180,46 @@
         snapshotTasks(tasks, false /* allowSnapshotHome */);
     }
 
+    void recordTaskSnapshot(Task task, boolean allowSnapshotHome) {
+        final TaskSnapshot snapshot;
+        final boolean snapshotHome = allowSnapshotHome && task.isActivityTypeHome();
+        if (snapshotHome) {
+            snapshot = snapshotTask(task);
+        } else {
+            switch (getSnapshotMode(task)) {
+                case SNAPSHOT_MODE_NONE:
+                    return;
+                case SNAPSHOT_MODE_APP_THEME:
+                    snapshot = drawAppThemeSnapshot(task);
+                    break;
+                case SNAPSHOT_MODE_REAL:
+                    snapshot = snapshotTask(task);
+                    break;
+                default:
+                    snapshot = null;
+                    break;
+            }
+        }
+        if (snapshot != null) {
+            final HardwareBuffer buffer = snapshot.getHardwareBuffer();
+            if (buffer.getWidth() == 0 || buffer.getHeight() == 0) {
+                buffer.close();
+                Slog.e(TAG, "Invalid task snapshot dimensions " + buffer.getWidth() + "x"
+                        + buffer.getHeight());
+            } else {
+                mCache.putSnapshot(task, snapshot);
+                // Don't persist or notify the change for the temporal snapshot.
+                if (!snapshotHome) {
+                    mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot);
+                    task.onSnapshotChanged(snapshot);
+                }
+            }
+        }
+    }
+
     private void snapshotTasks(ArraySet<Task> tasks, boolean allowSnapshotHome) {
         for (int i = tasks.size() - 1; i >= 0; i--) {
-            final Task task = tasks.valueAt(i);
-            final TaskSnapshot snapshot;
-            final boolean snapshotHome = allowSnapshotHome && task.isActivityTypeHome();
-            if (snapshotHome) {
-                snapshot = snapshotTask(task);
-            } else {
-                switch (getSnapshotMode(task)) {
-                    case SNAPSHOT_MODE_NONE:
-                        continue;
-                    case SNAPSHOT_MODE_APP_THEME:
-                        snapshot = drawAppThemeSnapshot(task);
-                        break;
-                    case SNAPSHOT_MODE_REAL:
-                        snapshot = snapshotTask(task);
-                        break;
-                    default:
-                        snapshot = null;
-                        break;
-                }
-            }
-            if (snapshot != null) {
-                final HardwareBuffer buffer = snapshot.getHardwareBuffer();
-                if (buffer.getWidth() == 0 || buffer.getHeight() == 0) {
-                    buffer.close();
-                    Slog.e(TAG, "Invalid task snapshot dimensions " + buffer.getWidth() + "x"
-                            + buffer.getHeight());
-                } else {
-                    mCache.putSnapshot(task, snapshot);
-                    // Don't persist or notify the change for the temporal snapshot.
-                    if (!snapshotHome) {
-                        mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot);
-                        task.onSnapshotChanged(snapshot);
-                    }
-                }
-            }
+            recordTaskSnapshot(tasks.valueAt(i), allowSnapshotHome);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index e50e8ef..e537c0a 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -415,7 +415,16 @@
                     if (commitVisibility) {
                         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
                                 "  Commit activity becoming invisible: %s", ar);
-                        ar.commitVisibility(false /* visible */, false /* performLayout */);
+                        final Task task = ar.getTask();
+                        if (task != null && !task.isVisibleRequested()
+                                && mTransientLaunches != null) {
+                            // If transition is transient, then snapshots are taken at end of
+                            // transition.
+                            mController.mTaskSnapshotController.recordTaskSnapshot(
+                                    task, false /* allowSnapshotHome */);
+                        }
+                        ar.commitVisibility(false /* visible */, false /* performLayout */,
+                                true /* fromTransition */);
                         activitiesWentInvisible = true;
                     }
                 }
@@ -558,6 +567,19 @@
             mVisibleAtTransitionEndTokens.add(wc.asWindowToken());
         }
 
+        // Take task snapshots before the animation so that we can capture IME before it gets
+        // transferred. If transition is transient, IME won't be moved during the transition and
+        // the tasks are still live, so we take the snapshot at the end of the transition instead.
+        if (mTransientLaunches == null) {
+            for (int i = mParticipants.size() - 1; i >= 0; --i) {
+                final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
+                if (ar == null || ar.isVisibleRequested() || ar.getTask() == null
+                        || ar.getTask().isVisibleRequested()) continue;
+                mController.mTaskSnapshotController.recordTaskSnapshot(
+                        ar.getTask(), false /* allowSnapshotHome */);
+            }
+        }
+
         mStartTransaction = transaction;
         mFinishTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get();
         buildFinishTransaction(mFinishTransaction, info.getRootLeash());
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index fc54239..ff4cc3d 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -59,6 +59,7 @@
 
     private ITransitionPlayer mTransitionPlayer;
     final ActivityTaskManagerService mAtm;
+    final TaskSnapshotController mTaskSnapshotController;
 
     private final ArrayList<WindowManagerInternal.AppTransitionListener> mLegacyListeners =
             new ArrayList<>();
@@ -79,9 +80,11 @@
     // TODO(b/188595497): remove when not needed.
     final StatusBarManagerInternal mStatusBar;
 
-    TransitionController(ActivityTaskManagerService atm) {
+    TransitionController(ActivityTaskManagerService atm,
+            TaskSnapshotController taskSnapshotController) {
         mAtm = atm;
         mStatusBar = LocalServices.getService(StatusBarManagerInternal.class);
+        mTaskSnapshotController = taskSnapshotController;
         mTransitionPlayerDeath = () -> {
             synchronized (mAtm.mGlobalLock) {
                 // Clean-up/finish any playing transitions.
diff --git a/services/core/java/com/android/server/wm/WallpaperAnimationAdapter.java b/services/core/java/com/android/server/wm/WallpaperAnimationAdapter.java
index 25f7269..2652723 100644
--- a/services/core/java/com/android/server/wm/WallpaperAnimationAdapter.java
+++ b/services/core/java/com/android/server/wm/WallpaperAnimationAdapter.java
@@ -20,6 +20,7 @@
 import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
 
+import android.annotation.NonNull;
 import android.graphics.Point;
 import android.os.SystemClock;
 import android.util.proto.ProtoOutputStream;
@@ -64,18 +65,17 @@
      *
      * @return RemoteAnimationTarget[] targets for all the visible wallpaper windows
      */
-    public static RemoteAnimationTarget[] startWallpaperAnimations(WindowManagerService service,
+    public static RemoteAnimationTarget[] startWallpaperAnimations(DisplayContent displayContent,
             long durationHint, long statusBarTransitionDelay,
             Consumer<WallpaperAnimationAdapter> animationCanceledRunnable,
             ArrayList<WallpaperAnimationAdapter> adaptersOut) {
+        if (!displayContent.mWallpaperController.isWallpaperVisible()) {
+            ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS,
+                    "\tWallpaper of display=%s is not visible", displayContent);
+            return new RemoteAnimationTarget[0];
+        }
         final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>();
-        service.mRoot.forAllWallpaperWindows(wallpaperWindow -> {
-            if (!wallpaperWindow.getDisplayContent().mWallpaperController.isWallpaperVisible()) {
-                ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "\tNot visible=%s", wallpaperWindow);
-                return;
-            }
-
-            ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "\tvisible=%s", wallpaperWindow);
+        displayContent.forAllWallpaperWindows(wallpaperWindow -> {
             final WallpaperAnimationAdapter wallpaperAdapter = new WallpaperAnimationAdapter(
                     wallpaperWindow, durationHint, statusBarTransitionDelay,
                     animationCanceledRunnable);
@@ -134,7 +134,8 @@
 
     @Override
     public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t,
-            @AnimationType int type, SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
+            @AnimationType int type,
+            @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) {
         ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation");
 
         // Restore z-layering until client has a chance to modify it.
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 0909462..a92e088 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -24,12 +24,12 @@
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER;
 
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WALLPAPER;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 import static com.android.server.wm.WindowManagerService.H.WALLPAPER_DRAW_PENDING_TIMEOUT;
@@ -49,6 +49,8 @@
 import android.view.animation.Animation;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.ProtoLogImpl;
+import com.android.internal.protolog.common.ProtoLog;
 import com.android.internal.util.ToBooleanFunction;
 
 import java.io.PrintWriter;
@@ -291,10 +293,11 @@
         for (int i = mWallpaperTokens.size() - 1; i >= 0; i--) {
             final WallpaperWindowToken token = mWallpaperTokens.get(i);
             token.setVisibility(false);
-            if (DEBUG_WALLPAPER_LIGHT && token.isVisible()) {
-                Slog.d(TAG, "Hiding wallpaper " + token
-                        + " from " + winGoingAway + " target=" + mWallpaperTarget + " prev="
-                        + mPrevWallpaperTarget + "\n" + Debug.getCallers(5, "  "));
+            if (ProtoLogImpl.isEnabled(WM_DEBUG_WALLPAPER) && token.isVisible()) {
+                ProtoLog.d(WM_DEBUG_WALLPAPER,
+                        "Hiding wallpaper %s from %s target=%s prev=%s callers=%s",
+                        token, winGoingAway, mWallpaperTarget, mPrevWallpaperTarget,
+                        Debug.getCallers(5));
             }
         }
     }
@@ -544,15 +547,15 @@
 
             // Is it time to stop animating?
             if (!mPrevWallpaperTarget.isAnimatingLw()) {
-                if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG, "No longer animating wallpaper targets!");
+                ProtoLog.v(WM_DEBUG_WALLPAPER, "No longer animating wallpaper targets!");
                 mPrevWallpaperTarget = null;
                 mWallpaperTarget = wallpaperTarget;
             }
             return;
         }
 
-        if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG,
-                "New wallpaper target: " + wallpaperTarget + " prevTarget: " + mWallpaperTarget);
+        ProtoLog.v(WM_DEBUG_WALLPAPER, "New wallpaper target: %s prevTarget: %s caller=%s",
+                wallpaperTarget, mWallpaperTarget, Debug.getCallers(5));
 
         mPrevWallpaperTarget = null;
 
@@ -570,8 +573,8 @@
         // then we are in our super special mode!
         boolean oldAnim = prevWallpaperTarget.isAnimatingLw();
         boolean foundAnim = wallpaperTarget.isAnimatingLw();
-        if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG,
-                "New animation: " + foundAnim + " old animation: " + oldAnim);
+        ProtoLog.v(WM_DEBUG_WALLPAPER, "New animation: %s old animation: %s",
+                foundAnim, oldAnim);
 
         if (!foundAnim || !oldAnim) {
             return;
@@ -586,14 +589,14 @@
         final boolean oldTargetHidden = prevWallpaperTarget.mActivityRecord != null
                 && !prevWallpaperTarget.mActivityRecord.mVisibleRequested;
 
-        if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG, "Animating wallpapers:" + " old: "
-                + prevWallpaperTarget + " hidden=" + oldTargetHidden + " new: " + wallpaperTarget
-                + " hidden=" + newTargetHidden);
+        ProtoLog.v(WM_DEBUG_WALLPAPER, "Animating wallpapers: "
+                + "old: %s hidden=%b new: %s hidden=%b",
+                prevWallpaperTarget, oldTargetHidden, wallpaperTarget, newTargetHidden);
 
         mPrevWallpaperTarget = prevWallpaperTarget;
 
         if (newTargetHidden && !oldTargetHidden) {
-            if (DEBUG_WALLPAPER_LIGHT) Slog.v(TAG, "Old wallpaper still the target.");
+            ProtoLog.v(WM_DEBUG_WALLPAPER, "Old wallpaper still the target.");
             // Use the old target if new target is hidden but old target
             // is not. If they're both hidden, still use the new target.
             mWallpaperTarget = prevWallpaperTarget;
@@ -661,8 +664,8 @@
                     /* x= */ 0, /* y= */ 0, /* z= */ 0, /* extras= */ null, /* sync= */ false);
         }
 
-        if (DEBUG_WALLPAPER_LIGHT)  Slog.d(TAG, "New wallpaper: target=" + mWallpaperTarget
-                + " prev=" + mPrevWallpaperTarget);
+        ProtoLog.d(WM_DEBUG_WALLPAPER, "New wallpaper: target=%s prev=%s",
+                mWallpaperTarget, mPrevWallpaperTarget);
     }
 
     boolean processWallpaperDrawPendingTimeout() {
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index 75c84c4..3a639f5 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -20,7 +20,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WALLPAPER;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 
@@ -28,7 +28,6 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.util.Slog;
 import android.view.DisplayInfo;
 import android.view.ViewGroup;
 import android.view.WindowManager;
@@ -107,8 +106,8 @@
 
     void updateWallpaperWindows(boolean visible) {
         if (isVisible() != visible) {
-            if (DEBUG_WALLPAPER_LIGHT) Slog.d(TAG,
-                    "Wallpaper token " + token + " visible=" + visible);
+            ProtoLog.d(WM_DEBUG_WALLPAPER, "Wallpaper token %s visible=%b",
+                    token, visible);
             setVisibility(visible);
         }
         final WallpaperController wallpaperController = mDisplayContent.mWallpaperController;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 6b21858..cae5b14 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -71,7 +71,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityThread;
-import android.app.WindowConfiguration;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
@@ -112,6 +111,7 @@
 import java.util.Comparator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -128,26 +128,6 @@
 
     private static final String TAG = TAG_WITH_CLASS_NAME ? "WindowContainer" : TAG_WM;
 
-    /** Animation layer that happens above all animating {@link Task}s. */
-    static final int ANIMATION_LAYER_STANDARD = 0;
-
-    /** Animation layer that happens above all {@link Task}s. */
-    static final int ANIMATION_LAYER_BOOSTED = 1;
-
-    /**
-     * Animation layer that is reserved for {@link WindowConfiguration#ACTIVITY_TYPE_HOME}
-     * activities and all activities that are being controlled by the recents animation. This
-     * layer is generally below all {@link Task}s.
-     */
-    static final int ANIMATION_LAYER_HOME = 2;
-
-    @IntDef(prefix = { "ANIMATION_LAYER_" }, value = {
-            ANIMATION_LAYER_STANDARD,
-            ANIMATION_LAYER_BOOSTED,
-            ANIMATION_LAYER_HOME,
-    })
-    @interface AnimationLayer {}
-
     static final int POSITION_TOP = Integer.MAX_VALUE;
     static final int POSITION_BOTTOM = Integer.MIN_VALUE;
 
@@ -199,6 +179,10 @@
      */
     protected final SurfaceAnimator mSurfaceAnimator;
 
+    /** The parent leash added for animation. */
+    @Nullable
+    private SurfaceControl mAnimationLeash;
+
     final SurfaceFreezer mSurfaceFreezer;
     protected final WindowManagerService mWmService;
     final TransitionController mTransitionController;
@@ -956,21 +940,6 @@
     }
 
     /**
-     * Similar to {@link #isAnimating(int, int)} except provide a bitmask of
-     * {@link AnimationType} to exclude, rather than include
-     * @param flags The combination of bitmask flags to specify targets and condition for
-     *              checking animating status.
-     * @param typesToExclude The combination of bitmask {@link AnimationType} to exclude when
-     *                     checking if animating.
-     *
-     * @deprecated Use {@link #isAnimating(int, int)}
-     */
-    @Deprecated
-    final boolean isAnimatingExcluding(int flags, int typesToExclude) {
-        return isAnimating(flags, ANIMATION_TYPE_ALL & ~typesToExclude);
-    }
-
-    /**
      * @deprecated Use {@link #isAnimating(int, int)}
      * TODO (b/152333373): Migrate calls to use isAnimating with specified animation type
      */
@@ -1440,12 +1409,11 @@
         wrapper.release();
     }
 
-    boolean forAllActivities(Function<ActivityRecord, Boolean> callback) {
+    boolean forAllActivities(Predicate<ActivityRecord> callback) {
         return forAllActivities(callback, true /*traverseTopToBottom*/);
     }
 
-    boolean forAllActivities(
-            Function<ActivityRecord, Boolean> callback, boolean traverseTopToBottom) {
+    boolean forAllActivities(Predicate<ActivityRecord> callback, boolean traverseTopToBottom) {
         if (traverseTopToBottom) {
             for (int i = mChildren.size() - 1; i >= 0; --i) {
                 if (mChildren.get(i).forAllActivities(callback, traverseTopToBottom)) return true;
@@ -1487,13 +1455,13 @@
      * @param traverseTopToBottom direction to traverse the tree.
      * @return {@code true} if we ended the search before reaching the end of the tree.
      */
-    final boolean forAllActivities(Function<ActivityRecord, Boolean> callback,
+    final boolean forAllActivities(Predicate<ActivityRecord> callback,
             WindowContainer boundary, boolean includeBoundary, boolean traverseTopToBottom) {
         return forAllActivities(
                 callback, boundary, includeBoundary, traverseTopToBottom, new boolean[1]);
     }
 
-    private boolean forAllActivities(Function<ActivityRecord, Boolean> callback,
+    private boolean forAllActivities(Predicate<ActivityRecord> callback,
             WindowContainer boundary, boolean includeBoundary, boolean traverseTopToBottom,
             boolean[] boundaryFound) {
         if (traverseTopToBottom) {
@@ -1516,7 +1484,7 @@
         return false;
     }
 
-    private boolean processForAllActivitiesWithBoundary(Function<ActivityRecord, Boolean> callback,
+    private boolean processForAllActivitiesWithBoundary(Predicate<ActivityRecord> callback,
             WindowContainer boundary, boolean includeBoundary, boolean traverseTopToBottom,
             boolean[] boundaryFound, WindowContainer wc) {
         if (wc == boundary) {
@@ -1681,7 +1649,7 @@
      * @param callback Calls the {@link ToBooleanFunction#apply} method for each task found and
      *                 stops the search if {@link ToBooleanFunction#apply} returns {@code true}.
      */
-    boolean forAllTasks(Function<Task, Boolean> callback) {
+    boolean forAllTasks(Predicate<Task> callback) {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
             if (mChildren.get(i).forAllTasks(callback)) {
                 return true;
@@ -1690,7 +1658,7 @@
         return false;
     }
 
-    boolean forAllLeafTasks(Function<Task, Boolean> callback) {
+    boolean forAllLeafTasks(Predicate<Task> callback) {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
             if (mChildren.get(i).forAllLeafTasks(callback)) {
                 return true;
@@ -1699,7 +1667,7 @@
         return false;
     }
 
-    boolean forAllLeafTaskFragments(Function<TaskFragment, Boolean> callback) {
+    boolean forAllLeafTaskFragments(Predicate<TaskFragment> callback) {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
             if (mChildren.get(i).forAllLeafTaskFragments(callback)) {
                 return true;
@@ -1714,11 +1682,11 @@
      * @param callback Calls the {@link ToBooleanFunction#apply} method for each root task found and
      *                 stops the search if {@link ToBooleanFunction#apply} returns {@code true}.
      */
-    boolean forAllRootTasks(Function<Task, Boolean> callback) {
+    boolean forAllRootTasks(Predicate<Task> callback) {
         return forAllRootTasks(callback, true /* traverseTopToBottom */);
     }
 
-    boolean forAllRootTasks(Function<Task, Boolean> callback, boolean traverseTopToBottom) {
+    boolean forAllRootTasks(Predicate<Task> callback, boolean traverseTopToBottom) {
         int count = mChildren.size();
         if (traverseTopToBottom) {
             for (int i = count - 1; i >= 0; --i) {
@@ -2000,7 +1968,7 @@
      * @return {@code true} if the search ended before we reached the end of the hierarchy due to
      *         callback returning {@code true}.
      */
-    boolean forAllTaskDisplayAreas(Function<TaskDisplayArea, Boolean> callback,
+    boolean forAllTaskDisplayAreas(Predicate<TaskDisplayArea> callback,
             boolean traverseTopToBottom) {
         int childCount = mChildren.size();
         int i = traverseTopToBottom ? childCount - 1 : 0;
@@ -2021,7 +1989,7 @@
      * @return {@code true} if the search ended before we reached the end of the hierarchy due to
      *         callback returning {@code true}.
      */
-    boolean forAllTaskDisplayAreas(Function<TaskDisplayArea, Boolean> callback) {
+    boolean forAllTaskDisplayAreas(Predicate<TaskDisplayArea> callback) {
         return forAllTaskDisplayAreas(callback, true /* traverseTopToBottom */);
     }
 
@@ -2582,11 +2550,14 @@
      * @param animationFinishedCallback The callback being triggered when the animation finishes.
      * @param animationCancelledCallback The callback is triggered after the SurfaceAnimator sends a
      *                                   cancel call to the underlying AnimationAdapter.
+     * @param snapshotAnim  The animation to run for the snapshot. {@code null} if there is no
+     *                      snapshot.
      */
     void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
             @AnimationType int type,
             @Nullable OnAnimationFinishedCallback animationFinishedCallback,
-            @Nullable Runnable animationCancelledCallback) {
+            @Nullable Runnable animationCancelledCallback,
+            @Nullable AnimationAdapter snapshotAnim) {
         if (DEBUG_ANIM) {
             Slog.v(TAG, "Starting animation on " + this + ": type=" + type + ", anim=" + anim);
         }
@@ -2594,14 +2565,14 @@
         // TODO: This should use isVisible() but because isVisible has a really weird meaning at
         // the moment this doesn't work for all animatable window containers.
         mSurfaceAnimator.startAnimation(t, anim, hidden, type, animationFinishedCallback,
-                animationCancelledCallback, mSurfaceFreezer);
+                animationCancelledCallback, snapshotAnim, mSurfaceFreezer);
     }
 
     void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
             @AnimationType int type,
             @Nullable OnAnimationFinishedCallback animationFinishedCallback) {
         startAnimation(t, anim, hidden, type, animationFinishedCallback,
-                null /* adapterAnimationCancelledCallback */);
+                null /* adapterAnimationCancelledCallback */, null /* snapshotAnim */);
     }
 
     void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
@@ -2672,17 +2643,6 @@
         return getParentSurfaceControl();
     }
 
-    /**
-     * @return The layer on which all app animations are happening.
-     */
-    SurfaceControl getAppAnimationLayer(@AnimationLayer int animationLayer) {
-        final WindowContainer parent = getParent();
-        if (parent != null) {
-            return parent.getAppAnimationLayer(animationLayer);
-        }
-        return null;
-    }
-
     // TODO: Remove this and use #getBounds() instead once we set an app transition animation
     // on TaskStack.
     Rect getAnimationBounds(int appRootTaskClipMode) {
@@ -2860,21 +2820,26 @@
                 taskDisplayArea.setBackgroundColor(backgroundColor);
             }
 
+            // Atomic counter to make sure the clearColor callback is only called one.
+            // It will be called twice in the case we cancel the animation without restart
+            // (in that case it will run as the cancel and finished callbacks).
+            final AtomicInteger callbackCounter = new AtomicInteger(0);
+            final Runnable clearBackgroundColorHandler = () -> {
+                if (callbackCounter.getAndIncrement() == 0) {
+                    taskDisplayArea.clearBackgroundColor();
+                }
+            };
+
             final Runnable cleanUpCallback = isSettingBackgroundColor
-                    ? taskDisplayArea::clearBackgroundColor : () -> {};
+                    ? clearBackgroundColorHandler : () -> {};
 
             startAnimation(getPendingTransaction(), adapter, !isVisible(),
-                    ANIMATION_TYPE_APP_TRANSITION,
-                    (type, anim) -> cleanUpCallback.run(),
-                    cleanUpCallback);
+                    ANIMATION_TYPE_APP_TRANSITION, (type, anim) -> cleanUpCallback.run(),
+                    cleanUpCallback, thumbnailAdapter);
 
             if (adapter.getShowWallpaper()) {
                 getDisplayContent().pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
             }
-            if (thumbnailAdapter != null) {
-                mSurfaceFreezer.mSnapshot.startAnimation(getPendingTransaction(),
-                        thumbnailAdapter, ANIMATION_TYPE_APP_TRANSITION, (type, anim) -> { });
-            }
         }
     }
 
@@ -3004,6 +2969,7 @@
     @Override
     public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) {
         mLastLayer = -1;
+        mAnimationLeash = leash;
         reassignLayer(t);
 
         // Leash is now responsible for position, so set our position to 0.
@@ -3013,11 +2979,16 @@
     @Override
     public void onAnimationLeashLost(Transaction t) {
         mLastLayer = -1;
-        mSurfaceFreezer.unfreeze(t);
+        mAnimationLeash = null;
         reassignLayer(t);
         updateSurfacePosition(t);
     }
 
+    @Override
+    public SurfaceControl getAnimationLeash() {
+        return mAnimationLeash;
+    }
+
     private void doAnimationFinished(@AnimationType int type, AnimationAdapter anim) {
         for (int i = 0; i < mSurfaceAnimationSources.size(); ++i) {
             mSurfaceAnimationSources.valueAt(i).onAnimationFinished(type, anim);
diff --git a/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java b/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java
index 0840441..c954700 100644
--- a/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java
+++ b/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java
@@ -43,7 +43,6 @@
     static final boolean DEBUG_CONFIGURATION = false;
     static final boolean DEBUG_STARTING_WINDOW_VERBOSE = false;
     static final boolean DEBUG_WALLPAPER = false;
-    static final boolean DEBUG_WALLPAPER_LIGHT = false || DEBUG_WALLPAPER;
     static final boolean DEBUG_DRAG = true;
     static final boolean DEBUG_SCREENSHOT = false;
     static final boolean DEBUG_LAYOUT_REPEATS = false;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 553e982..2ea0f35 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1134,7 +1134,7 @@
                     atoken.mEnteringAnimation = false;
                     if (atoken.attachedToProcess()) {
                         try {
-                            atoken.app.getThread().scheduleEnterAnimationComplete(atoken.appToken);
+                            atoken.app.getThread().scheduleEnterAnimationComplete(atoken.token);
                         } catch (RemoteException e) {
                         }
                     }
@@ -2069,26 +2069,14 @@
         }
 
         final WindowToken token = win.mToken;
-        final ActivityRecord activity = win.mActivityRecord;
         ProtoLog.v(WM_DEBUG_ADD_REMOVE, "Removing %s from %s", win, token);
         // Window will already be removed from token before this post clean-up method is called.
-        if (token.isEmpty()) {
-            if (!token.mPersistOnEmpty) {
-                token.removeImmediately();
-            } else if (activity != null) {
-                // TODO: Should this be moved into ActivityRecord.removeWindow? Might go away after
-                // re-factor.
-                activity.firstWindowDrawn = false;
-                activity.clearAllDrawn();
-                final Task rootTask = activity.getRootTask();
-                if (rootTask != null) {
-                    rootTask.mExitingActivities.remove(activity);
-                }
-            }
+        if (token.isEmpty() && !token.mPersistOnEmpty) {
+            token.removeImmediately();
         }
 
-        if (activity != null) {
-            activity.postWindowRemoveStartingWindowCleanup(win);
+        if (win.mActivityRecord != null) {
+            win.mActivityRecord.postWindowRemoveStartingWindowCleanup(win);
         }
 
         if (win.mAttrs.type == TYPE_WALLPAPER) {
@@ -2473,7 +2461,8 @@
             if (isPrimaryDisplay) {
                 transformHint = (transformHint + mPrimaryDisplayOrientation) % 4;
             }
-            outSurfaceControl.setTransformHint(transformHint);
+            outSurfaceControl.setTransformHint(
+                    SurfaceControl.rotationToBufferTransform(transformHint));
             ProtoLog.v(WM_DEBUG_ORIENTATION,
                     "Passing transform hint %d for window %s%s",
                     transformHint, win,
@@ -4982,23 +4971,49 @@
         return Surface.ROTATION_0;
     }
 
-    void reportFocusChanged(IBinder oldToken, IBinder newToken) {
-        WindowState lastFocus;
-        WindowState newFocus;
-        synchronized (mGlobalLock) {
-            lastFocus = mInputToWindowMap.get(oldToken);
-            newFocus = mInputToWindowMap.get(newToken);
-            ProtoLog.i(WM_DEBUG_FOCUS_LIGHT, "Focus changing: %s -> %s", lastFocus, newFocus);
+    // Returns an input target which is mapped to the given input token. This can be a WindowState
+    // or an embedded window.
+    @Nullable InputTarget getInputTargetFromToken(IBinder inputToken) {
+        WindowState windowState = mInputToWindowMap.get(inputToken);
+        if (windowState != null) {
+            return windowState;
         }
 
-        if (newFocus != null) {
-            mAnrController.onFocusChanged(newFocus);
-            newFocus.reportFocusChangedSerialized(true);
+        EmbeddedWindowController.EmbeddedWindow embeddedWindow =
+                mEmbeddedWindowController.get(inputToken);
+        if (embeddedWindow != null) {
+            return embeddedWindow;
+        }
+
+        return null;
+    }
+
+    void reportFocusChanged(IBinder oldToken, IBinder newToken) {
+        InputTarget lastTarget;
+        InputTarget newTarget;
+        synchronized (mGlobalLock) {
+            lastTarget = getInputTargetFromToken(oldToken);
+            newTarget = getInputTargetFromToken(newToken);
+            if (newTarget == null && lastTarget == null) {
+                Slog.v(TAG_WM, "Unknown focus tokens, dropping reportFocusChanged");
+                return;
+            }
+
+            mAccessibilityController.onFocusChanged(lastTarget, newTarget);
+            ProtoLog.i(WM_DEBUG_FOCUS_LIGHT, "Focus changing: %s -> %s", lastTarget, newTarget);
+        }
+
+        // Call WindowState focus change observers
+        WindowState newFocusedWindow = newTarget != null ? newTarget.getWindowState() : null;
+        if (newFocusedWindow != null && newFocusedWindow.mInputChannelToken == newToken) {
+            mAnrController.onFocusChanged(newFocusedWindow);
+            newFocusedWindow.reportFocusChangedSerialized(true);
             notifyFocusChanged();
         }
 
-        if (lastFocus != null) {
-            lastFocus.reportFocusChangedSerialized(false);
+        WindowState lastFocusedWindow = lastTarget != null ? lastTarget.getWindowState() : null;
+        if (lastFocusedWindow != null && lastFocusedWindow.mInputChannelToken == oldToken) {
+            lastFocusedWindow.reportFocusChangedSerialized(false);
         }
     }
 
@@ -6392,6 +6407,7 @@
         pw.print("  mGlobalConfiguration="); pw.println(mRoot.getConfiguration());
         pw.print("  mHasPermanentDpad="); pw.println(mHasPermanentDpad);
         mRoot.dumpTopFocusedDisplayId(pw);
+        mRoot.dumpDefaultMinSizeOfResizableTask(pw);
         mRoot.forAllDisplays(dc -> {
             final int displayId = dc.getDisplayId();
             final InsetsControlTarget imeLayeringTarget = dc.getImeTarget(IME_TARGET_LAYERING);
@@ -7045,42 +7061,6 @@
         }
     }
 
-    private void checkCallerOwnsDisplay(int displayId) {
-        final Display display = mDisplayManager.getDisplay(displayId);
-        if (display == null) {
-            throw new IllegalArgumentException(
-                    "Cannot find display for non-existent displayId: " + displayId);
-        }
-
-        final int callingUid = Binder.getCallingUid();
-        final int displayOwnerUid = display.getOwnerUid();
-        if (callingUid != displayOwnerUid) {
-            throw new SecurityException("The caller doesn't own the display.");
-        }
-    }
-
-    /** @see Session#updateDisplayContentLocation(IWindow, int, int, int)  */
-    void updateDisplayContentLocation(IWindow client, int x, int y, int displayId) {
-        checkCallerOwnsDisplay(displayId);
-
-        synchronized (mGlobalLock) {
-            final long token = Binder.clearCallingIdentity();
-            try {
-                final WindowState win = windowForClientLocked(null, client, false);
-                if (win == null) {
-                    ProtoLog.w(WM_ERROR, "Bad requesting window %s", client);
-                    return;
-                }
-                final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
-                if (displayContent != null) {
-                    displayContent.updateLocation(win, x, y);
-                }
-            } finally {
-                Binder.restoreCallingIdentity(token);
-            }
-        }
-    }
-
     /**
      * Update a tap exclude region in the window identified by the provided id. Touches down on this
      * region will not:
@@ -7159,29 +7139,6 @@
     }
 
     @Override
-    public void dontOverrideDisplayInfo(int displayId) {
-        final long token = Binder.clearCallingIdentity();
-        try {
-            synchronized (mGlobalLock) {
-                final DisplayContent dc = getDisplayContentOrCreate(displayId, null /* token */);
-                if (dc == null) {
-                    throw new IllegalArgumentException(
-                            "Trying to configure a non existent display.");
-                }
-                // We usually set the override info in DisplayManager so that we get consistent
-                // values when displays are changing. However, we don't do this for displays that
-                // serve as containers for ActivityViews because we don't want letter-/pillar-boxing
-                // during resize.
-                dc.mShouldOverrideDisplayConfiguration = false;
-                mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(displayId,
-                        null /* info */);
-            }
-        } finally {
-            Binder.restoreCallingIdentity(token);
-        }
-    }
-
-    @Override
     public int getWindowingMode(int displayId) {
         if (!checkCallingPermission(INTERNAL_SYSTEM_WINDOW, "getWindowingMode()")) {
             throw new SecurityException("Requires INTERNAL_SYSTEM_WINDOW permission");
@@ -7522,11 +7479,7 @@
         @Override
         public IBinder getFocusedWindowToken() {
             synchronized (mGlobalLock) {
-                WindowState windowState = getFocusedWindowLocked();
-                if (windowState != null) {
-                    return windowState.mClient.asBinder();
-                }
-                return null;
+                return mAccessibilityController.getFocusedWindowToken();
             }
         }
 
@@ -8270,7 +8223,7 @@
             clientChannel = win.openInputChannel();
             mEmbeddedWindowController.add(clientChannel.getToken(), win);
             applicationHandle = win.getApplicationHandle();
-            name = win.getName();
+            name = win.toString();
         }
 
         updateInputChannel(clientChannel.getToken(), callingUid, callingPid, displayId, surface,
@@ -8338,7 +8291,7 @@
                 Slog.e(TAG, "Couldn't find window for provided channelToken.");
                 return;
             }
-            name = win.getName();
+            name = win.toString();
             applicationHandle = win.getApplicationHandle();
         }
 
@@ -8547,10 +8500,9 @@
             SurfaceControl.Transaction t = mTransactionFactory.get();
             final int displayId = embeddedWindow.mDisplayId;
             if (grantFocus) {
-                t.setFocusedWindow(inputToken, embeddedWindow.getName(), displayId).apply();
+                t.setFocusedWindow(inputToken, embeddedWindow.toString(), displayId).apply();
                 EventLog.writeEvent(LOGTAG_INPUT_FOCUS,
-                        "Focus request " + embeddedWindow.getName(),
-                        "reason=grantEmbeddedWindowFocus(true)");
+                        "Focus request " + embeddedWindow, "reason=grantEmbeddedWindowFocus(true)");
             } else {
                 // Search for a new focus target
                 DisplayContent displayContent = mRoot.getDisplayContent(displayId);
@@ -8559,18 +8511,18 @@
                 if (newFocusTarget == null) {
                     ProtoLog.v(WM_DEBUG_FOCUS, "grantEmbeddedWindowFocus remove request for "
                                     + "win=%s dropped since no candidate was found",
-                            embeddedWindow.getName());
+                            embeddedWindow);
                     return;
                 }
                 t.requestFocusTransfer(newFocusTarget.mInputChannelToken, newFocusTarget.getName(),
-                        inputToken, embeddedWindow.getName(),
+                        inputToken, embeddedWindow.toString(),
                         displayId).apply();
                 EventLog.writeEvent(LOGTAG_INPUT_FOCUS,
                         "Transfer focus request " + newFocusTarget,
                         "reason=grantEmbeddedWindowFocus(false)");
             }
             ProtoLog.v(WM_DEBUG_FOCUS, "grantEmbeddedWindowFocus win=%s grantFocus=%s",
-                    embeddedWindow.getName(), grantFocus);
+                    embeddedWindow, grantFocus);
         }
     }
 
@@ -8599,24 +8551,24 @@
             }
             SurfaceControl.Transaction t = mTransactionFactory.get();
             if (grantFocus) {
-                t.requestFocusTransfer(targetInputToken, embeddedWindow.getName(),
+                t.requestFocusTransfer(targetInputToken, embeddedWindow.toString(),
                         hostWindow.mInputChannel.getToken(),
                         hostWindow.getName(),
                         hostWindow.getDisplayId()).apply();
                 EventLog.writeEvent(LOGTAG_INPUT_FOCUS,
-                        "Transfer focus request " + embeddedWindow.getName(),
+                        "Transfer focus request " + embeddedWindow,
                         "reason=grantEmbeddedWindowFocus(true)");
             } else {
                 t.requestFocusTransfer(hostWindow.mInputChannel.getToken(), hostWindow.getName(),
                         targetInputToken,
-                        embeddedWindow.getName(),
+                        embeddedWindow.toString(),
                         hostWindow.getDisplayId()).apply();
                 EventLog.writeEvent(LOGTAG_INPUT_FOCUS,
                         "Transfer focus request " + hostWindow,
                         "reason=grantEmbeddedWindowFocus(false)");
             }
             ProtoLog.v(WM_DEBUG_FOCUS, "grantEmbeddedWindowFocus win=%s grantFocus=%s",
-                    embeddedWindow.getName(), grantFocus);
+                    embeddedWindow, grantFocus);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 781b53d..968c635 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -83,7 +83,6 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Consumer;
 
 /**
  * Server side implementation for the interface for organizing windows
@@ -117,7 +116,7 @@
     final DisplayAreaOrganizerController mDisplayAreaOrganizerController;
     final TaskFragmentOrganizerController mTaskFragmentOrganizerController;
 
-    final TransitionController mTransitionController;
+    TransitionController mTransitionController;
     /**
      * A Map which manages the relationship between
      * {@link TaskFragmentCreationParams#getFragmentToken()} and {@link TaskFragment}
@@ -131,7 +130,10 @@
         mTaskOrganizerController = new TaskOrganizerController(mService);
         mDisplayAreaOrganizerController = new DisplayAreaOrganizerController(mService);
         mTaskFragmentOrganizerController = new TaskFragmentOrganizerController(atm);
-        mTransitionController = new TransitionController(atm);
+    }
+
+    void setWindowManager(WindowManagerService wms) {
+        mTransitionController = new TransitionController(mService, wms.mTaskSnapshotController);
     }
 
     TransitionController getTransitionController() {
@@ -847,7 +849,7 @@
 
     private int reparentChildrenTasksHierarchyOp(WindowContainerTransaction.HierarchyOp hop,
             @Nullable Transition transition, int syncId) {
-        WindowContainer currentParent = hop.getContainer() != null
+        WindowContainer<?> currentParent = hop.getContainer() != null
                 ? WindowContainer.fromBinder(hop.getContainer()) : null;
         WindowContainer newParent = hop.getNewParent() != null
                 ? WindowContainer.fromBinder(hop.getNewParent()) : null;
@@ -890,24 +892,31 @@
         // We want to collect the tasks first before re-parenting to avoid array shifting on us.
         final ArrayList<Task> tasksToReparent = new ArrayList<>();
 
-        currentParent.forAllTasks((Consumer<Task>) (task) -> {
+        currentParent.forAllTasks(task -> {
             Slog.i(TAG, " Processing task=" + task);
-            if (task.mCreatedByOrganizer
-                    || task.getParent() != finalCurrentParent) {
+            final boolean reparent;
+            if (task.mCreatedByOrganizer || task.getParent() != finalCurrentParent) {
                 // We only care about non-organized task that are direct children of the thing we
                 // are reparenting from.
-                return;
+                return false;
             }
             if (newParentInMultiWindow && !task.supportsMultiWindowInDisplayArea(newParentTda)) {
                 Slog.e(TAG, "reparentChildrenTasksHierarchyOp non-resizeable task to multi window,"
                         + " task=" + task);
-                return;
+                return false;
             }
-            if (!ArrayUtils.contains(hop.getActivityTypes(), task.getActivityType())) return;
-            if (!ArrayUtils.contains(hop.getWindowingModes(), task.getWindowingMode())) return;
+            if (!ArrayUtils.contains(hop.getActivityTypes(), task.getActivityType())
+                    || !ArrayUtils.contains(hop.getWindowingModes(), task.getWindowingMode())) {
+                return false;
+            }
 
-            tasksToReparent.add(task);
-        }, !hop.getToTop());
+            if (hop.getToTop()) {
+                tasksToReparent.add(0, task);
+            } else {
+                tasksToReparent.add(task);
+            }
+            return hop.getReparentTopOnly() && tasksToReparent.size() == 1;
+        });
 
         final int count = tasksToReparent.size();
         for (int i = 0; i < count; ++i) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index f966cc1..f77b6f2 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -216,7 +216,6 @@
 import android.os.WorkSource;
 import android.provider.Settings;
 import android.text.TextUtils;
-import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.util.MergedConfiguration;
 import android.util.Slog;
@@ -226,7 +225,6 @@
 import android.view.Display;
 import android.view.DisplayInfo;
 import android.view.Gravity;
-import android.view.IApplicationToken;
 import android.view.IWindow;
 import android.view.IWindowFocusObserver;
 import android.view.IWindowId;
@@ -273,7 +271,7 @@
 
 /** A window in the window manager. */
 class WindowState extends WindowContainer<WindowState> implements WindowManagerPolicy.WindowState,
-        InsetsControlTarget {
+        InsetsControlTarget, InputTarget {
     static final String TAG = TAG_WITH_CLASS_NAME ? "WindowState" : TAG_WM;
 
     // The minimal size of a window within the usable area of the freeform root task.
@@ -405,9 +403,6 @@
 
     int mLayoutSeq = -1;
 
-    /** @see #addEmbeddedDisplayContent(DisplayContent dc) */
-    private final ArraySet<DisplayContent> mEmbeddedDisplayContents = new ArraySet<>();
-
     /**
      * Used to store last reported to client configuration and check if we have newer available.
      * We'll send configuration to client only if it is different from the last applied one and
@@ -669,12 +664,6 @@
     private final Transaction mTmpTransaction;
 
     /**
-     * If a window is on a display which has been re-parented to a view in another window,
-     * use this offset to indicate the correct location.
-     */
-    private final Point mLastReportedDisplayOffset = new Point();
-
-    /**
      * Whether the window was resized by us while it was gone for layout.
      */
     boolean mResizedWhileGone = false;
@@ -1516,11 +1505,6 @@
         return getTopParentWindow().mAttrs.type;
     }
 
-    @Override
-    public IApplicationToken getAppToken() {
-        return mActivityRecord != null ? mActivityRecord.appToken : null;
-    }
-
     /** Returns true if this window is participating in voice interaction. */
     boolean isVoiceInteraction() {
         return mActivityRecord != null && mActivityRecord.mVoiceInteraction;
@@ -1727,7 +1711,8 @@
         return state;
     }
 
-    int getDisplayId() {
+    @Override
+    public int getDisplayId() {
         final DisplayContent displayContent = getDisplayContent();
         if (displayContent == null) {
             return Display.INVALID_DISPLAY;
@@ -1735,6 +1720,21 @@
         return displayContent.getDisplayId();
     }
 
+    @Override
+    public WindowState getWindowState() {
+        return this;
+    }
+
+    @Override
+    public IWindow getIWindow() {
+        return mClient;
+    }
+
+    @Override
+    public int getPid() {
+        return mSession.mPid;
+    }
+
     Task getTask() {
         return mActivityRecord != null ? mActivityRecord.getTask() : null;
     }
@@ -2290,7 +2290,6 @@
         if (mWmService.mAccessibilityController.hasCallbacks()) {
             mWmService.mAccessibilityController.onSomeWindowResizedOrMoved(getDisplayId());
         }
-        updateLocationInParentDisplayIfNeeded();
 
         try {
             mClient.moved(left, top);
@@ -2486,7 +2485,7 @@
             // Cancel the remove starting window animation on shell. The main window might changed
             // during animating, checking for all windows would be safer.
             if (mActivityRecord != null) {
-                mActivityRecord.forAllWindowsUnchecked(w -> {
+                mActivityRecord.forAllWindows(w -> {
                     if (w.isSelfAnimating(0, ANIMATION_TYPE_STARTING_REVEAL)) {
                         w.cancelAnimation();
                         return true;
@@ -2637,29 +2636,7 @@
         mHasSurface = hasSurface;
     }
 
-    /**
-     * Checks whether one of the Windows in a Display embedded in this Window can be an IME target.
-     */
-    private boolean canWindowInEmbeddedDisplayBeImeTarget() {
-        final int embeddedDisplayContentsSize = mEmbeddedDisplayContents.size();
-        for (int i = embeddedDisplayContentsSize - 1; i >= 0; i--) {
-            final DisplayContent edc = mEmbeddedDisplayContents.valueAt(i);
-            if (edc.forAllWindows(WindowState::canBeImeTarget, true)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     boolean canBeImeTarget() {
-        // If any of the embedded windows can be the IME target, this window will be the final IME
-        // target. This is because embedded windows are on a different display in WM so it would
-        // cause confusion trying to set the IME to a window on a different display. Instead, just
-        // make the host window the IME target.
-        if (canWindowInEmbeddedDisplayBeImeTarget()) {
-            return true;
-        }
-
         if (mIsImWindow) {
             // IME windows can't be IME targets. IME targets are required to be below the IME
             // windows and that wouldn't be possible if the IME window is its own target...silly.
@@ -3945,7 +3922,6 @@
             if (mWmService.mAccessibilityController.hasCallbacks()) {
                 mWmService.mAccessibilityController.onSomeWindowResizedOrMoved(displayId);
             }
-            updateLocationInParentDisplayIfNeeded();
         } catch (RemoteException e) {
             // Cancel orientation change of this window to avoid blocking unfreeze display.
             setOrientationChanging(false);
@@ -3960,36 +3936,6 @@
         return mClient instanceof IWindow.Stub;
     }
 
-    void updateLocationInParentDisplayIfNeeded() {
-        final int embeddedDisplayContentsSize = mEmbeddedDisplayContents.size();
-        // If there is any embedded display which is re-parented to this window, we need to
-        // notify all windows in the embedded display about the location change.
-        if (embeddedDisplayContentsSize != 0) {
-            for (int i = embeddedDisplayContentsSize - 1; i >= 0; i--) {
-                final DisplayContent edc = mEmbeddedDisplayContents.valueAt(i);
-                edc.notifyLocationInParentDisplayChanged();
-            }
-        }
-        // If this window is in a embedded display which is re-parented to another window,
-        // we may need to update its correct on-screen location.
-        final DisplayContent dc = getDisplayContent();
-        if (dc.getParentWindow() == null) {
-            return;
-        }
-
-        final Point offset = dc.getLocationInParentDisplay();
-        if (mLastReportedDisplayOffset.equals(offset)) {
-            return;
-        }
-
-        mLastReportedDisplayOffset.set(offset.x, offset.y);
-        try {
-            mClient.locationInParentDisplayChanged(mLastReportedDisplayOffset);
-        } catch (RemoteException e) {
-            Slog.e(TAG, "Failed to update offset from DisplayContent", e);
-        }
-    }
-
     /**
      * Called when the insets state changed.
      */
@@ -4423,9 +4369,6 @@
         }
         pw.println(prefix + "isOnScreen=" + isOnScreen());
         pw.println(prefix + "isVisible=" + isVisible());
-        if (!mEmbeddedDisplayContents.isEmpty()) {
-            pw.println(prefix + "mEmbeddedDisplayContents=" + mEmbeddedDisplayContents);
-        }
         if (dumpAll) {
             final String visibilityString = mRequestedVisibilities.toString();
             if (!visibilityString.isEmpty()) {
@@ -4822,7 +4765,7 @@
         windowInfo.layer = mLayer;
         windowInfo.token = mClient.asBinder();
         if (mActivityRecord != null) {
-            windowInfo.activityToken = mActivityRecord.appToken.asBinder();
+            windowInfo.activityToken = mActivityRecord.token;
         }
         windowInfo.title = mAttrs.accessibilityTitle;
         // Panel windows have no public way to set the a11y title directly. Use the
@@ -5251,28 +5194,6 @@
         return mLayoutSeq != -1;
     }
 
-    /**
-     * Add the DisplayContent of the embedded display which is re-parented to this window to
-     * the list of embedded displays.
-     *
-     * @param dc DisplayContent of the re-parented embedded display.
-     * @return {@code true} if the giving DisplayContent is added, {@code false} otherwise.
-     */
-    boolean addEmbeddedDisplayContent(DisplayContent dc) {
-        return mEmbeddedDisplayContents.add(dc);
-    }
-
-    /**
-     * Remove the DisplayContent of the embedded display which is re-parented to this window from
-     * the list of embedded displays.
-     *
-     * @param dc DisplayContent of the re-parented embedded display.
-     * @return {@code true} if the giving DisplayContent is removed, {@code false} otherwise.
-     */
-    boolean removeEmbeddedDisplayContent(DisplayContent dc) {
-        return mEmbeddedDisplayContents.remove(dc);
-    }
-
     /** Updates the last frames and relative frames to the current ones. */
     void updateLastFrames() {
         mWindowFrames.mLastFrame.set(mWindowFrames.mFrame);
@@ -5358,18 +5279,6 @@
         int x = mSurfacePosition.x + mTmpPoint.x;
         int y = mSurfacePosition.y + mTmpPoint.y;
 
-        // We might be on a display which has been re-parented to a view in another window, so here
-        // computes the global location of our display.
-        DisplayContent dc = getDisplayContent();
-        while (dc != null && dc.getParentWindow() != null) {
-            final WindowState displayParent = dc.getParentWindow();
-            x += displayParent.mWindowFrames.mFrame.left
-                    + (dc.getLocationInParentWindow().x * displayParent.mGlobalScale + 0.5f);
-            y += displayParent.mWindowFrames.mFrame.top
-                    + (dc.getLocationInParentWindow().y * displayParent.mGlobalScale + 0.5f);
-            dc = displayParent.getDisplayContent();
-        }
-
         // If changed, also adjust transformFrameToSurfacePosition
         final WindowContainer parent = getParent();
         if (isChildWindow()) {
@@ -5681,7 +5590,7 @@
         // elevating the IME and windows above it's target above the docked divider in
         // split-screen, or make the popupMenu to be above the IME when the parent window is the
         // IME layering target in bubble/freeform mode.
-        if (mDisplayContent.isImeAttachedToApp()) {
+        if (mDisplayContent.shouldImeAttachedToApp()) {
             return false;
         }
 
@@ -5706,8 +5615,8 @@
     }
 
     /**
-     * Get IME target that should host IME when this window's display has a parent.
-     * Note: IME is never hosted by a display that has a parent.
+     * Get IME target that should host IME.
+     * Note: IME is never hosted by a display that doesn't support IME/system decorations.
      * When window calling
      * {@link android.view.inputmethod.InputMethodManager#showSoftInput(View, int)} is unknown,
      * use {@link DisplayContent#getImeControlTarget()} instead.
@@ -5716,11 +5625,7 @@
      *         When window is doesn't have a parent, it is returned as-is.
      */
     InsetsControlTarget getImeControlTarget() {
-        final DisplayContent dc = getDisplayContent();
-        final WindowState parentWindow = dc.getParentWindow();
-
-        // If target's display has a parent, IME is displayed in the parent display.
-        return dc.getImeHostOrFallback(parentWindow != null ? parentWindow : this);
+        return getDisplayContent().getImeHostOrFallback(this);
     }
 
     @Override
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 59f831e..34576a6 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -10683,7 +10683,10 @@
             }
         }
 
-        if (!mOwners.hasDeviceOwner() || !user.isFull() || user.isManagedProfile()) return;
+        if (!mOwners.hasDeviceOwner() || !user.isFull() || user.isManagedProfile()
+                || user.isGuest()) {
+            return;
+        }
 
         if (mInjector.userManagerIsHeadlessSystemUserMode()) {
             ComponentName admin = mOwners.getDeviceOwnerComponent();
@@ -10698,7 +10701,7 @@
     }
 
     @Override
-    public void resetNewUserDisclaimer() {
+    public void acknowledgeNewUserDisclaimer() {
         CallerIdentity callerIdentity = getCallerIdentity();
         canManageUsers(callerIdentity);
 
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index dae1275..4a2b51d 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -114,6 +114,7 @@
 import com.android.server.broadcastradio.BroadcastRadioService;
 import com.android.server.camera.CameraServiceProxy;
 import com.android.server.clipboard.ClipboardService;
+import com.android.server.communal.CommunalManagerService;
 import com.android.server.compat.PlatformCompat;
 import com.android.server.compat.PlatformCompatNative;
 import com.android.server.connectivity.PacProxyService;
@@ -133,6 +134,7 @@
 import com.android.server.inputmethod.InputMethodManagerService;
 import com.android.server.integrity.AppIntegrityManagerService;
 import com.android.server.lights.LightsService;
+import com.android.server.locales.LocaleManagerService;
 import com.android.server.location.LocationManagerService;
 import com.android.server.media.MediaRouterService;
 import com.android.server.media.metrics.MediaMetricsManagerService;
@@ -1684,6 +1686,15 @@
         mSystemServiceManager.startService(UiModeManagerService.class);
         t.traceEnd();
 
+        t.traceBegin("StartLocaleManagerService");
+        try {
+            mSystemServiceManager.startService(LocaleManagerService.class);
+        } catch (Throwable e) {
+            reportWtf("starting LocaleManagerService service", e);
+        }
+        t.traceEnd();
+
+
         if (!mOnlyCore) {
             t.traceBegin("UpdatePackagesIfNeeded");
             try {
@@ -2687,6 +2698,12 @@
         mSystemServiceManager.startService(APP_COMPAT_OVERRIDES_SERVICE_CLASS);
         t.traceEnd();
 
+        if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_COMMUNAL_MODE)) {
+            t.traceBegin("CommunalManagerService");
+            mSystemServiceManager.startService(CommunalManagerService.class);
+            t.traceEnd();
+        }
+
         // These are needed to propagate to the runnable below.
         final NetworkManagementService networkManagementF = networkManagement;
         final NetworkStatsService networkStatsF = networkStats;
diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java
index ef1201e..e6fd916 100644
--- a/services/people/java/com/android/server/people/data/DataManager.java
+++ b/services/people/java/com/android/server/people/data/DataManager.java
@@ -161,7 +161,8 @@
 
         mStatusExpReceiver = new ConversationStatusExpirationBroadcastReceiver();
         mContext.registerReceiver(mStatusExpReceiver,
-                ConversationStatusExpirationBroadcastReceiver.getFilter());
+                ConversationStatusExpirationBroadcastReceiver.getFilter(),
+                Context.RECEIVER_NOT_EXPORTED);
 
         IntentFilter shutdownIntentFilter = new IntentFilter(Intent.ACTION_SHUTDOWN);
         BroadcastReceiver shutdownBroadcastReceiver = new ShutdownBroadcastReceiver();
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 680e6db..6bb127a 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -45,7 +45,6 @@
         "service-jobscheduler",
         "service-permission.impl",
         "service-blobstore",
-        "service-appsearch",
         "androidx.test.core",
         "androidx.test.runner",
         "androidx.test.ext.truth",
diff --git a/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micMute_camMute.xml b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micMute_camMute.xml
new file mode 100644
index 0000000..a4de08a
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micMute_camMute.xml
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+<sensor-privacy persistence-version="1" version="1">
+    <user id="0" enabled="false">
+        <individual-sensor-privacy sensor="1" enabled="true" />
+        <individual-sensor-privacy sensor="2" enabled="true" />
+    </user>
+</sensor-privacy>
diff --git a/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micMute_camUnmute.xml b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micMute_camUnmute.xml
new file mode 100644
index 0000000..47649d7
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micMute_camUnmute.xml
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+<sensor-privacy persistence-version="1" version="1">
+    <user id="0" enabled="false">
+        <individual-sensor-privacy sensor="1" enabled="true" />
+        <individual-sensor-privacy sensor="2" enabled="false" />
+    </user>
+</sensor-privacy>
diff --git a/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micUnmute_camMute.xml b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micUnmute_camMute.xml
new file mode 100644
index 0000000..4fd9ebf
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micUnmute_camMute.xml
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+<sensor-privacy persistence-version="1" version="1">
+    <user id="0" enabled="false">
+        <individual-sensor-privacy sensor="1" enabled="false" />
+        <individual-sensor-privacy sensor="2" enabled="true" />
+    </user>
+</sensor-privacy>
diff --git a/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micUnmute_camUnmute.xml b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micUnmute_camUnmute.xml
new file mode 100644
index 0000000..e8f9edf
--- /dev/null
+++ b/services/tests/mockingservicestests/assets/SensorPrivacyServiceMockingTest/persisted_file_micUnmute_camUnmute.xml
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+<sensor-privacy persistence-version="1" version="1">
+    <user id="0" enabled="false">
+        <individual-sensor-privacy sensor="1" enabled="false" />
+        <individual-sensor-privacy sensor="2" enabled="false" />
+    </user>
+</sensor-privacy>
diff --git a/services/tests/mockingservicestests/src/com/android/server/communal/CommunalManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/communal/CommunalManagerServiceTest.java
new file mode 100644
index 0000000..5e0cbca
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/communal/CommunalManagerServiceTest.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.communal;
+
+import static android.content.pm.ActivityInfo.FLAG_SHOW_WHEN_LOCKED;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.wm.ActivityInterceptorCallback.COMMUNAL_MODE_ORDERED_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.app.KeyguardManager;
+import android.app.communal.ICommunalManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.LocalServices;
+import com.android.server.wm.ActivityInterceptorCallback;
+import com.android.server.wm.ActivityTaskManagerInternal;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.quality.Strictness;
+
+/**
+ * Test class for {@link CommunalManagerService}.
+ *
+ * Build/Install/Run:
+ *   atest FrameworksMockingServicesTests:CommunalManagerServiceTest
+ */
+@RunWith(MockitoJUnitRunner.class)
+@SmallTest
+@Presubmit
+public class CommunalManagerServiceTest {
+    private static final int TEST_USER_ID = 1;
+    private static final int TEST_REAL_CALLING_UID = 2;
+    private static final int TEST_REAL_CALLING_PID = 3;
+    private static final String TEST_CALLING_PACKAGE = "com.test.caller";
+    private static final String TEST_PACKAGE_NAME = "com.test.package";
+
+    private MockitoSession mMockingSession;
+    private CommunalManagerService mService;
+
+    @Mock
+    private ActivityTaskManagerInternal mAtmInternal;
+    @Mock
+    private KeyguardManager mKeyguardManager;
+    @Mock
+    private Context mMockContext;
+    @Mock
+    private ContentResolver mContentResolver;
+
+    private ActivityInterceptorCallback mActivityInterceptorCallback;
+    private ActivityInfo mAInfo;
+    private ICommunalManager mBinder;
+
+    @Before
+    public final void setUp() {
+        mMockingSession = mockitoSession()
+                .initMocks(this)
+                .mockStatic(LocalServices.class)
+                .mockStatic(ServiceManager.class)
+                .mockStatic(Settings.Secure.class)
+                .mockStatic(KeyguardManager.class)
+                .strictness(Strictness.WARN)
+                .startMocking();
+
+        when(mMockContext.getContentResolver()).thenReturn(mContentResolver);
+        when(mMockContext.getSystemService(KeyguardManager.class)).thenReturn(mKeyguardManager);
+        doReturn(mAtmInternal).when(() -> LocalServices.getService(
+                ActivityTaskManagerInternal.class));
+
+        mService = new CommunalManagerService(mMockContext);
+        mService.onStart();
+
+        ArgumentCaptor<ActivityInterceptorCallback> activityInterceptorCaptor =
+                ArgumentCaptor.forClass(ActivityInterceptorCallback.class);
+        verify(mAtmInternal).registerActivityStartInterceptor(eq(COMMUNAL_MODE_ORDERED_ID),
+                activityInterceptorCaptor.capture());
+        mActivityInterceptorCallback = activityInterceptorCaptor.getValue();
+
+        ArgumentCaptor<IBinder> binderCaptor = ArgumentCaptor.forClass(IBinder.class);
+        verify(() -> ServiceManager.addService(eq(Context.COMMUNAL_MANAGER_SERVICE),
+                binderCaptor.capture(),
+                anyBoolean(), anyInt()));
+        mBinder = ICommunalManager.Stub.asInterface(binderCaptor.getValue());
+
+        mAInfo = new ActivityInfo();
+        mAInfo.applicationInfo = new ApplicationInfo();
+        mAInfo.packageName = mAInfo.applicationInfo.packageName = TEST_PACKAGE_NAME;
+    }
+
+    @After
+    public void tearDown() {
+        if (mMockingSession != null) {
+            mMockingSession.finishMocking();
+        }
+    }
+
+    private ActivityInterceptorCallback.ActivityInterceptorInfo buildActivityInfo(Intent intent) {
+        return new ActivityInterceptorCallback.ActivityInterceptorInfo(
+                TEST_REAL_CALLING_UID,
+                TEST_REAL_CALLING_PID,
+                TEST_USER_ID,
+                TEST_CALLING_PACKAGE,
+                "featureId",
+                intent,
+                null,
+                mAInfo,
+                "resolvedType",
+                TEST_REAL_CALLING_PID,
+                TEST_REAL_CALLING_UID,
+                null);
+    }
+
+    private void allowPackages(String packages) {
+        doReturn(packages).when(
+                () -> Settings.Secure.getStringForUser(mContentResolver,
+                        Settings.Secure.COMMUNAL_MODE_PACKAGES, UserHandle.USER_SYSTEM));
+        mService.updateSelectedApps();
+    }
+
+    @Test
+    public void testIntercept_unlocked_communalOff_appNotEnabled_showWhenLockedOff() {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(false);
+        mAInfo.flags = 0;
+        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+    }
+
+    @Test
+    public void testIntercept_unlocked_communalOn_appNotEnabled_showWhenLockedOff()
+            throws RemoteException {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        mBinder.setCommunalViewShowing(true);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(false);
+        mAInfo.flags = 0;
+        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+    }
+
+    @Test
+    public void testIntercept_locked_communalOff_appNotEnabled_showWhenLockedOff() {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        mAInfo.flags = 0;
+        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+    }
+
+    @Test
+    public void testIntercept_locked_communalOn_appNotEnabled_showWhenLockedOff()
+            throws RemoteException {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        mBinder.setCommunalViewShowing(true);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        mAInfo.flags = 0;
+        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNotNull();
+    }
+
+    @Test
+    public void testIntercept_locked_communalOn_appNotEnabled_showWhenLockedOn()
+            throws RemoteException {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        mBinder.setCommunalViewShowing(true);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        mAInfo.flags = FLAG_SHOW_WHEN_LOCKED;
+
+        allowPackages("package1,package2");
+        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNotNull();
+    }
+
+    @Test
+    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOff()
+            throws RemoteException {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        mBinder.setCommunalViewShowing(true);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        mAInfo.flags = 0;
+
+        allowPackages(TEST_PACKAGE_NAME);
+        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNotNull();
+    }
+
+    @Test
+    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOn()
+            throws RemoteException {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        mBinder.setCommunalViewShowing(true);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        mAInfo.flags = FLAG_SHOW_WHEN_LOCKED;
+
+        allowPackages(TEST_PACKAGE_NAME);
+        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+    }
+}
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
new file mode 100644
index 0000000..98e089e
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2018 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.job.controllers;
+
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.eq;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.provider.DeviceConfig;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.controllers.PrefetchController.PcConstants;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
+
+import java.time.Clock;
+import java.time.Duration;
+import java.time.ZoneOffset;
+import java.util.concurrent.Executor;
+
+@RunWith(AndroidJUnit4.class)
+public class PrefetchControllerTest {
+    private PrefetchController mPrefetchController;
+    private PcConstants mPcConstants;
+    private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder;
+
+    private MockitoSession mMockingSession;
+    @Mock
+    private Context mContext;
+    @Mock
+    private JobSchedulerService mJobSchedulerService;
+
+    @Before
+    public void setUp() {
+        mMockingSession = mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .spyStatic(DeviceConfig.class)
+                .mockStatic(LocalServices.class)
+                .startMocking();
+
+        // Called in StateController constructor.
+        when(mJobSchedulerService.getTestableContext()).thenReturn(mContext);
+        when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService);
+        // Used in PrefetchController.PcConstants
+        doAnswer((Answer<Void>) invocationOnMock -> null)
+                .when(() -> DeviceConfig.addOnPropertiesChangedListener(
+                        anyString(), any(Executor.class),
+                        any(DeviceConfig.OnPropertiesChangedListener.class)));
+        mDeviceConfigPropertiesBuilder =
+                new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
+        doAnswer(
+                (Answer<DeviceConfig.Properties>) invocationOnMock
+                        -> mDeviceConfigPropertiesBuilder.build())
+                .when(() -> DeviceConfig.getProperties(
+                        eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.<String>any()));
+
+        // Freeze the clocks at 24 hours after this moment in time. Several tests create sessions
+        // in the past, and PrefetchController sometimes floors values at 0, so if the test time
+        // causes sessions with negative timestamps, they will fail.
+        JobSchedulerService.sSystemClock =
+                getShiftedClock(Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC),
+                        24 * HOUR_IN_MILLIS);
+        JobSchedulerService.sUptimeMillisClock = getShiftedClock(
+                Clock.fixed(SystemClock.uptimeClock().instant(), ZoneOffset.UTC),
+                24 * HOUR_IN_MILLIS);
+        JobSchedulerService.sElapsedRealtimeClock = getShiftedClock(
+                Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC),
+                24 * HOUR_IN_MILLIS);
+
+        // Initialize real objects.
+        // Capture the listeners.
+        mPrefetchController = new PrefetchController(mJobSchedulerService);
+        mPcConstants = mPrefetchController.getPcConstants();
+    }
+
+    @After
+    public void tearDown() {
+        if (mMockingSession != null) {
+            mMockingSession.finishMocking();
+        }
+    }
+
+    private Clock getShiftedClock(Clock clock, long incrementMs) {
+        return Clock.offset(clock, Duration.ofMillis(incrementMs));
+    }
+
+    private void setDeviceConfigLong(String key, long val) {
+        mDeviceConfigPropertiesBuilder.setLong(key, val);
+        synchronized (mPrefetchController.mLock) {
+            mPrefetchController.prepareForUpdatedConstantsLocked();
+            mPcConstants.processConstantLocked(mDeviceConfigPropertiesBuilder.build(), key);
+        }
+    }
+
+    @Test
+    public void testConstantsUpdating_ValidValues() {
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 5 * HOUR_IN_MILLIS);
+
+        assertEquals(5 * HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs());
+    }
+
+    @Test
+    public void testConstantsUpdating_InvalidValues() {
+        // Test negatives/too low.
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 4 * MINUTE_IN_MILLIS);
+
+        assertEquals(HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs());
+
+        // Test larger than a day. Controller should cap at one day.
+        setDeviceConfigLong(PcConstants.KEY_LAUNCH_TIME_THRESHOLD_MS, 25 * HOUR_IN_MILLIS);
+
+        assertEquals(24 * HOUR_IN_MILLIS, mPrefetchController.getLaunchTimeThresholdMs());
+    }
+}
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 4738159..300f93f 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
@@ -178,7 +178,7 @@
             fail("registerUidObserver threw exception: " + e.getMessage());
         }
         when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
-        when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager);
+        when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager);
         doReturn(mActivityMangerInternal)
                 .when(() -> LocalServices.getService(ActivityManagerInternal.class));
         doReturn(mock(AppStandbyInternal.class))
@@ -2128,7 +2128,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
         // Test with timing sessions out of window but still under max execution limit.
@@ -2144,7 +2145,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                 createTimingSession(now - 2 * HOUR_IN_MILLIS, 55 * MINUTE_IN_MILLIS, 1), false);
@@ -2152,7 +2154,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         JobStatus jobStatus = createJobStatus("testMaybeScheduleStartAlarmLocked_Active", 1);
         setStandbyBucket(standbyBucket, jobStatus);
@@ -2169,8 +2172,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        verify(mAlarmManager, times(1)).set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK),
-                any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     @Test
@@ -2187,7 +2190,8 @@
             // No sessions saved yet.
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions out of window.
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -2196,7 +2200,8 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions in window but still in quota.
         final long end = now - (2 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS);
@@ -2208,7 +2213,8 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Add some more sessions, but still in quota.
         mQuotaController.saveTimingSession(0, "com.android.test",
@@ -2218,7 +2224,8 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test when out of quota.
         mQuotaController.saveTimingSession(0, "com.android.test",
@@ -2226,15 +2233,15 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     @Test
@@ -2251,7 +2258,8 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions out of window.
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -2260,7 +2268,8 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions in window but still in quota.
         final long start = now - (6 * HOUR_IN_MILLIS);
@@ -2270,7 +2279,8 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Add some more sessions, but still in quota.
         mQuotaController.saveTimingSession(0, "com.android.test",
@@ -2280,7 +2290,8 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test when out of quota.
         mQuotaController.saveTimingSession(0, "com.android.test",
@@ -2288,15 +2299,15 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     /**
@@ -2319,7 +2330,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions out of window.
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -2329,7 +2341,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions in window but still in quota.
         final long start = now - (6 * HOUR_IN_MILLIS);
@@ -2340,7 +2353,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Add some more sessions, but still in quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -2351,7 +2365,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test when out of quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -2360,16 +2375,16 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, effectiveStandbyBucket);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     @Test
@@ -2389,7 +2404,8 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions out of window.
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -2398,7 +2414,8 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions in window but still in quota.
         final long start = now - (6 * HOUR_IN_MILLIS);
@@ -2412,7 +2429,8 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Add some more sessions, but still in quota.
         mQuotaController.saveTimingSession(0, "com.android.test",
@@ -2422,7 +2440,8 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test when out of quota.
         mQuotaController.saveTimingSession(0, "com.android.test",
@@ -2430,15 +2449,15 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", standbyBucket);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     /** Tests that the start alarm is properly rescheduled if the app's bucket is changed. */
@@ -2471,9 +2490,10 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX);
         }
-        inOrder.verify(mAlarmManager, never())
-                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
-        inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class));
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .cancel(any(AlarmManager.OnAlarmListener.class));
 
         // And down from there.
         final long expectedWorkingAlarmTime =
@@ -2482,8 +2502,9 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", WORKING_INDEX);
         }
-        inOrder.verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
+                eq(TAG_QUOTA_CHECK), any(), any());
 
         final long expectedFrequentAlarmTime =
                 outOfQuotaTime + (8 * HOUR_IN_MILLIS)
@@ -2491,8 +2512,9 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", FREQUENT_INDEX);
         }
-        inOrder.verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
+                eq(TAG_QUOTA_CHECK), any(), any());
 
         final long expectedRareAlarmTime =
                 outOfQuotaTime + (24 * HOUR_IN_MILLIS)
@@ -2500,28 +2522,31 @@
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", RARE_INDEX);
         }
-        inOrder.verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedRareAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // And back up again.
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", FREQUENT_INDEX);
         }
-        inOrder.verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
+                eq(TAG_QUOTA_CHECK), any(), any());
 
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", WORKING_INDEX);
         }
-        inOrder.verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
+                eq(TAG_QUOTA_CHECK), any(), any());
 
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(0, "com.android.test", ACTIVE_INDEX);
         }
-        inOrder.verify(mAlarmManager, never())
-                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
-        inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class));
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1))
+                .cancel(any(AlarmManager.OnAlarmListener.class));
     }
 
     @Test
@@ -2547,7 +2572,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Valid time in the future, so the count should be used.
         stats.jobRateLimitExpirationTimeElapsed = now + 5 * MINUTE_IN_MILLIS / 2;
@@ -2556,8 +2582,9 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
+                eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     /**
@@ -2647,8 +2674,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
 
@@ -2680,8 +2707,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     @Test
@@ -3828,7 +3855,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Ran jobs up to the job limit. All of them should be allowed to run.
         for (int i = 0; i < mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW; ++i) {
@@ -3846,7 +3874,8 @@
             advanceElapsedClock(SECOND_IN_MILLIS);
         }
         // Start alarm shouldn't have been scheduled since the app was in quota up until this point.
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // The app is now out of job count quota
         JobStatus throttledJob = createJobStatus(
@@ -3863,8 +3892,9 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         final long expectedWorkingAlarmTime = stats.jobRateLimitExpirationTimeElapsed;
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
+                eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     /**
@@ -3894,7 +3924,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Ran jobs up to the job limit. All of them should be allowed to run.
         for (int i = 0; i < mQcConstants.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW; ++i) {
@@ -3914,7 +3945,8 @@
             advanceElapsedClock(SECOND_IN_MILLIS);
         }
         // Start alarm shouldn't have been scheduled since the app was in quota up until this point.
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(0)).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // The app is now out of session count quota
         JobStatus throttledJob = createJobStatus(
@@ -3932,8 +3964,9 @@
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
         final long expectedWorkingAlarmTime = stats.sessionRateLimitExpirationTimeElapsed;
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
+                eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     @Test
@@ -4391,8 +4424,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        inOrder.verify(mAlarmManager, never())
-                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions out of window.
         final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
@@ -4402,8 +4435,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        inOrder.verify(mAlarmManager, never())
-                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions in window but still in quota.
         final long end = now - (22 * HOUR_IN_MILLIS - 5 * MINUTE_IN_MILLIS);
@@ -4414,8 +4447,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        inOrder.verify(mAlarmManager, never())
-                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Add some more sessions, but still in quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -4426,8 +4459,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        inOrder.verify(mAlarmManager, never())
-                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test when out of quota.
         mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
@@ -4436,16 +4469,16 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        inOrder.verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, standbyBucket);
         }
-        inOrder.verify(mAlarmManager, never())
-                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     /** Tests that the start alarm is properly rescheduled if the app's bucket is changed. */
@@ -4483,9 +4516,10 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX);
         }
-        inOrder.verify(mAlarmManager, never())
-                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
-        inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class));
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .cancel(any(AlarmManager.OnAlarmListener.class));
 
         // And down from there.
         setStandbyBucket(WORKING_INDEX);
@@ -4496,8 +4530,9 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX);
         }
-        inOrder.verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
+                eq(TAG_QUOTA_CHECK), any(), any());
 
         setStandbyBucket(FREQUENT_INDEX);
         final long expectedFrequentAlarmTime =
@@ -4506,8 +4541,9 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, FREQUENT_INDEX);
         }
-        inOrder.verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
+                eq(TAG_QUOTA_CHECK), any(), any());
 
         setStandbyBucket(RARE_INDEX);
         final long expectedRareAlarmTime =
@@ -4517,8 +4553,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, RARE_INDEX);
         }
-        inOrder.verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedRareAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedRareAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // And back up again.
         setStandbyBucket(FREQUENT_INDEX);
@@ -4526,25 +4562,28 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, FREQUENT_INDEX);
         }
-        inOrder.verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
+                eq(TAG_QUOTA_CHECK), any(), any());
 
         setStandbyBucket(WORKING_INDEX);
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX);
         }
-        inOrder.verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
+                eq(TAG_QUOTA_CHECK), any(), any());
 
         setStandbyBucket(ACTIVE_INDEX);
         synchronized (mQuotaController.mLock) {
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, ACTIVE_INDEX);
         }
-        inOrder.verify(mAlarmManager, never())
-                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
-        inOrder.verify(mAlarmManager, times(1)).cancel(any(AlarmManager.OnAlarmListener.class));
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        inOrder.verify(mAlarmManager, timeout(1000).times(1))
+                .cancel(any(AlarmManager.OnAlarmListener.class));
     }
 
     /**
@@ -4578,8 +4617,8 @@
             mQuotaController.maybeScheduleStartAlarmLocked(
                     SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX);
         }
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     /** Tests that TimingSessions aren't saved when the device is charging. */
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/StationaryThrottlingLocationProviderTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/StationaryThrottlingLocationProviderTest.java
index 63996f0..4d6f49e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/provider/StationaryThrottlingLocationProviderTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/StationaryThrottlingLocationProviderTest.java
@@ -90,7 +90,7 @@
     }
 
     @Test
-    public void testThrottle() {
+    public void testThrottle_stationaryExit() {
         ProviderRequest request = new ProviderRequest.Builder().setIntervalMillis(50).build();
 
         mProvider.getController().setRequest(request);
@@ -113,6 +113,29 @@
     }
 
     @Test
+    public void testThrottle_idleExit() {
+        ProviderRequest request = new ProviderRequest.Builder().setIntervalMillis(50).build();
+
+        mProvider.getController().setRequest(request);
+        verify(mDelegate).onSetRequest(request);
+
+        mDelegateProvider.reportLocation(createLocationResult("test_provider", mRandom));
+        verify(mListener, times(1)).onReportLocation(any(LocationResult.class));
+
+        mInjector.getDeviceIdleHelper().setIdle(true);
+        verify(mDelegate, never()).onSetRequest(ProviderRequest.EMPTY_REQUEST);
+
+        mInjector.getDeviceStationaryHelper().setStationary(true);
+        verify(mDelegate).onSetRequest(ProviderRequest.EMPTY_REQUEST);
+        verify(mListener, timeout(75).times(2)).onReportLocation(any(LocationResult.class));
+        verify(mListener, timeout(75).times(3)).onReportLocation(any(LocationResult.class));
+
+        mInjector.getDeviceIdleHelper().setIdle(false);
+        verify(mDelegate, times(2)).onSetRequest(request);
+        verify(mListener, after(75).times(3)).onReportLocation(any(LocationResult.class));
+    }
+
+    @Test
     public void testThrottle_NoInitialLocation() {
         ProviderRequest request = new ProviderRequest.Builder().setIntervalMillis(50).build();
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/SensorPrivacyServiceMockingTest.java b/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/SensorPrivacyServiceMockingTest.java
index ba79a76..38f01b5 100644
--- a/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/SensorPrivacyServiceMockingTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/sensorprivacy/SensorPrivacyServiceMockingTest.java
@@ -16,12 +16,18 @@
 
 package com.android.server.sensorprivacy;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.AppOpsManager;
+import android.app.AppOpsManagerInternal;
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.os.Environment;
@@ -33,8 +39,10 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.server.LocalServices;
 import com.android.server.SensorPrivacyService;
+import com.android.server.SystemService;
 import com.android.server.pm.UserManagerInternal;
 
+import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -44,6 +52,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.util.concurrent.CompletableFuture;
 
 @RunWith(AndroidTestingRunner.class)
 public class SensorPrivacyServiceMockingTest {
@@ -63,10 +72,21 @@
     public static final String PERSISTENCE_FILE6 =
             String.format(PERSISTENCE_FILE_PATHS_TEMPLATE, 6);
 
+    public static final String PERSISTENCE_FILE_MIC_MUTE_CAM_MUTE =
+            "SensorPrivacyServiceMockingTest/persisted_file_micMute_camMute.xml";
+    public static final String PERSISTENCE_FILE_MIC_MUTE_CAM_UNMUTE =
+            "SensorPrivacyServiceMockingTest/persisted_file_micMute_camUnmute.xml";
+    public static final String PERSISTENCE_FILE_MIC_UNMUTE_CAM_MUTE =
+            "SensorPrivacyServiceMockingTest/persisted_file_micUnmute_camMute.xml";
+    public static final String PERSISTENCE_FILE_MIC_UNMUTE_CAM_UNMUTE =
+            "SensorPrivacyServiceMockingTest/persisted_file_micUnmute_camUnmute.xml";
+
     private Context mContext;
     @Mock
     private AppOpsManager mMockedAppOpsManager;
     @Mock
+    private AppOpsManagerInternal mMockedAppOpsManagerInternal;
+    @Mock
     private UserManagerInternal mMockedUserManagerInternal;
     @Mock
     private ActivityManager mMockedActivityManager;
@@ -134,13 +154,103 @@
         }
     }
 
+    @Test
+    public void testServiceInit_AppOpsRestricted_micMute_camMute() throws IOException {
+        testServiceInit_AppOpsRestricted(PERSISTENCE_FILE_MIC_MUTE_CAM_MUTE, true, true);
+    }
+
+    @Test
+    public void testServiceInit_AppOpsRestricted_micMute_camUnmute() throws IOException {
+        testServiceInit_AppOpsRestricted(PERSISTENCE_FILE_MIC_MUTE_CAM_UNMUTE, true, false);
+    }
+
+    @Test
+    public void testServiceInit_AppOpsRestricted_micUnmute_camMute() throws IOException {
+        testServiceInit_AppOpsRestricted(PERSISTENCE_FILE_MIC_UNMUTE_CAM_MUTE, false, true);
+    }
+
+    @Test
+    public void testServiceInit_AppOpsRestricted_micUnmute_camUnmute() throws IOException {
+        testServiceInit_AppOpsRestricted(PERSISTENCE_FILE_MIC_UNMUTE_CAM_UNMUTE, false, false);
+    }
+
+    private void testServiceInit_AppOpsRestricted(String persistenceFileMicMuteCamMute,
+            boolean expectedMicState, boolean expectedCamState)
+            throws IOException {
+        MockitoSession mockitoSession = ExtendedMockito.mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.WARN)
+                .spyStatic(LocalServices.class)
+                .spyStatic(Environment.class)
+                .startMocking();
+
+        try {
+            mContext = InstrumentationRegistry.getInstrumentation().getContext();
+            spyOn(mContext);
+
+            doReturn(mMockedAppOpsManager).when(mContext).getSystemService(AppOpsManager.class);
+            doReturn(mMockedAppOpsManagerInternal)
+                    .when(() -> LocalServices.getService(AppOpsManagerInternal.class));
+            doReturn(mMockedUserManagerInternal)
+                    .when(() -> LocalServices.getService(UserManagerInternal.class));
+            doReturn(mMockedActivityManager).when(mContext).getSystemService(ActivityManager.class);
+            doReturn(mMockedActivityTaskManager)
+                    .when(mContext).getSystemService(ActivityTaskManager.class);
+            doReturn(mMockedTelephonyManager).when(mContext).getSystemService(
+                    TelephonyManager.class);
+
+            String dataDir = mContext.getApplicationInfo().dataDir;
+            doReturn(new File(dataDir)).when(() -> Environment.getDataSystemDirectory());
+
+            File onDeviceFile = new File(dataDir, "sensor_privacy.xml");
+            onDeviceFile.delete();
+
+            doReturn(new int[]{0}).when(mMockedUserManagerInternal).getUserIds();
+            doReturn(ExtendedMockito.mock(UserInfo.class)).when(mMockedUserManagerInternal)
+                    .getUserInfo(0);
+
+            CompletableFuture<Boolean> micState = new CompletableFuture<>();
+            CompletableFuture<Boolean> camState = new CompletableFuture<>();
+            doAnswer(invocation -> {
+                int code = invocation.getArgument(0);
+                boolean restricted = invocation.getArgument(1);
+                if (code == AppOpsManager.OP_RECORD_AUDIO) {
+                    micState.complete(restricted);
+                } else if (code == AppOpsManager.OP_CAMERA) {
+                    camState.complete(restricted);
+                }
+                return null;
+            }).when(mMockedAppOpsManagerInternal).setGlobalRestriction(anyInt(), anyBoolean(),
+                    any());
+
+            initServiceWithPersistenceFile(onDeviceFile, persistenceFileMicMuteCamMute, 0);
+
+            Assert.assertTrue(micState.join() == expectedMicState);
+            Assert.assertTrue(camState.join() == expectedCamState);
+
+        } finally {
+            mockitoSession.finishMocking();
+        }
+    }
+
     private void initServiceWithPersistenceFile(File onDeviceFile,
             String persistenceFilePath) throws IOException {
+        initServiceWithPersistenceFile(onDeviceFile, persistenceFilePath, -1);
+    }
+
+    private void initServiceWithPersistenceFile(File onDeviceFile,
+            String persistenceFilePath, int startingUserId) throws IOException {
         if (persistenceFilePath != null) {
             Files.copy(mContext.getAssets().open(persistenceFilePath),
                     onDeviceFile.toPath());
         }
-        new SensorPrivacyService(mContext);
+        SensorPrivacyService service = new SensorPrivacyService(mContext);
+        if (startingUserId != -1) {
+            SystemService.TargetUser mockedTargetUser =
+                    ExtendedMockito.mock(SystemService.TargetUser.class);
+            doReturn(startingUserId).when(mockedTargetUser).getUserIdentifier();
+            service.onUserStarting(mockedTargetUser);
+        }
         onDeviceFile.delete();
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
index 9e48045..6751b80 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
@@ -57,6 +57,11 @@
         MockScribe(InternalResourceService irs) {
             super(irs);
         }
+
+        @Override
+        void postWrite() {
+            // Do nothing
+        }
     }
 
     @Before
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
new file mode 100644
index 0000000..b72fc23
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.tare;
+
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.SparseArrayMap;
+
+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 org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for various Scribe behavior, including reading and writing correctly from file.
+ *
+ * atest FrameworksServicesTests:ScribeTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ScribeTest {
+    private static final String TAG = "ScribeTest";
+
+    private static final int TEST_USER_ID = 27;
+    private static final String TEST_PACKAGE = "com.android.test";
+
+    private MockitoSession mMockingSession;
+    private Scribe mScribeUnderTest;
+    private File mTestFileDir;
+    private final List<PackageInfo> mInstalledPackages = new ArrayList<>();
+
+    @Mock
+    private InternalResourceService mIrs;
+
+    private Context getContext() {
+        return InstrumentationRegistry.getContext();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mMockingSession = mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .mockStatic(LocalServices.class)
+                .startMocking();
+        when(mIrs.getLock()).thenReturn(new Object());
+        when(mIrs.isEnabled()).thenReturn(true);
+        when(mIrs.getInstalledPackages()).thenReturn(mInstalledPackages);
+        mTestFileDir = new File(getContext().getFilesDir(), "scribe_test");
+        //noinspection ResultOfMethodCallIgnored
+        mTestFileDir.mkdirs();
+        Log.d(TAG, "Saving data to '" + mTestFileDir + "'");
+        mScribeUnderTest = new Scribe(mIrs, mTestFileDir);
+
+        addInstalledPackage(TEST_USER_ID, TEST_PACKAGE);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mScribeUnderTest.tearDownLocked();
+        if (mTestFileDir.exists() && !mTestFileDir.delete()) {
+            Log.w(TAG, "Failed to delete test file directory");
+        }
+        if (mMockingSession != null) {
+            mMockingSession.finishMocking();
+        }
+    }
+
+    @Test
+    public void testWriteHighLevelStateToDisk() {
+        long lastReclamationTime = System.currentTimeMillis();
+        long narcsInCirculation = 2000L;
+
+        Ledger ledger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
+        ledger.recordTransaction(new Ledger.Transaction(0, 1000L, 1, null, 2000));
+        // Negative ledger balance shouldn't affect the total circulation value.
+        ledger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID + 1, TEST_PACKAGE);
+        ledger.recordTransaction(new Ledger.Transaction(0, 1000L, 1, null, -5000));
+        mScribeUnderTest.setLastReclamationTimeLocked(lastReclamationTime);
+        mScribeUnderTest.writeImmediatelyForTesting();
+
+        mScribeUnderTest.loadFromDiskLocked();
+
+        assertEquals(lastReclamationTime, mScribeUnderTest.getLastReclamationTimeLocked());
+        assertEquals(narcsInCirculation, mScribeUnderTest.getNarcsInCirculationLocked());
+    }
+
+    @Test
+    public void testWritingEmptyLedgerToDisk() {
+        final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
+        mScribeUnderTest.writeImmediatelyForTesting();
+
+        mScribeUnderTest.loadFromDiskLocked();
+        assertLedgersEqual(ogLedger, mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
+    }
+
+    @Test
+    public void testWritingPopulatedLedgerToDisk() {
+        final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
+        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51));
+        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52));
+        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3));
+        mScribeUnderTest.writeImmediatelyForTesting();
+
+        mScribeUnderTest.loadFromDiskLocked();
+        assertLedgersEqual(ogLedger, mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
+    }
+
+    @Test
+    public void testWritingMultipleLedgersToDisk() {
+        final SparseArrayMap<String, Ledger> ledgers = new SparseArrayMap<>();
+        final int numUsers = 3;
+        final int numLedgers = 5;
+        for (int u = 0; u < numUsers; ++u) {
+            final int userId = TEST_USER_ID + u;
+            for (int l = 0; l < numLedgers; ++l) {
+                final String pkgName = TEST_PACKAGE + l;
+                addInstalledPackage(userId, pkgName);
+                final Ledger ledger = mScribeUnderTest.getLedgerLocked(userId, pkgName);
+                ledger.recordTransaction(new Ledger.Transaction(
+                        0, 1000L * u + l, 1, null, 51L * u + l));
+                ledger.recordTransaction(new Ledger.Transaction(
+                        1500L * u + l, 2000L * u + l, 2 * u + l, "green" + u + l, 52L * u + l));
+                ledger.recordTransaction(new Ledger.Transaction(
+                        2500L * u + l, 3000L * u + l, 3 * u + l, "blue" + u + l, 3L * u + l));
+                ledgers.add(userId, pkgName, ledger);
+            }
+        }
+        mScribeUnderTest.writeImmediatelyForTesting();
+
+        mScribeUnderTest.loadFromDiskLocked();
+        ledgers.forEach((userId, pkgName, ledger)
+                -> assertLedgersEqual(ledger, mScribeUnderTest.getLedgerLocked(userId, pkgName)));
+    }
+
+    @Test
+    public void testDiscardLedgerFromDisk() {
+        final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
+        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51));
+        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52));
+        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3));
+        mScribeUnderTest.writeImmediatelyForTesting();
+
+        mScribeUnderTest.loadFromDiskLocked();
+        assertLedgersEqual(ogLedger, mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
+
+        mScribeUnderTest.discardLedgerLocked(TEST_USER_ID, TEST_PACKAGE);
+        mScribeUnderTest.writeImmediatelyForTesting();
+
+        // Make sure there's no more saved ledger.
+        mScribeUnderTest.loadFromDiskLocked();
+        assertLedgersEqual(new Ledger(),
+                mScribeUnderTest.getLedgerLocked(TEST_USER_ID, TEST_PACKAGE));
+    }
+
+    @Test
+    public void testLoadingMissingPackageFromDisk() {
+        final String pkgName = TEST_PACKAGE + ".uninstalled";
+        final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(TEST_USER_ID, pkgName);
+        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51));
+        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52));
+        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3));
+        mScribeUnderTest.writeImmediatelyForTesting();
+
+        // Package isn't installed, so make sure it's not saved to memory after loading.
+        mScribeUnderTest.loadFromDiskLocked();
+        assertLedgersEqual(new Ledger(), mScribeUnderTest.getLedgerLocked(TEST_USER_ID, pkgName));
+    }
+
+    @Test
+    public void testLoadingMissingUserFromDisk() {
+        final int userId = TEST_USER_ID + 1;
+        final Ledger ogLedger = mScribeUnderTest.getLedgerLocked(userId, TEST_PACKAGE);
+        ogLedger.recordTransaction(new Ledger.Transaction(0, 1000, 1, null, 51));
+        ogLedger.recordTransaction(new Ledger.Transaction(1500, 2000, 2, "green", 52));
+        ogLedger.recordTransaction(new Ledger.Transaction(2500, 3000, 3, "blue", 3));
+        mScribeUnderTest.writeImmediatelyForTesting();
+
+        // User doesn't show up with any packages, so make sure nothing is saved after loading.
+        mScribeUnderTest.loadFromDiskLocked();
+        assertLedgersEqual(new Ledger(), mScribeUnderTest.getLedgerLocked(userId, TEST_PACKAGE));
+    }
+
+    private void assertLedgersEqual(Ledger expected, Ledger actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        assertEquals(expected.getCurrentBalance(), actual.getCurrentBalance());
+        List<Ledger.Transaction> expectedTransactions = expected.getTransactions();
+        List<Ledger.Transaction> actualTransactions = actual.getTransactions();
+        assertEquals(expectedTransactions.size(), actualTransactions.size());
+        for (int i = 0; i < expectedTransactions.size(); ++i) {
+            assertTransactionsEqual(expectedTransactions.get(i), actualTransactions.get(i));
+        }
+    }
+
+    private void assertTransactionsEqual(Ledger.Transaction expected, Ledger.Transaction actual) {
+        if (expected == null) {
+            assertNull(actual);
+            return;
+        }
+        assertNotNull(actual);
+        assertEquals(expected.startTimeMs, actual.startTimeMs);
+        assertEquals(expected.endTimeMs, actual.endTimeMs);
+        assertEquals(expected.eventId, actual.eventId);
+        assertEquals(expected.tag, actual.tag);
+        assertEquals(expected.delta, actual.delta);
+    }
+
+    private void addInstalledPackage(int userId, String pkgName) {
+        PackageInfo pkgInfo = new PackageInfo();
+        pkgInfo.packageName = pkgName;
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.uid = UserHandle.getUid(userId, Math.abs(pkgName.hashCode()));
+        pkgInfo.applicationInfo = applicationInfo;
+        mInstalledPackages.add(pkgInfo);
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java
new file mode 100644
index 0000000..849e673
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/utils/AlarmQueueTest.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.utils;
+
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlarmManager;
+import android.content.Context;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.ArraySet;
+
+import androidx.annotation.NonNull;
+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 org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+/**
+ * Tests for {@link AlarmQueue}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class AlarmQueueTest {
+    private static final String ALARM_TAG = "*test*";
+
+    private final InjectorForTest mInjector = new InjectorForTest();
+    private ArraySet<String> mExpiredPackages;
+    private MockitoSession mMockingSession;
+    @Mock
+    private AlarmManager mAlarmManager;
+    @Mock
+    private Context mContext;
+
+    private static class InjectorForTest extends AlarmQueue.Injector {
+        private long mElapsedTime = SystemClock.elapsedRealtime();
+
+        @Override
+        long getElapsedRealtime() {
+            return mElapsedTime;
+        }
+    }
+
+    @Before
+    public void setUp() {
+        mMockingSession = mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .mockStatic(LocalServices.class)
+                .startMocking();
+
+        when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper());
+        when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager);
+
+        // Freeze the clocks at 24 hours after this moment in time.
+        advanceElapsedClock(24 * HOUR_IN_MILLIS);
+    }
+
+    @After
+    public void tearDown() {
+        if (mMockingSession != null) {
+            mMockingSession.finishMocking();
+        }
+    }
+
+    private void advanceElapsedClock(long incrementMs) {
+        mInjector.mElapsedTime += incrementMs;
+    }
+
+    @NonNull
+    private AlarmQueue<String> createAlarmQueue(boolean exactAlarm, long minTimeBetweenAlarmsMs) {
+        return new AlarmQueue<String>(mContext, mContext.getMainLooper(), ALARM_TAG, "Test",
+                exactAlarm, minTimeBetweenAlarmsMs, mInjector) {
+            @Override
+            protected boolean isForUser(String key, int userId) {
+                return true;
+            }
+
+            @Override
+            protected void processExpiredAlarms(@NonNull ArraySet<String> expired) {
+                mExpiredPackages = expired;
+            }
+        };
+    }
+
+    @Test
+    public void testAddingIncreasingAlarms() {
+        final AlarmQueue<String> alarmQueue = createAlarmQueue(true, 0);
+        final long nowElapsed = mInjector.getElapsedRealtime();
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        alarmQueue.addAlarm("com.android.test.1", nowElapsed + HOUR_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1))
+                .setExact(anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any());
+        alarmQueue.addAlarm("com.android.test.2", nowElapsed + 2 * HOUR_IN_MILLIS);
+        inOrder.verify(mAlarmManager, never())
+                .setExact(anyInt(), anyLong(), eq(ALARM_TAG), any(), any());
+        alarmQueue.addAlarm("com.android.test.3", nowElapsed + 3 * HOUR_IN_MILLIS);
+        inOrder.verify(mAlarmManager, never())
+                .setExact(anyInt(), anyLong(), eq(ALARM_TAG), any(), any());
+    }
+
+    @Test
+    public void testAddingDecreasingAlarms() {
+        final AlarmQueue<String> alarmQueue = createAlarmQueue(true, 0);
+        final long nowElapsed = mInjector.getElapsedRealtime();
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        alarmQueue.addAlarm("com.android.test.3", nowElapsed + 3 * HOUR_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + 3 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any());
+        alarmQueue.addAlarm("com.android.test.2", nowElapsed + 2 * HOUR_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + 2 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any());
+        alarmQueue.addAlarm("com.android.test.1", nowElapsed + HOUR_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any());
+    }
+
+    /**
+     * Verify that updating the alarm time for a key will result in the AlarmManager alarm changing,
+     * if needed.
+     */
+    @Test
+    public void testChangingKeyAlarm() {
+        final AlarmQueue<String> alarmQueue = createAlarmQueue(true, 0);
+        final long nowElapsed = mInjector.getElapsedRealtime();
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        alarmQueue.addAlarm("1", nowElapsed + 5 * MINUTE_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + 5 * MINUTE_IN_MILLIS), eq(ALARM_TAG), any(), any());
+
+        // Only alarm, but the time has changed, so we should reschedule what's set with AM.
+        alarmQueue.addAlarm("1", nowElapsed + 20 * MINUTE_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + 20 * MINUTE_IN_MILLIS), eq(ALARM_TAG), any(), any());
+
+        alarmQueue.addAlarm("1", nowElapsed + 10 * MINUTE_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + 10 * MINUTE_IN_MILLIS), eq(ALARM_TAG), any(), any());
+
+        // Add another keyed alarm and check that we don't bother rescheduling when the changed
+        // alarm is after the first alarm to go off.
+        alarmQueue.addAlarm("2", nowElapsed + 11 * MINUTE_IN_MILLIS);
+        inOrder.verify(mAlarmManager, never()).setExact(
+                anyInt(), anyLong(), eq(ALARM_TAG), any(), any());
+
+        alarmQueue.addAlarm("2", nowElapsed + 51 * MINUTE_IN_MILLIS);
+        inOrder.verify(mAlarmManager, never()).setExact(
+                anyInt(), anyLong(), eq(ALARM_TAG), any(), any());
+
+        alarmQueue.addAlarm("1", nowElapsed + 52 * MINUTE_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + 51 * MINUTE_IN_MILLIS), eq(ALARM_TAG), any(), any());
+    }
+
+    @Test
+    public void testInexactQueue() {
+        final AlarmQueue<String> alarmQueue = createAlarmQueue(false, 0);
+        final long nowElapsed = mInjector.getElapsedRealtime();
+
+        alarmQueue.addAlarm("com.android.test.1", nowElapsed + HOUR_IN_MILLIS);
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), anyLong(), eq(ALARM_TAG), any(), any());
+    }
+
+    @Test
+    public void testMinTimeBetweenAlarms() {
+        final AlarmQueue<String> alarmQueue = createAlarmQueue(true, 2 * HOUR_IN_MILLIS);
+        final long nowElapsed = mInjector.getElapsedRealtime();
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        final String pkg1 = "com.android.test.1";
+        final String pkg2 = "com.android.test.2";
+        final String pkg3 = "com.android.test.3";
+        alarmQueue.addAlarm(pkg1, nowElapsed + HOUR_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any());
+        alarmQueue.addAlarm(pkg2, nowElapsed + 2 * HOUR_IN_MILLIS);
+        alarmQueue.addAlarm(pkg3, nowElapsed + 3 * HOUR_IN_MILLIS);
+        alarmQueue.addAlarm("com.android.test.4", nowElapsed + 4 * HOUR_IN_MILLIS);
+
+        advanceElapsedClock(HOUR_IN_MILLIS);
+
+        alarmQueue.onAlarm();
+        // Minimum of 2 hours between alarms, so the next alarm should be 2 hours after the first.
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + 3 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any());
+
+        advanceElapsedClock(2 * HOUR_IN_MILLIS);
+        alarmQueue.onAlarm();
+        // Minimum of 2 hours between alarms, so the next alarm should be 2 hours after the second.
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + 5 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any());
+    }
+
+    @Test
+    public void testOnAlarm() {
+        final AlarmQueue<String> alarmQueue = createAlarmQueue(true, 0);
+        final long nowElapsed = mInjector.getElapsedRealtime();
+
+        InOrder inOrder = inOrder(mAlarmManager);
+
+        final String pkg1 = "com.android.test.1";
+        final String pkg2 = "com.android.test.2";
+        final String pkg3 = "com.android.test.3";
+        final String pkg4 = "com.android.test.4";
+        alarmQueue.addAlarm(pkg1, nowElapsed + HOUR_IN_MILLIS);
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any());
+        alarmQueue.addAlarm(pkg2, nowElapsed + 2 * HOUR_IN_MILLIS);
+        alarmQueue.addAlarm(pkg3, nowElapsed + 3 * HOUR_IN_MILLIS);
+        alarmQueue.addAlarm(pkg4, nowElapsed + 4 * HOUR_IN_MILLIS);
+
+        advanceElapsedClock(HOUR_IN_MILLIS);
+
+        final ArraySet<String> expectedExpired = new ArraySet<>();
+
+        expectedExpired.add(pkg1);
+        alarmQueue.onAlarm();
+        assertEquals(expectedExpired, mExpiredPackages);
+        // The next alarm should also be scheduled.
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + 2 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any());
+
+        advanceElapsedClock(2 * HOUR_IN_MILLIS);
+
+        expectedExpired.clear();
+        expectedExpired.add(pkg2);
+        expectedExpired.add(pkg3);
+        alarmQueue.onAlarm();
+        assertEquals(expectedExpired, mExpiredPackages);
+        // The next alarm should also be scheduled.
+        inOrder.verify(mAlarmManager, timeout(1000).times(1)).setExact(
+                anyInt(), eq(nowElapsed + 4 * HOUR_IN_MILLIS), eq(ALARM_TAG), any(), any());
+
+        advanceElapsedClock(HOUR_IN_MILLIS);
+
+        expectedExpired.clear();
+        expectedExpired.add(pkg4);
+        alarmQueue.onAlarm();
+        assertEquals(expectedExpired, mExpiredPackages);
+        // No more alarms, so nothing should be scheduled with AlarmManager.
+        inOrder.verify(mAlarmManager, timeout(1000).times(0))
+                .setExact(anyInt(), anyLong(), eq(ALARM_TAG), any(), any());
+    }
+
+    @Test
+    public void testSettingMinTimeBetweenAlarms() {
+        final AlarmQueue<String> alarmQueue = createAlarmQueue(true, 50);
+        assertEquals(50, alarmQueue.getMinTimeBetweenAlarmsMs());
+
+        alarmQueue.setMinTimeBetweenAlarmsMs(2345);
+        assertEquals(2345, alarmQueue.getMinTimeBetweenAlarmsMs());
+
+        try {
+            alarmQueue.setMinTimeBetweenAlarmsMs(-1);
+            fail("Successfully set negative time between alarms");
+        } catch (IllegalArgumentException expected) {
+            // Success
+        }
+        try {
+            createAlarmQueue(false, -1);
+            fail("Successfully set negative time between alarms");
+        } catch (IllegalArgumentException expected) {
+            // Success
+        }
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java
index 80aec73..608b64e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java
@@ -588,37 +588,41 @@
 
         // No sessions saved yet.
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, never()).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions out of window.
         final long now = mInjector.getElapsedRealtime();
         logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 10 * HOUR_IN_MILLIS, 20);
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, never()).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test with timing sessions in window but still in quota.
         final long start = now - (6 * HOUR_IN_MILLIS);
         final long expectedAlarmTime = start + 8 * HOUR_IN_MILLIS;
         logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, start, 5);
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, never()).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Add some more sessions, but still in quota.
         logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 3 * HOUR_IN_MILLIS, 1);
         logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 3);
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
-        verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, never()).setWindow(
+                anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Test when out of quota.
         logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 1);
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
-        verify(mAlarmManager, timeout(1000).times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, timeout(1000).times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
 
         // Alarm already scheduled, so make sure it's not scheduled again.
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
-        verify(mAlarmManager, times(1))
-                .set(anyInt(), eq(expectedAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+        verify(mAlarmManager, times(1)).setWindow(
+                anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     /** Tests that the start alarm is properly rescheduled if the app's category is changed. */
@@ -652,7 +656,7 @@
         mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, never())
-                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
         inOrder.verify(mAlarmManager, never()).cancel(any(AlarmManager.OnAlarmListener.class));
 
         // And down from there.
@@ -660,37 +664,42 @@
         mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
-                .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
+                        eq(TAG_QUOTA_CHECK), any(), any());
 
         final long expectedFrequentAlarmTime = outOfQuotaTime + (8 * HOUR_IN_MILLIS);
         mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
-                .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
+                        eq(TAG_QUOTA_CHECK), any(), any());
 
         final long expectedRareAlarmTime = outOfQuotaTime + (24 * HOUR_IN_MILLIS);
         mCategorizer.mCategoryToUse = RARE_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
-                .set(anyInt(), eq(expectedRareAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), eq(expectedRareAlarmTime), anyLong(),
+                        eq(TAG_QUOTA_CHECK), any(), any());
 
         // And back up again.
         mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
-                .set(anyInt(), eq(expectedFrequentAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
+                        eq(TAG_QUOTA_CHECK), any(), any());
 
         mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
-                .set(anyInt(), eq(expectedWorkingAlarmTime), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
+                        eq(TAG_QUOTA_CHECK), any(), any());
 
         mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY;
         mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
         inOrder.verify(mAlarmManager, timeout(1000).times(1))
                 .cancel(any(AlarmManager.OnAlarmListener.class));
         inOrder.verify(mAlarmManager, timeout(1000).times(0))
-                .set(anyInt(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
+                .setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any());
     }
 
     @Test
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 8848098..ba580ec 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -56,7 +56,6 @@
         "framework-protos",
         "hamcrest-library",
         "servicestests-utils",
-        "service-appsearch",
         "service-jobscheduler",
         "service-permission.impl",
         // TODO: remove once Android migrates to JUnit 4.12,
@@ -91,7 +90,6 @@
         "libbinder",
         "libc++",
         "libcutils",
-        "libicing",
         "liblog",
         "liblzma",
         "libnativehelper",
diff --git a/services/tests/servicestests/src/com/android/server/am/BatteryExternalStatsWorkerTest.java b/services/tests/servicestests/src/com/android/server/am/BatteryExternalStatsWorkerTest.java
index 4a67ec7..6faa7e7 100644
--- a/services/tests/servicestests/src/com/android/server/am/BatteryExternalStatsWorkerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/BatteryExternalStatsWorkerTest.java
@@ -74,15 +74,20 @@
     @Test
     public void testTargetedEnergyConsumerQuerying() {
         final int numCpuClusters = 4;
+        final int numDisplays = 5;
         final int numOther = 3;
 
         // Add some energy consumers used by BatteryExternalStatsWorker.
         final IntArray tempAllIds = new IntArray();
 
-        final int displayId = mPowerStatsInternal.addEnergyConsumer(EnergyConsumerType.DISPLAY, 0,
-                "display");
-        tempAllIds.add(displayId);
-        mPowerStatsInternal.incrementEnergyConsumption(displayId, 12345);
+        final int[] displayIds = new int[numDisplays];
+        for (int i = 0; i < numDisplays; i++) {
+            displayIds[i] = mPowerStatsInternal.addEnergyConsumer(
+                    EnergyConsumerType.DISPLAY, i, "display" + i);
+            tempAllIds.add(displayIds[i]);
+            mPowerStatsInternal.incrementEnergyConsumption(displayIds[i], 12345 + i);
+        }
+        Arrays.sort(displayIds);
 
         final int wifiId = mPowerStatsInternal.addEnergyConsumer(EnergyConsumerType.WIFI, 0,
                 "wifi");
@@ -130,9 +135,13 @@
 
         final EnergyConsumerResult[] displayResults =
                 mBatteryExternalStatsWorker.getMeasuredEnergyLocked(UPDATE_DISPLAY).getNow(null);
-        // Results should only have the display energy consumer
-        assertEquals(1, displayResults.length);
-        assertEquals(displayId, displayResults[0].id);
+        // Results should only have the cpu cluster energy consumers
+        final int[] receivedDisplayIds = new int[displayResults.length];
+        for (int i = 0; i < displayResults.length; i++) {
+            receivedDisplayIds[i] = displayResults[i].id;
+        }
+        Arrays.sort(receivedDisplayIds);
+        assertArrayEquals(displayIds, receivedDisplayIds);
 
         final EnergyConsumerResult[] wifiResults =
                 mBatteryExternalStatsWorker.getMeasuredEnergyLocked(UPDATE_WIFI).getNow(null);
@@ -193,6 +202,7 @@
     public class TestBatteryStatsImpl extends BatteryStatsImpl {
         public TestBatteryStatsImpl(Context context) {
             mPowerProfile = new PowerProfile(context, true /* forTest */);
+            initTimersAndCounters();
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/am/MeasuredEnergySnapshotTest.java b/services/tests/servicestests/src/com/android/server/am/MeasuredEnergySnapshotTest.java
index 8c87506..a0cbcad 100644
--- a/services/tests/servicestests/src/com/android/server/am/MeasuredEnergySnapshotTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/MeasuredEnergySnapshotTest.java
@@ -16,8 +16,6 @@
 
 package com.android.server.am;
 
-import static com.android.server.am.MeasuredEnergySnapshot.UNAVAILABLE;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -120,7 +118,7 @@
         // results0
         MeasuredEnergyDeltaData delta = snapshot.updateAndGetDelta(RESULTS_0, VOLTAGE_0);
         if (delta != null) { // null is fine here. If non-null, it better be uninteresting though.
-            assertEquals(UNAVAILABLE, delta.displayChargeUC);
+            assertNull(delta.displayChargeUC);
             assertNull(delta.otherTotalChargeUC);
             assertNull(delta.otherUidChargesUC);
         }
@@ -130,7 +128,7 @@
         assertNotNull(delta);
         long expectedChargeUC;
         expectedChargeUC = calculateChargeConsumedUC(14_000, VOLTAGE_0, 24_000, VOLTAGE_1);
-        assertEquals(expectedChargeUC, delta.displayChargeUC);
+        assertEquals(expectedChargeUC, delta.displayChargeUC[0]);
 
         assertNotNull(delta.otherTotalChargeUC);
 
@@ -149,14 +147,14 @@
         delta = snapshot.updateAndGetDelta(RESULTS_2, VOLTAGE_2);
         assertNotNull(delta);
         expectedChargeUC = calculateChargeConsumedUC(24_000, VOLTAGE_1, 36_000, VOLTAGE_2);
-        assertEquals(expectedChargeUC, delta.displayChargeUC);
+        assertEquals(expectedChargeUC, delta.displayChargeUC[0]);
         assertNull(delta.otherUidChargesUC);
         assertNull(delta.otherTotalChargeUC);
 
         // results3
         delta = snapshot.updateAndGetDelta(RESULTS_3, VOLTAGE_3);
         assertNotNull(delta);
-        assertEquals(UNAVAILABLE, delta.displayChargeUC);
+        assertNull(delta.displayChargeUC);
 
         assertNotNull(delta.otherTotalChargeUC);
 
@@ -183,7 +181,7 @@
         delta = snapshot.updateAndGetDelta(RESULTS_4, VOLTAGE_4);
         assertNotNull(delta);
         expectedChargeUC = calculateChargeConsumedUC(36_000, VOLTAGE_2, 43_000, VOLTAGE_4);
-        assertEquals(expectedChargeUC, delta.displayChargeUC);
+        assertEquals(expectedChargeUC, delta.displayChargeUC[0]);
 
         assertNotNull(delta.otherTotalChargeUC);
         expectedChargeUC = calculateChargeConsumedUC(190_000, VOLTAGE_3, 290_000, VOLTAGE_4);
@@ -210,7 +208,7 @@
         // results0
         MeasuredEnergyDeltaData delta = snapshot.updateAndGetDelta(RESULTS_0, VOLTAGE_0);
         if (delta != null) { // null is fine here. If non-null, it better be uninteresting though.
-            assertEquals(UNAVAILABLE, delta.displayChargeUC);
+            assertNull(delta.displayChargeUC);
             assertNull(delta.otherTotalChargeUC);
             assertNull(delta.otherUidChargesUC);
         }
@@ -220,7 +218,7 @@
         assertNotNull(delta);
         final long expectedChargeUC =
                 calculateChargeConsumedUC(14_000, VOLTAGE_0, 24_000, VOLTAGE_1);
-        assertEquals(expectedChargeUC, delta.displayChargeUC);
+        assertEquals(expectedChargeUC, delta.displayChargeUC[0]);
         assertNull(delta.otherTotalChargeUC); // Although in the results, they're not in the idMap
         assertNull(delta.otherUidChargesUC);
     }
diff --git a/services/tests/servicestests/src/com/android/server/criticalevents/OWNERS b/services/tests/servicestests/src/com/android/server/criticalevents/OWNERS
new file mode 100644
index 0000000..07c0e70
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/criticalevents/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/criticalevents/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index 0dd5c61..418831f 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -26,6 +26,7 @@
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE;
 
 import static com.android.server.display.DisplayModeDirector.Vote.INVALID_SIZE;
+import static com.android.server.display.HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -74,6 +75,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.internal.util.test.FakeSettingsProviderRule;
@@ -110,6 +112,7 @@
     private static final boolean DEBUG = false;
     private static final float FLOAT_TOLERANCE = 0.01f;
     private static final int DISPLAY_ID = 0;
+    private static final float TRANSITION_POINT = 0.763f;
 
     private Context mContext;
     private FakesInjector mInjector;
@@ -751,19 +754,27 @@
 
         director.start(sensorManager);
 
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
+        ArgumentCaptor<DisplayListener> displayListenerCaptor =
+                  ArgumentCaptor.forClass(DisplayListener.class);
+        verify(mInjector).registerDisplayListener(displayListenerCaptor.capture(),
+                any(Handler.class),
+                eq(DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
+                    | DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS));
+        DisplayListener displayListener = displayListenerCaptor.getValue();
+
+        ArgumentCaptor<SensorEventListener> sensorListenerCaptor =
                 ArgumentCaptor.forClass(SensorEventListener.class);
         Mockito.verify(sensorManager, Mockito.timeout(TimeUnit.SECONDS.toMillis(1)))
                 .registerListener(
-                        listenerCaptor.capture(),
+                        sensorListenerCaptor.capture(),
                         eq(lightSensor),
                         anyInt(),
                         any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        SensorEventListener sensorListener = sensorListenerCaptor.getValue();
 
-        setBrightness(10);
+        setBrightness(10, 10, displayListener);
         // Sensor reads 20 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 20 /*lux*/));
+        sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 20 /*lux*/));
 
         Vote vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
         assertVoteForRefreshRate(vote, 90 /*fps*/);
@@ -771,9 +782,11 @@
         assertThat(vote).isNotNull();
         assertThat(vote.disableRefreshRateSwitching).isTrue();
 
-        setBrightness(125);
+        // We expect DisplayModeDirector to act on BrightnessInfo.adjustedBrightness; set only this
+        // parameter to the necessary threshold
+        setBrightness(10, 125, displayListener);
         // Sensor reads 1000 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 1000 /*lux*/));
+        sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 1000 /*lux*/));
 
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
         assertThat(vote).isNull();
@@ -799,6 +812,14 @@
 
         director.start(sensorManager);
 
+        ArgumentCaptor<DisplayListener> displayListenerCaptor =
+                  ArgumentCaptor.forClass(DisplayListener.class);
+        verify(mInjector).registerDisplayListener(displayListenerCaptor.capture(),
+                any(Handler.class),
+                eq(DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
+                    | DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS));
+        DisplayListener displayListener = displayListenerCaptor.getValue();
+
         ArgumentCaptor<SensorEventListener> listenerCaptor =
                 ArgumentCaptor.forClass(SensorEventListener.class);
         verify(sensorManager, Mockito.timeout(TimeUnit.SECONDS.toMillis(1)))
@@ -807,20 +828,22 @@
                         eq(lightSensor),
                         anyInt(),
                         any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
+        SensorEventListener sensorListener = listenerCaptor.getValue();
 
-        setBrightness(100);
+        setBrightness(100, 100, displayListener);
         // Sensor reads 2000 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 2000));
+        sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 2000));
 
         Vote vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
         assertThat(vote).isNull();
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNull();
 
-        setBrightness(255);
+        // We expect DisplayModeDirector to act on BrightnessInfo.adjustedBrightness; set only this
+        // parameter to the necessary threshold
+        setBrightness(100, 255, displayListener);
         // Sensor reads 9000 lux,
-        listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 9000));
+        sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 9000));
 
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
         assertVoteForRefreshRate(vote, 60 /*fps*/);
@@ -1435,16 +1458,58 @@
         Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
         assertNull(vote);
 
-        // Turn on HBM
+        // Turn on HBM, with brightness in the HBM range
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
-                new BrightnessInfo(0.45f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR));
+                new BrightnessInfo(TRANSITION_POINT + FLOAT_TOLERANCE, 0.0f, 1.0f,
+                    BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR, TRANSITION_POINT));
+        listener.onDisplayChanged(DISPLAY_ID);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
+        assertVoteForRefreshRate(vote, hbmRefreshRate);
+
+        // Turn on HBM, with brightness below the HBM range
+        when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
+                new BrightnessInfo(TRANSITION_POINT - FLOAT_TOLERANCE, 0.0f, 1.0f,
+                    BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR, TRANSITION_POINT));
+        listener.onDisplayChanged(DISPLAY_ID);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
+        assertNull(vote);
+
+        // Turn off HBM
+        when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
+                new BrightnessInfo(0.45f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF,
+                    TRANSITION_POINT));
+        listener.onDisplayChanged(DISPLAY_ID);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
+        assertNull(vote);
+
+        // Turn on HBM, with brightness in the HBM range
+        when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
+                new BrightnessInfo(TRANSITION_POINT + FLOAT_TOLERANCE, 0.0f, 1.0f,
+                    BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR, TRANSITION_POINT));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
         assertVoteForRefreshRate(vote, hbmRefreshRate);
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
-                new BrightnessInfo(0.45f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF));
+                new BrightnessInfo(0.45f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF,
+                    TRANSITION_POINT));
+        listener.onDisplayChanged(DISPLAY_ID);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
+        assertNull(vote);
+
+        // Turn on HBM, with brightness below the HBM range
+        when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
+                new BrightnessInfo(TRANSITION_POINT - FLOAT_TOLERANCE, 0.0f, 1.0f,
+                    BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR, TRANSITION_POINT));
+        listener.onDisplayChanged(DISPLAY_ID);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
+        assertNull(vote);
+
+        // Turn off HBM
+        when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
+                new BrightnessInfo(0.45f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF,
+                    TRANSITION_POINT));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
         assertNull(vote);
@@ -1514,7 +1579,8 @@
 
         // Turn on HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
-                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT));
+                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT,
+                    TRANSITION_POINT));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
         assertVoteForRefreshRate(vote, initialRefreshRate);
@@ -1531,14 +1597,16 @@
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
-                new BrightnessInfo(0.43f, 0.1f, 0.8f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF));
+                new BrightnessInfo(0.43f, 0.1f, 0.8f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF,
+                    TRANSITION_POINT));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
         assertNull(vote);
 
         // Turn HBM on again and ensure the updated vote value stuck
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
-                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT));
+                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT,
+                    TRANSITION_POINT));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
         assertVoteForRefreshRate(vote, updatedRefreshRate);
@@ -1553,7 +1621,8 @@
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
-                new BrightnessInfo(0.43f, 0.1f, 0.8f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF));
+                new BrightnessInfo(0.43f, 0.1f, 0.8f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF,
+                    TRANSITION_POINT));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
         assertNull(vote);
@@ -1584,14 +1653,82 @@
 
         // Turn on HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
-                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT));
+                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT,
+                    TRANSITION_POINT));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
         assertNull(vote);
 
         // Turn off HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
-                new BrightnessInfo(0.43f, 0.1f, 0.8f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF));
+                new BrightnessInfo(0.43f, 0.1f, 0.8f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF,
+                    TRANSITION_POINT));
+        listener.onDisplayChanged(DISPLAY_ID);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
+        assertNull(vote);
+    }
+
+    @Test
+    public void testHbmVoting_HbmUnsupported() {
+        DisplayModeDirector director =
+                createDirectorFromRefreshRateArray(new float[] {60.0f, 90.0f}, 0);
+        director.start(createMockSensorManager());
+
+        ArgumentCaptor<DisplayListener> captor =
+                  ArgumentCaptor.forClass(DisplayListener.class);
+        verify(mInjector).registerDisplayListener(captor.capture(), any(Handler.class),
+                  eq(DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS
+                  | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED));
+        DisplayListener listener = captor.getValue();
+
+        // Specify Limitation
+        when(mDisplayManagerInternalMock.getRefreshRateLimitations(DISPLAY_ID)).thenReturn(
+                List.of(new RefreshRateLimitation(
+                        DisplayManagerInternal.REFRESH_RATE_LIMIT_HIGH_BRIGHTNESS_MODE,
+                        60.0f, 60.0f)));
+
+        // Verify that there is no HBM vote initially
+        Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
+        assertNull(vote);
+
+        // Turn on HBM when HBM is supported; expect a valid transition point and a vote.
+        when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
+                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT,
+                    TRANSITION_POINT));
+        listener.onDisplayChanged(DISPLAY_ID);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
+        assertVoteForRefreshRate(vote, 60.0f);
+
+        // Turn off HBM
+        when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
+                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF,
+                    TRANSITION_POINT));
+        listener.onDisplayChanged(DISPLAY_ID);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
+        assertNull(vote);
+
+        // Turn on Sunlight HBM when HBM is unsupported; expect an invalid transition point and
+        // no vote.
+        when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
+                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT,
+                    HBM_TRANSITION_POINT_INVALID));
+        listener.onDisplayChanged(DISPLAY_ID);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
+        assertNull(vote);
+
+        // Turn on HDR HBM when HBM is unsupported; expect an invalid transition point and
+        // no vote.
+        when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
+                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR,
+                    HBM_TRANSITION_POINT_INVALID));
+        listener.onDisplayChanged(DISPLAY_ID);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
+        assertNull(vote);
+
+        // Turn off HBM
+        when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
+                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF,
+                    TRANSITION_POINT));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
         assertNull(vote);
@@ -1600,7 +1737,7 @@
     private void setHbmAndAssertRefreshRate(
             DisplayModeDirector director, DisplayListener listener, int mode, float rr) {
         when(mInjector.getBrightnessInfo(DISPLAY_ID))
-                .thenReturn(new BrightnessInfo(1.0f, 0.0f, 1.0f, mode));
+                .thenReturn(new BrightnessInfo(1.0f, 0.0f, 1.0f, mode, TRANSITION_POINT));
         listener.onDisplayChanged(DISPLAY_ID);
 
         final Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
@@ -1679,7 +1816,8 @@
 
         // Turn on HBM
         when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
-                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT));
+                new BrightnessInfo(1.0f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT,
+                    TRANSITION_POINT));
         listener.onDisplayChanged(DISPLAY_ID);
         vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE);
         assertVoteForRefreshRate(vote, 60.f);
@@ -1834,11 +1972,14 @@
         }
     }
 
-    private void setBrightness(int brightness) {
-        Settings.System.putInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS,
-                brightness);
-        mInjector.notifyBrightnessChanged();
-        waitForIdleSync();
+    private void setBrightness(int brightness, int adjustedBrightness, DisplayListener listener) {
+        float floatBri = BrightnessSynchronizer.brightnessIntToFloat(brightness);
+        float floatAdjBri = BrightnessSynchronizer.brightnessIntToFloat(adjustedBrightness);
+
+        when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn(
+                new BrightnessInfo(floatBri, floatAdjBri, 0.0f, 1.0f,
+                    BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF, TRANSITION_POINT));
+        listener.onDisplayChanged(DISPLAY_ID);
     }
 
     private void setPeakRefreshRate(float fps) {
@@ -1902,27 +2043,6 @@
         }
 
         @Override
-        public void registerBrightnessObserver(@NonNull ContentResolver cr,
-                @NonNull ContentObserver observer) {
-            if (mBrightnessObserver != null) {
-                throw new IllegalStateException("Tried to register a second brightness observer");
-            }
-            mBrightnessObserver = observer;
-        }
-
-        @Override
-        public void unregisterBrightnessObserver(@NonNull ContentResolver cr,
-                @NonNull ContentObserver observer) {
-            mBrightnessObserver = null;
-        }
-
-        void notifyBrightnessChanged() {
-            if (mBrightnessObserver != null) {
-                mBrightnessObserver.dispatchChange(false /*selfChange*/, DISPLAY_BRIGHTNESS_URI);
-            }
-        }
-
-        @Override
         public void registerPeakRefreshRateObserver(@NonNull ContentResolver cr,
                 @NonNull ContentObserver observer) {
             mPeakRefreshRateObserver = observer;
diff --git a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java
index cc3591c8..aca8632 100644
--- a/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/HighBrightnessModeControllerTest.java
@@ -20,6 +20,8 @@
 import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF;
 import static android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_SUNLIGHT;
 
+import static com.android.server.display.HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.spy;
@@ -124,6 +126,7 @@
                 mInjectorMock, mHandler, DISPLAY_WIDTH, DISPLAY_HEIGHT, mDisplayToken, DEFAULT_MIN,
                 DEFAULT_MAX, null, () -> {}, mContextSpy);
         assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF);
+        assertEquals(hbmc.getTransitionPoint(), HBM_TRANSITION_POINT_INVALID, 0.0f);
     }
 
     @Test
@@ -135,6 +138,7 @@
         hbmc.setAutoBrightnessEnabled(true);
         hbmc.onAmbientLuxChange(MINIMUM_LUX - 1); // below allowed range
         assertState(hbmc, DEFAULT_MIN, DEFAULT_MAX, HIGH_BRIGHTNESS_MODE_OFF);
+        assertEquals(hbmc.getTransitionPoint(), HBM_TRANSITION_POINT_INVALID, 0.0f);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
index ac2756b..cf4bdf6 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
@@ -28,6 +28,7 @@
 import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -42,6 +43,7 @@
 
 /** Tests for {@link ActiveSourceAction} */
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 public class ActiveSourceActionTest {
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
index 29eb0da..638b386 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
@@ -34,6 +34,7 @@
 import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -50,6 +51,7 @@
 
 /** Tests for {@link DevicePowerStatusAction} */
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 public class DevicePowerStatusActionTest {
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
index 8137d12..41231e0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
@@ -36,6 +36,7 @@
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.os.Looper;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -52,6 +53,7 @@
 import java.util.Collections;
 
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 public class DeviceSelectActionFromPlaybackTest {
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
index 28ee592..dd74864 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
@@ -36,6 +36,7 @@
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.os.Looper;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -51,6 +52,7 @@
 import java.util.Collections;
 
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 public class DeviceSelectActionFromTvTest {
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
index a94690e..c1d9857 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
@@ -339,18 +339,6 @@
     }
 
     @Test
-    public void getStringValue_GlobalSetting_BasicSanity() {
-        when(mStorageAdapter.retrieveGlobalSetting(
-                  Global.HDMI_CONTROL_SEND_STANDBY_ON_SLEEP,
-                  HdmiControlManager.POWER_CONTROL_MODE_TV_AND_AUDIO_SYSTEM))
-            .thenReturn(HdmiControlManager.POWER_CONTROL_MODE_BROADCAST);
-        HdmiCecConfig hdmiCecConfig = new HdmiCecConfig(mContext, mStorageAdapter);
-        assertThat(hdmiCecConfig.getStringValue(
-                    HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE))
-                .isEqualTo(HdmiControlManager.POWER_CONTROL_MODE_BROADCAST);
-    }
-
-    @Test
     public void getStringValue_SharedPref_BasicSanity() {
         when(mStorageAdapter.retrieveSharedPref(
                   HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST,
@@ -429,16 +417,6 @@
     }
 
     @Test
-    public void setStringValue_GlobalSetting_BasicSanity() {
-        HdmiCecConfig hdmiCecConfig = new HdmiCecConfig(mContext, mStorageAdapter);
-        hdmiCecConfig.setStringValue(HdmiControlManager.CEC_SETTING_NAME_POWER_CONTROL_MODE,
-                               HdmiControlManager.POWER_CONTROL_MODE_BROADCAST);
-        verify(mStorageAdapter).storeGlobalSetting(
-                  Global.HDMI_CONTROL_SEND_STANDBY_ON_SLEEP,
-                  HdmiControlManager.POWER_CONTROL_MODE_BROADCAST);
-    }
-
-    @Test
     public void setStringValue_SharedPref_BasicSanity() {
         HdmiCecConfig hdmiCecConfig = new HdmiCecConfig(mContext, mStorageAdapter);
         hdmiCecConfig.setStringValue(
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
index bd6e46d..2dcc449 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
@@ -85,6 +85,7 @@
     private HdmiCecController mHdmiCecController;
     private int mCecVersion = HdmiControlManager.HDMI_CEC_VERSION_1_4_B;
     private int mLogicalAddress = 16;
+    private int mPlaybackLogicalAddress;
     private AllocateAddressCallback mCallback =
             new AllocateAddressCallback() {
                 @Override
@@ -121,7 +122,6 @@
         HdmiCecLocalDevicePlayback playbackDevice =
                 new HdmiCecLocalDevicePlayback(mHdmiControlServiceSpy);
         playbackDevice.init();
-
         ArrayList<HdmiCecLocalDevice> localDevices = new ArrayList<>();
         localDevices.add(playbackDevice);
 
@@ -129,7 +129,11 @@
         mHdmiControlServiceSpy.allocateLogicalAddress(localDevices,
                 HdmiControlService.INITIATED_BY_ENABLE_CEC);
         mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
+        mTestLooper.dispatchAll();
 
+        synchronized (playbackDevice.mLock) {
+            mPlaybackLogicalAddress = playbackDevice.getDeviceInfo().getLogicalAddress();
+        }
         mTestLooper.dispatchAll();
     }
 
@@ -374,7 +378,7 @@
         doReturn(HANDLED).when(mHdmiControlServiceSpy).handleCecCommand(any());
 
         HdmiCecMessage receivedMessage = HdmiCecMessageBuilder.buildStandby(
-                ADDR_TV, ADDR_PLAYBACK_1);
+                ADDR_TV, mPlaybackLogicalAddress);
         mNativeWrapper.onCecMessage(receivedMessage);
 
         mTestLooper.dispatchAll();
@@ -391,13 +395,13 @@
         doReturn(NOT_HANDLED).when(mHdmiControlServiceSpy).handleCecCommand(any());
 
         HdmiCecMessage receivedMessage = HdmiCecMessageBuilder.buildStandby(
-                ADDR_TV, ADDR_PLAYBACK_1);
+                ADDR_TV, mPlaybackLogicalAddress);
         mNativeWrapper.onCecMessage(receivedMessage);
 
         mTestLooper.dispatchAll();
 
         HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
-                DEVICE_PLAYBACK, DEVICE_TV, MESSAGE_STANDBY, ABORT_UNRECOGNIZED_OPCODE);
+                mPlaybackLogicalAddress, DEVICE_TV, MESSAGE_STANDBY, ABORT_UNRECOGNIZED_OPCODE);
         assertThat(mNativeWrapper.getResultMessages()).contains(featureAbort);
     }
 
@@ -408,13 +412,13 @@
         doReturn(ABORT_REFUSED).when(mHdmiControlServiceSpy).handleCecCommand(any());
 
         HdmiCecMessage receivedMessage = HdmiCecMessageBuilder.buildStandby(
-                ADDR_TV, ADDR_PLAYBACK_1);
+                ADDR_TV, mPlaybackLogicalAddress);
         mNativeWrapper.onCecMessage(receivedMessage);
 
         mTestLooper.dispatchAll();
 
         HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
-                DEVICE_PLAYBACK, DEVICE_TV, MESSAGE_STANDBY, ABORT_REFUSED);
+                mPlaybackLogicalAddress, DEVICE_TV, MESSAGE_STANDBY, ABORT_REFUSED);
         assertThat(mNativeWrapper.getResultMessages()).contains(featureAbort);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 372fd60..a260a6d 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -39,6 +39,7 @@
 import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -55,6 +56,7 @@
 import java.util.concurrent.TimeUnit;
 
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 /** Tests for {@link HdmiCecLocalDeviceTv} class. */
 public class HdmiCecLocalDeviceTvTest {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageTest.java
index 036d084..cca5094 100755
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageTest.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.hdmi;
 
+import android.platform.test.annotations.Presubmit;
+
 import androidx.test.filters.SmallTest;
 
 import com.google.common.testing.EqualsTester;
@@ -25,6 +27,7 @@
 
 /** Tests for {@link HdmiCecMessage} class. */
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 public class HdmiCecMessageTest {
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
index 5074f64..22ad956 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
@@ -29,9 +29,11 @@
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.IHdmiControlCallback;
+import android.hardware.tv.cec.V1_0.SendMessageResult;
 import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -47,6 +49,7 @@
 
 /** Tests for {@link OneTouchPlayAction} */
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 public class OneTouchPlayActionTest {
     private static final byte[] POWER_ON = new byte[]{HdmiControlManager.POWER_STATUS_ON};
@@ -180,13 +183,16 @@
     public void succeedAfterGettingPowerStatusOn_Cec14b() throws Exception {
         setUp(true);
 
-        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
                 mHdmiControlService);
         playbackDevice.init();
         mLocalDevices.add(playbackDevice);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
+
+        mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
+        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
+        mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
 
         TestActionTimer actionTimer = new TestActionTimer();
@@ -230,13 +236,16 @@
     public void succeedAfterGettingTransientPowerStatus_Cec14b() throws Exception {
         setUp(true);
 
-        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
                 mHdmiControlService);
         playbackDevice.init();
         mLocalDevices.add(playbackDevice);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
+
+        mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
+        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
+        mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
 
         TestActionTimer actionTimer = new TestActionTimer();
@@ -293,13 +302,16 @@
     public void timeOut_Cec14b() throws Exception {
         setUp(true);
 
-        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
                 mHdmiControlService);
         playbackDevice.init();
         mLocalDevices.add(playbackDevice);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
+
+        mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
+        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
+        mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
 
         TestActionTimer actionTimer = new TestActionTimer();
@@ -339,13 +351,15 @@
     @Test
     public void succeedIfPowerStatusOn_Cec20() throws Exception {
         setUp(true);
-
-        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
                 mHdmiControlService);
         playbackDevice.init();
         mLocalDevices.add(playbackDevice);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        mTestLooper.dispatchAll();
+
+        mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
+        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         mHdmiControlService.getHdmiCecNetwork().updateDevicePowerStatus(ADDR_TV,
                 HdmiControlManager.POWER_STATUS_ON);
         mTestLooper.dispatchAll();
@@ -377,13 +391,15 @@
     @Test
     public void succeedIfPowerStatusUnknown_Cec20() throws Exception {
         setUp(true);
-
-        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
                 mHdmiControlService);
         playbackDevice.init();
         mLocalDevices.add(playbackDevice);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        mTestLooper.dispatchAll();
+
+        mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
+        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         mHdmiControlService.getHdmiCecNetwork().updateDevicePowerStatus(ADDR_TV,
                 HdmiControlManager.POWER_STATUS_UNKNOWN);
         mTestLooper.dispatchAll();
@@ -429,13 +445,15 @@
     @Test
     public void succeedIfPowerStatusStandby_Cec20() throws Exception {
         setUp(true);
-
-        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
                 mHdmiControlService);
         playbackDevice.init();
         mLocalDevices.add(playbackDevice);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
+        mTestLooper.dispatchAll();
+
+        mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
+        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         mHdmiControlService.getHdmiCecNetwork().updateDevicePowerStatus(ADDR_TV,
                 HdmiControlManager.POWER_STATUS_STANDBY);
         mTestLooper.dispatchAll();
@@ -565,7 +583,6 @@
     public void pendingActionDoesNotBlockSendingStandby_Cec14b() throws Exception {
         setUp(true);
 
-        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
         HdmiCecLocalDevicePlayback playbackDevice = new HdmiCecLocalDevicePlayback(
                 mHdmiControlService);
         playbackDevice.init();
@@ -575,6 +592,10 @@
                 HdmiControlManager.POWER_CONTROL_MODE_TV);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
+
+        mNativeWrapper.setPollAddressResponse(ADDR_TV, SendMessageResult.SUCCESS);
+        mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
+        mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
 
         TestActionTimer actionTimer = new TestActionTimer();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
index 2b49095..2f22bce 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
@@ -33,6 +33,7 @@
 import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -48,6 +49,7 @@
 
 /** Tests for {@link PowerStatusMonitorAction} */
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 public class PowerStatusMonitorActionTest {
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index 9589b73..823ed2a 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.os.Looper;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -41,6 +42,7 @@
 import java.util.List;
 
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 public class RequestSadActionTest {
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
index 0916b12..302c0ac 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
@@ -35,6 +35,7 @@
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.os.Looper;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -51,6 +52,7 @@
 import java.util.List;
 
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 public class RoutingControlActionTest {
     /*
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
index 949cf9f..b34b853 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
@@ -31,6 +31,7 @@
 import android.media.AudioManager;
 import android.os.Looper;
 import android.os.test.TestLooper;
+import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -47,6 +48,7 @@
  * Test for {@link SystemAudioAutoInitiationAction}.
  */
 @SmallTest
+@Presubmit
 @RunWith(JUnit4.class)
 public class SystemAudioAutoInitiationActionTest {
 
diff --git a/services/tests/servicestests/src/com/android/server/locales/FakePackageConfigurationUpdater.java b/services/tests/servicestests/src/com/android/server/locales/FakePackageConfigurationUpdater.java
new file mode 100644
index 0000000..1613b11
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locales/FakePackageConfigurationUpdater.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locales;
+
+import android.annotation.Nullable;
+import android.os.LocaleList;
+
+import com.android.server.wm.ActivityTaskManagerInternal.PackageConfigurationUpdater;
+
+/**
+ * Test double for the {@link PackageConfigurationUpdater}. For use in
+ * {@link LocaleManagerServiceTest}s to stub out storage and check for state-based changes.
+ */
+class FakePackageConfigurationUpdater implements PackageConfigurationUpdater {
+
+    FakePackageConfigurationUpdater() {}
+
+    LocaleList mLocales = null;
+
+    @Override
+    public PackageConfigurationUpdater setNightMode(int nightMode) {
+        return this;
+    }
+
+    @Override
+    public PackageConfigurationUpdater setLocales(LocaleList locales) {
+        mLocales = locales;
+        return this;
+    }
+
+    @Override
+    public void commit() {}
+
+    /**
+     * Returns the locales that were stored during the test run. Returns {@code null} if no locales
+     * were set.
+     */
+    @Nullable
+    LocaleList getStoredLocales() {
+        return mLocales;
+    }
+
+}
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
new file mode 100644
index 0000000..1141d9b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locales;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.fail;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.Manifest;
+import android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.content.pm.PackageManagerInternal;
+import android.os.Binder;
+import android.os.LocaleList;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.server.wm.ActivityTaskManagerInternal.PackageConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/**
+ * Unit tests for the {@link LocaleManagerService}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class LocaleManagerServiceTest {
+    private static final String DEFAULT_PACKAGE_NAME = "com.android.myapp";
+    private static final int DEFAULT_USER_ID = 0;
+    private static final int DEFAULT_UID = Binder.getCallingUid() + 100;
+    private static final int INVALID_UID = -1;
+    private static final String DEFAULT_LOCALE_TAGS = "en-XC, ar-XB";
+    private static final LocaleList DEFAULT_LOCALES =
+            LocaleList.forLanguageTags(DEFAULT_LOCALE_TAGS);
+
+    private LocaleManagerService mLocaleManagerService;
+
+    @Mock
+    private Context mMockContext;
+    @Mock
+    private PackageManagerInternal mMockPackageManager;
+    @Mock
+    private FakePackageConfigurationUpdater mFakePackageConfigurationUpdater;
+    @Mock
+    private ActivityTaskManagerInternal mMockActivityTaskManager;
+    @Mock
+    private ActivityManagerInternal mMockActivityManager;
+
+    @Before
+    public void setUp() {
+        mMockContext = mock(Context.class);
+        mMockActivityTaskManager = mock(ActivityTaskManagerInternal.class);
+        mMockActivityManager = mock(ActivityManagerInternal.class);
+        mMockPackageManager = mock(PackageManagerInternal.class);
+
+        mFakePackageConfigurationUpdater = new FakePackageConfigurationUpdater();
+        doReturn(mFakePackageConfigurationUpdater)
+                .when(mMockActivityTaskManager)
+                .createPackageConfigurationUpdater(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+        doReturn(mFakePackageConfigurationUpdater)
+                .when(mMockActivityTaskManager).createPackageConfigurationUpdater();
+
+        doReturn(DEFAULT_USER_ID).when(mMockActivityManager)
+                .handleIncomingUser(anyInt(), anyInt(), eq(DEFAULT_USER_ID), anyBoolean(), anyInt(),
+                        anyString(), anyString());
+
+        mLocaleManagerService = new LocaleManagerService(mMockContext, mMockActivityTaskManager,
+                mMockActivityManager, mMockPackageManager);
+    }
+
+    @Test(expected = SecurityException.class)
+    public void testSetApplicationLocales_arbitraryAppWithoutPermissions_fails() throws Exception {
+        doReturn(DEFAULT_UID)
+                .when(mMockPackageManager).getPackageUid(anyString(), anyInt(), anyInt());
+        setUpFailingPermissionCheckFor(Manifest.permission.CHANGE_CONFIGURATION);
+
+        try {
+            mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
+                    LocaleList.getEmptyLocaleList());
+            fail("Expected SecurityException");
+        } finally {
+            verify(mMockContext).enforceCallingPermission(
+                    eq(android.Manifest.permission.CHANGE_CONFIGURATION),
+                    anyString());
+            assertNoLocalesStored(mFakePackageConfigurationUpdater.getStoredLocales());
+        }
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testSetApplicationLocales_nullPackageName_fails() throws Exception {
+        try {
+            mLocaleManagerService.setApplicationLocales(/* appPackageName = */ null,
+                    DEFAULT_USER_ID, LocaleList.getEmptyLocaleList());
+            fail("Expected NullPointerException");
+        } finally {
+            assertNoLocalesStored(mFakePackageConfigurationUpdater.getStoredLocales());
+        }
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testSetApplicationLocales_nullLocaleList_fails() throws Exception {
+        setUpPassingPermissionCheckFor(Manifest.permission.CHANGE_CONFIGURATION);
+
+        try {
+            mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
+                    /* locales = */ null);
+            fail("Expected NullPointerException");
+        } finally {
+            assertNoLocalesStored(mFakePackageConfigurationUpdater.getStoredLocales());
+        }
+    }
+
+
+    @Test
+    public void testSetApplicationLocales_arbitraryAppWithPermission_succeeds() throws Exception {
+        doReturn(DEFAULT_UID)
+                .when(mMockPackageManager).getPackageUid(anyString(), anyInt(), anyInt());
+        // if package is not owned by the caller, the calling app should have the following
+        //   permission. We will mock this to succeed to imitate that.
+        setUpPassingPermissionCheckFor(Manifest.permission.CHANGE_CONFIGURATION);
+
+        mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
+                DEFAULT_LOCALES);
+
+        assertEquals(DEFAULT_LOCALES, mFakePackageConfigurationUpdater.getStoredLocales());
+
+    }
+
+    @Test
+    public void testSetApplicationLocales_callerOwnsPackage_succeeds() throws Exception {
+        doReturn(Binder.getCallingUid())
+                .when(mMockPackageManager).getPackageUid(anyString(), anyInt(), anyInt());
+
+        mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
+                DEFAULT_LOCALES);
+
+        assertEquals(DEFAULT_LOCALES, mFakePackageConfigurationUpdater.getStoredLocales());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testSetApplicationLocales_invalidPackageOrUserId_fails() throws Exception {
+        doReturn(INVALID_UID)
+                .when(mMockPackageManager).getPackageUid(anyString(), anyInt(), anyInt());
+        try {
+            mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
+                    LocaleList.getEmptyLocaleList());
+            fail("Expected IllegalArgumentException");
+        } finally {
+            assertNoLocalesStored(mFakePackageConfigurationUpdater.getStoredLocales());
+        }
+    }
+
+    @Test(expected = SecurityException.class)
+    public void testGetApplicationLocales_arbitraryAppWithoutPermission_fails() throws Exception {
+        doReturn(DEFAULT_UID).when(mMockPackageManager)
+                .getPackageUid(anyString(), anyInt(), anyInt());
+        setUpFailingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES);
+
+        try {
+            mLocaleManagerService.getApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+            fail("Expected SecurityException");
+        } finally {
+            verify(mMockContext).enforceCallingPermission(
+                    eq(android.Manifest.permission.READ_APP_SPECIFIC_LOCALES),
+                    anyString());
+        }
+    }
+
+    @Test
+    public void testGetApplicationLocales_appSpecificConfigAbsent_returnsEmptyList()
+            throws Exception {
+        // any valid app calling for its own package or having appropriate permission
+        doReturn(DEFAULT_UID).when(mMockPackageManager)
+                .getPackageUid(anyString(), anyInt(), anyInt());
+        setUpPassingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES);
+        doReturn(null)
+                .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
+
+        LocaleList locales = mLocaleManagerService.getApplicationLocales(
+                    DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+
+        assertEquals(LocaleList.getEmptyLocaleList(), locales);
+    }
+
+    @Test
+    public void testGetApplicationLocales_appSpecificLocalesAbsent_returnsEmptyList()
+            throws Exception {
+        doReturn(DEFAULT_UID).when(mMockPackageManager)
+                .getPackageUid(anyString(), anyInt(), anyInt());
+        setUpPassingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES);
+        doReturn(new PackageConfig(/* nightMode = */ 0, /* locales = */ null))
+                .when(mMockActivityTaskManager).getApplicationConfig(any(), anyInt());
+
+        LocaleList locales = mLocaleManagerService.getApplicationLocales(
+                DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+
+        assertEquals(LocaleList.getEmptyLocaleList(), locales);
+    }
+
+    @Test
+    public void testGetApplicationLocales_callerOwnsAppAndConfigPresent_returnsLocales()
+            throws Exception {
+        doReturn(Binder.getCallingUid()).when(mMockPackageManager)
+                .getPackageUid(anyString(), anyInt(), anyInt());
+        doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
+                .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
+
+        LocaleList locales =
+                mLocaleManagerService.getApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+
+        assertEquals(DEFAULT_LOCALES, locales);
+    }
+
+    @Test
+    public void testGetApplicationLocales_arbitraryCallerWithPermissions_returnsLocales()
+            throws Exception {
+        doReturn(DEFAULT_UID).when(mMockPackageManager)
+                .getPackageUid(anyString(), anyInt(), anyInt());
+        setUpPassingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES);
+        doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
+                .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
+
+        LocaleList locales =
+                mLocaleManagerService.getApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
+
+        assertEquals(DEFAULT_LOCALES, locales);
+    }
+
+    private static void assertNoLocalesStored(LocaleList locales) {
+        assertNull(locales);
+    }
+
+    private void setUpFailingPermissionCheckFor(String permission) {
+        doThrow(new SecurityException("Mock"))
+                .when(mMockContext).enforceCallingPermission(eq(permission), any());
+    }
+
+    private void setUpPassingPermissionCheckFor(String permission) {
+        doNothing().when(mMockContext).enforceCallingPermission(eq(permission), any());
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locales/OWNERS b/services/tests/servicestests/src/com/android/server/locales/OWNERS
new file mode 100644
index 0000000..eecb6ea
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locales/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/locales/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
index 2bda120..847fe2e 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java
@@ -348,24 +348,6 @@
     }
 
     @Test
-    public void testInstallPackageDowngrade() throws Exception {
-        File activeApex = extractResource("test.apex_rebootless_v2",
-                "test.rebootless_apex_v2.apex");
-        ApexInfo activeApexInfo = createApexInfo("test.apex_rebootless", 2, /* isActive= */ true,
-                /* isFactory= */ false, activeApex);
-        when(mApexService.getAllPackages()).thenReturn(new ApexInfo[]{activeApexInfo});
-        mApexManager.scanApexPackagesTraced(mPackageParser2,
-                ParallelPackageParser.makeExecutorService());
-
-        File installedApex = extractResource("test.apex_rebootless_v1",
-                "test.rebootless_apex_v1.apex");
-        PackageManagerException e = expectThrows(PackageManagerException.class,
-                () -> mApexManager.installPackage(installedApex, mPackageParser2));
-        assertThat(e).hasMessageThat().contains(
-                "Downgrade of APEX package test.apex.rebootless is not allowed");
-    }
-
-    @Test
     public void testInstallPackage_activeOnSystem() throws Exception {
         ApexInfo activeApexInfo = createApexInfo("test.apex_rebootless", 1, /* isActive= */ true,
                 /* isFactory= */ true, extractResource("test.apex_rebootless_v1",
diff --git a/services/tests/servicestests/src/com/android/server/pm/permission/LegacyPermissionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/permission/LegacyPermissionManagerServiceTest.java
index acd3fca..3261dfa 100644
--- a/services/tests/servicestests/src/com/android/server/pm/permission/LegacyPermissionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/permission/LegacyPermissionManagerServiceTest.java
@@ -125,7 +125,7 @@
     public void checkDeviceIdentifierAccess_hasPrivilegedPermission_returnsGranted() {
         // Apps with the READ_PRIVILEGED_PHONE_STATE permission should have access to device
         // identifiers.
-        setupCheckDeviceIdentifierAccessTest(SYSTEM_PID, SYSTEM_UID);
+        setupCheckDeviceIdentifierAccessTest(SYSTEM_PID, SYSTEM_UID, APP_UID);
         when(mInjector.checkPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
                 APP_PID, APP_UID)).thenReturn(PackageManager.PERMISSION_GRANTED);
 
@@ -140,7 +140,7 @@
     public void checkDeviceIdentifierAccess_hasAppOp_returnsGranted() {
         // Apps that have been granted the READ_DEVICE_IDENTIFIERS appop should have access to
         // device identifiers.
-        setupCheckDeviceIdentifierAccessTest(SYSTEM_PID, SYSTEM_UID);
+        setupCheckDeviceIdentifierAccessTest(SYSTEM_PID, SYSTEM_UID, APP_UID);
         when(mAppOpsManager.noteOpNoThrow(eq(AppOpsManager.OPSTR_READ_DEVICE_IDENTIFIERS),
                 eq(APP_UID), eq(mPackageName), any(), any())).thenReturn(
                 AppOpsManager.MODE_ALLOWED);
@@ -156,7 +156,7 @@
     public void checkDeviceIdentifierAccess_hasDpmAccess_returnsGranted() {
         // Apps that pass a DevicePolicyManager device / profile owner check should have access to
         // device identifiers.
-        setupCheckDeviceIdentifierAccessTest(SYSTEM_PID, SYSTEM_UID);
+        setupCheckDeviceIdentifierAccessTest(SYSTEM_PID, SYSTEM_UID, APP_UID);
         when(mDevicePolicyManager.hasDeviceIdentifierAccess(mPackageName, APP_PID,
                 APP_UID)).thenReturn(true);
 
@@ -236,7 +236,7 @@
         // both the permission and the appop must be granted. If the permission is granted but the
         // appop is not then AppOpsManager#MODE_IGNORED should be returned to indicate that this
         // should be a silent failure.
-        setupCheckPhoneNumberAccessTest(SYSTEM_PID, SYSTEM_UID);
+        setupCheckPhoneNumberAccessTest(SYSTEM_PID, SYSTEM_UID, APP_UID);
         setPackageTargetSdk(Build.VERSION_CODES.Q);
 
         grantPermissionAndAppop(android.Manifest.permission.READ_PHONE_STATE, null);
@@ -256,7 +256,7 @@
         // Apps targeting R+ with just the READ_PHONE_STATE permission granted should not have
         // access to the phone number; PERMISSION_DENIED should be returned both with and without
         // the appop granted since this check should be skipped for target SDK R+.
-        setupCheckPhoneNumberAccessTest(SYSTEM_PID, SYSTEM_UID);
+        setupCheckPhoneNumberAccessTest(SYSTEM_PID, SYSTEM_UID, APP_UID);
 
         grantPermissionAndAppop(android.Manifest.permission.READ_PHONE_STATE, null);
         int resultWithoutAppop = mLegacyPermissionManagerService.checkPhoneNumberAccess(
@@ -319,12 +319,79 @@
         assertEquals(PackageManager.PERMISSION_GRANTED, resultWithAppop);
     }
 
+    @Test
+    public void checkPhoneNumberAccess_providedUidDoesNotMatchPackageUid_throwsException()
+            throws Exception {
+        // An app can directly interact with one of the services that accepts a package name and
+        // returns a protected resource via a direct binder transact. This app could then provide
+        // the name of another app that targets pre-R, then determine if the app is installed based
+        // on whether the service throws an exception or not. While the app can provide the package
+        // name of another app, it cannot specify the package uid which is passed to the
+        // LegacyPermissionManager using Binder#getCallingUid. Ultimately this uid should then be
+        // compared against the actual uid of the package to ensure information about packages
+        // installed on the device is not leaked.
+        setupCheckPhoneNumberAccessTest(SYSTEM_PID, SYSTEM_UID, APP_UID + 1);
+
+        assertThrows(SecurityException.class,
+                () -> mLegacyPermissionManagerService.checkPhoneNumberAccess(mPackageName,
+                        CHECK_PHONE_NUMBER_MESSAGE, null, APP_PID, APP_UID));
+    }
+
+    @Test
+    public void checkPhoneNumberAccess_nullPackageNameSystemUid_returnsGranted() throws Exception {
+        // The platform can pass a null package name when checking if the platform itself has
+        // access to the device phone number(s) / identifier(s). This test ensures if a null package
+        // is provided, then the package uid check is skipped and the test is based on whether the
+        // the provided uid / pid has been granted the privileged permission.
+        setupCheckPhoneNumberAccessTest(SYSTEM_PID, SYSTEM_UID, -1);
+        when(mInjector.checkPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+                SYSTEM_PID, SYSTEM_UID)).thenReturn(PackageManager.PERMISSION_GRANTED);
+
+        int result = mLegacyPermissionManagerService.checkPhoneNumberAccess(null,
+                CHECK_PHONE_NUMBER_MESSAGE, null, SYSTEM_PID, SYSTEM_UID);
+
+        assertEquals(PackageManager.PERMISSION_GRANTED, result);
+    }
+
+    @Test
+    public void checkPhoneNumberAccess_systemUidMismatchPackageUid_returnsGranted()
+            throws Exception {
+        // When the platform is checking device phone number / identifier access checks for other
+        // components on the platform, a uid less than the first application UID is provided; this
+        // test verifies the package uid check is skipped and access is still granted with the
+        // privileged permission.
+        int telephonyUid = SYSTEM_UID + 1;
+        int telephonyPid = SYSTEM_PID + 1;
+        setupCheckPhoneNumberAccessTest(SYSTEM_PID, SYSTEM_UID, -1);
+        when(mInjector.checkPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
+                telephonyPid, telephonyUid)).thenReturn(PackageManager.PERMISSION_GRANTED);
+
+        int result = mLegacyPermissionManagerService.checkPhoneNumberAccess(mPackageName,
+                CHECK_PHONE_NUMBER_MESSAGE, null, telephonyPid, telephonyUid);
+
+        assertEquals(PackageManager.PERMISSION_GRANTED, result);
+    }
+
     /**
      * Configures device identifier access tests to fail; tests verifying access should individually
      * set an access check to succeed to verify access when that condition is met.
      */
     private void setupCheckDeviceIdentifierAccessTest(int callingPid, int callingUid) {
-        setupAccessTest(callingPid, callingUid);
+        setupCheckDeviceIdentifierAccessTest(callingPid, callingUid, callingUid);
+    }
+
+    /**
+     * Configures device identifier access tests to fail; tests verifying access should individually
+     * set an access check to succeed to verify access when that condition is met.
+     *
+     * <p>To prevent leaking package information, access checks for package UIDs >= {@link
+     * android.os.Process#FIRST_APPLICATION_UID} must ensure the provided uid matches the uid of
+     * the package being checked; to ensure this check is successful, this method accepts the
+     * {@code packageUid} to be used for the package being checked.
+     */
+    public void setupCheckDeviceIdentifierAccessTest(int callingPid, int callingUid,
+            int packageUid) {
+        setupAccessTest(callingPid, callingUid, packageUid);
 
         when(mDevicePolicyManager.hasDeviceIdentifierAccess(anyString(), anyInt(),
                 anyInt())).thenReturn(false);
@@ -333,11 +400,26 @@
     }
 
     /**
-     * Configures phone number access tests to fail; tests verifying access should individually set
-     * an access check to succeed to verify access when that condition is met.
+     * Configures phone number access tests to fail; tests verifying access should individually
+     * set an access check to succeed to verify access when that condition is set.
+     *
      */
     private void setupCheckPhoneNumberAccessTest(int callingPid, int callingUid) throws Exception {
-        setupAccessTest(callingPid, callingUid);
+        setupCheckPhoneNumberAccessTest(callingPid, callingUid, callingUid);
+    }
+
+    /**
+     * Configures phone number access tests to fail; tests verifying access should individually set
+     * an access check to succeed to verify access when that condition is met.
+     *
+     * <p>To prevent leaking package information, access checks for package UIDs >= {@link
+     * android.os.Process#FIRST_APPLICATION_UID} must ensure the provided uid matches the uid of
+     * the package being checked; to ensure this check is successful, this method accepts the
+     * {@code packageUid} to be used for the package being checked.
+     */
+    private void setupCheckPhoneNumberAccessTest(int callingPid, int callingUid, int packageUid)
+            throws Exception {
+        setupAccessTest(callingPid, callingUid, packageUid);
         setPackageTargetSdk(Build.VERSION_CODES.R);
     }
 
@@ -345,9 +427,10 @@
      * Configures the common mocks for any access tests using the provided {@code callingPid}
      * and {@code callingUid}.
      */
-    private void setupAccessTest(int callingPid, int callingUid) {
+    private void setupAccessTest(int callingPid, int callingUid, int packageUid) {
         when(mInjector.getCallingPid()).thenReturn(callingPid);
         when(mInjector.getCallingUid()).thenReturn(callingUid);
+        when(mInjector.getPackageUidForUser(anyString(), anyInt())).thenReturn(packageUid);
 
         when(mInjector.checkPermission(anyString(), anyInt(), anyInt())).thenReturn(
                 PackageManager.PERMISSION_DENIED);
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
index e84e365..e47a07c 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -866,6 +866,8 @@
         startSystem();
         advanceTime(20);
         assertThat(mService.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
+        assertThat(mService.getBinderServiceInstance().getLastSleepReason()).isEqualTo(
+                PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE);
     }
 
     @Test
@@ -886,6 +888,80 @@
         assertThat(mService.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
         advanceTime(60);
         assertThat(mService.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
+        assertThat(mService.getBinderServiceInstance().getLastSleepReason()).isEqualTo(
+                PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE);
+    }
+
+    @Test
+    public void testInattentiveSleep_wakeLockOnAfterRelease_inattentiveSleepTimeoutNotAffected()
+            throws Exception {
+        final DisplayInfo info = new DisplayInfo();
+        info.displayGroupId = Display.DEFAULT_DISPLAY_GROUP;
+        when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn(info);
+
+        final String pkg = mContextSpy.getOpPackageName();
+        final Binder token = new Binder();
+        final String tag = "testInattentiveSleep_wakeLockOnAfterRelease";
+
+        setMinimumScreenOffTimeoutConfig(5);
+        setAttentiveTimeout(2000);
+        createService();
+        startSystem();
+
+        mService.getBinderServiceInstance().acquireWakeLock(token,
+                PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, tag, pkg,
+                null /* workSource */, null /* historyTag */, Display.DEFAULT_DISPLAY);
+
+        advanceTime(1500);
+        mService.getBinderServiceInstance().releaseWakeLock(token, 0 /* flags */);
+
+        advanceTime(520);
+        assertThat(mService.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
+        assertThat(mService.getBinderServiceInstance().getLastSleepReason()).isEqualTo(
+                PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE);
+    }
+
+    @Test
+    public void testInattentiveSleep_userActivityNoChangeLights_inattentiveSleepTimeoutNotAffected()
+            throws Exception {
+        final DisplayInfo info = new DisplayInfo();
+        info.displayGroupId = Display.DEFAULT_DISPLAY_GROUP;
+        when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn(info);
+
+        setMinimumScreenOffTimeoutConfig(5);
+        setAttentiveTimeout(2000);
+        createService();
+        startSystem();
+
+        advanceTime(1500);
+        mService.getBinderServiceInstance().userActivity(Display.DEFAULT_DISPLAY, mClock.now(),
+                PowerManager.USER_ACTIVITY_EVENT_OTHER,
+                PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS);
+
+        advanceTime(520);
+        assertThat(mService.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP);
+        assertThat(mService.getBinderServiceInstance().getLastSleepReason()).isEqualTo(
+                PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE);
+    }
+
+    @Test
+    public void testInattentiveSleep_userActivity_inattentiveSleepTimeoutExtended()
+            throws Exception {
+        final DisplayInfo info = new DisplayInfo();
+        info.displayGroupId = Display.DEFAULT_DISPLAY_GROUP;
+        when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn(info);
+
+        setMinimumScreenOffTimeoutConfig(5);
+        setAttentiveTimeout(2000);
+        createService();
+        startSystem();
+
+        advanceTime(1500);
+        mService.getBinderServiceInstance().userActivity(Display.DEFAULT_DISPLAY, mClock.now(),
+                PowerManager.USER_ACTIVITY_EVENT_OTHER, 0 /* flags */);
+
+        advanceTime(520);
+        assertThat(mService.getWakefulnessLocked()).isNotEqualTo(WAKEFULNESS_ASLEEP);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/OWNERS b/services/tests/servicestests/src/com/android/server/timedetector/OWNERS
index 8f80897..a0f46e1 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/timedetector/OWNERS
@@ -1,3 +1,2 @@
 # Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+include /services/core/java/com/android/server/timedetector/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/timezone/OWNERS b/services/tests/servicestests/src/com/android/server/timezone/OWNERS
index 8f80897..6165260 100644
--- a/services/tests/servicestests/src/com/android/server/timezone/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/timezone/OWNERS
@@ -1,3 +1,2 @@
-# Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+# Bug component: 24949
+include /services/core/java/com/android/server/timezone/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/OWNERS b/services/tests/servicestests/src/com/android/server/timezonedetector/OWNERS
index 8f80897..a6ff1ba 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/OWNERS
@@ -1,3 +1,2 @@
 # Bug component: 847766
-mingaleev@google.com
-include /core/java/android/app/timedetector/OWNERS
+include /services/core/java/com/android/server/timezonedetector/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java
index da746ca..27d9546 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ControllerImplTest.java
@@ -36,6 +36,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.platform.test.annotations.Presubmit;
+import android.service.timezone.TimeZoneProviderEvent;
 import android.service.timezone.TimeZoneProviderSuggestion;
 import android.util.IndentingPrintWriter;
 
@@ -65,9 +66,9 @@
     private static final TimeZoneProviderEvent USER1_SUCCESS_LOCATION_TIME_ZONE_EVENT2 =
             createSuggestionEvent(asList("Europe/Paris"));
     private static final TimeZoneProviderEvent USER1_UNCERTAIN_LOCATION_TIME_ZONE_EVENT =
-            TimeZoneProviderEvent.createUncertainEvent();
+            TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_TIME_MILLIS);
     private static final TimeZoneProviderEvent USER1_PERM_FAILURE_LOCATION_TIME_ZONE_EVENT =
-            TimeZoneProviderEvent.createPermanentFailureEvent("Test");
+            TimeZoneProviderEvent.createPermanentFailureEvent(ARBITRARY_TIME_MILLIS, "Test");
 
     private TestThreadingDomain mTestThreadingDomain;
     private TestCallback mTestCallback;
@@ -1124,6 +1125,7 @@
 
     private static TimeZoneProviderEvent createSuggestionEvent(@NonNull List<String> timeZoneIds) {
         return TimeZoneProviderEvent.createSuggestionEvent(
+                ARBITRARY_TIME_MILLIS,
                 new TimeZoneProviderSuggestion.Builder()
                         .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS)
                         .setTimeZoneIds(timeZoneIds)
@@ -1136,10 +1138,13 @@
         // (initialization timeout * 2) < uncertainty delay
         //
         // That makes the order of initialization timeout Vs uncertainty delay deterministic.
-        static final Duration PROVIDER_INITIALIZATION_TIMEOUT = Duration.ofMinutes(5);
-        static final Duration PROVIDER_INITIALIZATION_TIMEOUT_FUZZ = Duration.ofMinutes(1);
+        private static final Duration PROVIDER_INITIALIZATION_TIMEOUT = Duration.ofMinutes(5);
+        private static final Duration PROVIDER_INITIALIZATION_TIMEOUT_FUZZ = Duration.ofMinutes(1);
         private static final Duration UNCERTAINTY_DELAY = Duration.ofMinutes(15);
 
+        private static final Duration PROVIDER_EVENT_FILTERING_AGE_THRESHOLD =
+                Duration.ofMinutes(3);
+
         private final LocationTimeZoneProviderController mController;
         private ConfigurationInternal mConfigurationInternal;
 
@@ -1172,6 +1177,11 @@
         }
 
         @Override
+        Duration getProviderEventFilteringAgeThreshold() {
+            return PROVIDER_EVENT_FILTERING_AGE_THRESHOLD;
+        }
+
+        @Override
         Duration getUncertaintyDelay() {
             return UNCERTAINTY_DELAY;
         }
@@ -1254,7 +1264,7 @@
         }
 
         @Override
-        void onStartUpdates(Duration initializationTimeout) {
+        void onStartUpdates(Duration initializationTimeout, Duration eventFilteringAgeThreshold) {
             // Nothing needed for tests.
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java
index e75d05c..52e9d3a 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/FakeTimeZoneProviderEventPreProcessor.java
@@ -16,6 +16,8 @@
 
 package com.android.server.timezonedetector.location;
 
+import android.service.timezone.TimeZoneProviderEvent;
+
 /**
  * Fake implementation of {@link TimeZoneProviderEventPreProcessor} which assumes that all events
  * are valid or always uncertain if {@link #enterUncertainMode()} was called.
@@ -28,7 +30,8 @@
     @Override
     public TimeZoneProviderEvent preProcess(TimeZoneProviderEvent timeZoneProviderEvent) {
         if (mIsUncertain) {
-            return TimeZoneProviderEvent.createUncertainEvent();
+            return TimeZoneProviderEvent.createUncertainEvent(
+                    timeZoneProviderEvent.getCreationElapsedMillis());
         }
         return timeZoneProviderEvent;
     }
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
index 03d56c7..cb2905d 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/LocationTimeZoneProviderTest.java
@@ -32,6 +32,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.platform.test.annotations.Presubmit;
+import android.service.timezone.TimeZoneProviderEvent;
 import android.service.timezone.TimeZoneProviderSuggestion;
 import android.util.IndentingPrintWriter;
 
@@ -91,10 +92,12 @@
         ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED;
         Duration arbitraryInitializationTimeout = Duration.ofMinutes(5);
         Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2);
+        Duration arbitraryEventFilteringAgeThreshold = Duration.ofMinutes(3);
         provider.startUpdates(config, arbitraryInitializationTimeout,
-                arbitraryInitializationTimeoutFuzz);
+                arbitraryInitializationTimeoutFuzz, arbitraryEventFilteringAgeThreshold);
 
-        provider.assertOnStartCalled(arbitraryInitializationTimeout);
+        provider.assertOnStartCalled(
+                arbitraryInitializationTimeout, arbitraryEventFilteringAgeThreshold);
 
         currentState = assertAndReturnProviderState(
                 provider, providerMetricsLogger, PROVIDER_STATE_STARTED_INITIALIZING);
@@ -117,7 +120,8 @@
                 .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS)
                 .setTimeZoneIds(Arrays.asList("Europe/London"))
                 .build();
-        TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(suggestion);
+        TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
+                ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion);
         provider.simulateProviderEventReceived(event);
 
         currentState = assertAndReturnProviderState(
@@ -129,7 +133,7 @@
         mProviderListener.assertProviderChangeReported(PROVIDER_STATE_STARTED_CERTAIN);
 
         // Simulate an uncertain event being received.
-        event = TimeZoneProviderEvent.createUncertainEvent();
+        event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS);
         provider.simulateProviderEventReceived(event);
 
         currentState = assertAndReturnProviderState(
@@ -178,8 +182,9 @@
         ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED;
         Duration arbitraryInitializationTimeout = Duration.ofMinutes(5);
         Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2);
+        Duration eventFilteringAgeThreshold = Duration.ofMinutes(3);
         provider.startUpdates(config, arbitraryInitializationTimeout,
-                arbitraryInitializationTimeoutFuzz);
+                arbitraryInitializationTimeoutFuzz, eventFilteringAgeThreshold);
         provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_INITIALIZING);
 
         // Simulate a suggestion event being received.
@@ -187,12 +192,13 @@
                 .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS)
                 .setTimeZoneIds(Arrays.asList("Europe/London"))
                 .build();
-        TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(suggestion);
+        TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
+                ARBITRARY_ELAPSED_REALTIME_MILLIS, suggestion);
         provider.simulateProviderEventReceived(event);
         provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_CERTAIN);
 
         // Simulate an uncertain event being received.
-        event = TimeZoneProviderEvent.createUncertainEvent();
+        event = TimeZoneProviderEvent.createUncertainEvent(ARBITRARY_ELAPSED_REALTIME_MILLIS);
         provider.simulateProviderEventReceived(event);
         provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_UNCERTAIN);
 
@@ -220,16 +226,17 @@
         ConfigurationInternal config = USER1_CONFIG_GEO_DETECTION_ENABLED;
         Duration arbitraryInitializationTimeout = Duration.ofMinutes(5);
         Duration arbitraryInitializationTimeoutFuzz = Duration.ofMinutes(2);
+        Duration eventFilteringAgeThreshold = Duration.ofMinutes(3);
         provider.startUpdates(config, arbitraryInitializationTimeout,
-                arbitraryInitializationTimeoutFuzz);
+                arbitraryInitializationTimeoutFuzz, eventFilteringAgeThreshold);
 
         List<String> invalidTimeZoneIds = asList("Atlantic/Atlantis");
         TimeZoneProviderSuggestion invalidIdSuggestion = new TimeZoneProviderSuggestion.Builder()
                 .setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS)
                 .setTimeZoneIds(invalidTimeZoneIds)
                 .build();
-        TimeZoneProviderEvent event =
-                TimeZoneProviderEvent.createSuggestionEvent(invalidIdSuggestion);
+        TimeZoneProviderEvent event = TimeZoneProviderEvent.createSuggestionEvent(
+                ARBITRARY_ELAPSED_REALTIME_MILLIS, invalidIdSuggestion);
         provider.simulateProviderEventReceived(event);
         provider.assertLatestRecordedState(PROVIDER_STATE_STARTED_UNCERTAIN);
     }
@@ -277,6 +284,7 @@
         private boolean mOnDestroyCalled;
         private boolean mOnStartUpdatesCalled;
         private Duration mInitializationTimeout;
+        private Duration mEventFilteringAgeThreshold;
         private boolean mOnStopUpdatesCalled;
 
         /** Creates the instance. */
@@ -300,9 +308,11 @@
         }
 
         @Override
-        void onStartUpdates(@NonNull Duration initializationTimeout) {
+        void onStartUpdates(@NonNull Duration initializationTimeout,
+                @NonNull Duration eventFilteringAgeThreshold) {
             mOnStartUpdatesCalled = true;
             mInitializationTimeout = initializationTimeout;
+            mEventFilteringAgeThreshold = eventFilteringAgeThreshold;
         }
 
         @Override
@@ -319,9 +329,11 @@
             assertTrue(mOnInitializeCalled);
         }
 
-        void assertOnStartCalled(Duration expectedInitializationTimeout) {
+        void assertOnStartCalled(Duration expectedInitializationTimeout,
+                Duration eventFilteringAgeThreshold) {
             assertTrue(mOnStartUpdatesCalled);
             assertEquals(expectedInitializationTimeout, mInitializationTimeout);
+            assertEquals(eventFilteringAgeThreshold, mEventFilteringAgeThreshold);
         }
 
         void simulateProviderEventReceived(TimeZoneProviderEvent event) {
diff --git a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
index 173705be..ab4fe29 100644
--- a/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
+++ b/services/tests/servicestests/src/com/android/server/timezonedetector/location/ZoneInfoDbTimeZoneProviderEventPreProcessorTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import android.platform.test.annotations.Presubmit;
+import android.service.timezone.TimeZoneProviderEvent;
 import android.service.timezone.TimeZoneProviderSuggestion;
 
 import org.junit.Test;
@@ -53,14 +54,17 @@
         for (String timeZone : nonExistingTimeZones) {
             TimeZoneProviderEvent event = timeZoneProviderEvent(timeZone);
 
+            TimeZoneProviderEvent expectedResultEvent =
+                    TimeZoneProviderEvent.createUncertainEvent(event.getCreationElapsedMillis());
             assertWithMessage(timeZone + " is not a valid time zone")
                     .that(mPreProcessor.preProcess(event))
-                    .isEqualTo(TimeZoneProviderEvent.createUncertainEvent());
+                    .isEqualTo(expectedResultEvent);
         }
     }
 
     private static TimeZoneProviderEvent timeZoneProviderEvent(String... timeZoneIds) {
         return TimeZoneProviderEvent.createSuggestionEvent(
+                ARBITRARY_TIME_MILLIS,
                 new TimeZoneProviderSuggestion.Builder()
                         .setTimeZoneIds(Arrays.asList(timeZoneIds))
                         .setElapsedRealtimeMillis(ARBITRARY_TIME_MILLIS)
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/RampDownAdapterTest.java b/services/tests/servicestests/src/com/android/server/vibrator/RampDownAdapterTest.java
index b90df21..4c3312c 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/RampDownAdapterTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/RampDownAdapterTest.java
@@ -103,21 +103,18 @@
     public void testStepSegments_withShortZeroSegment_replaceWithStepsDown() {
         List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
                 new StepSegment(/* amplitude= */ 1, /* frequency= */ 0, /* duration= */ 10),
-                new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 10),
-                new StepSegment(/* amplitude= */ 0.8f, /* frequency= */ 0, /* duration= */ 100)));
+                new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 10)));
         List<VibrationEffectSegment> expectedSegments = Arrays.asList(
                 new StepSegment(/* amplitude= */ 1, /* frequency= */ 0, /* duration= */ 10),
                 new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 0, /* duration= */ 5),
-                new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 5),
-                new StepSegment(/* amplitude= */ 0.8f, /* frequency= */ 0, /* duration= */ 100));
+                new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 5));
 
-        assertEquals(1, mAdapter.apply(segments, 1, TEST_VIBRATOR_INFO));
-
+        assertEquals(-1, mAdapter.apply(segments, -1, TEST_VIBRATOR_INFO));
         assertEquals(expectedSegments, segments);
     }
 
     @Test
-    public void testStepSegments_withLongZeroSegment_replaceWithStepsDown() {
+    public void testStepSegments_withLongZeroSegment_replaceWithStepsDownWithRemainingOffSegment() {
         List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
                 new StepSegment(/* amplitude= */ 1, /* frequency= */ 0, /* duration= */ 10),
                 new RampSegment(/* startAmplitude= */ 0, /* endAmplitude= */ 0,
@@ -131,9 +128,67 @@
                 new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 35),
                 new StepSegment(/* amplitude= */ 0.8f, /* frequency= */ 0, /* duration= */ 100));
 
+        assertEquals(-1, mAdapter.apply(segments, -1, TEST_VIBRATOR_INFO));
+        assertEquals(expectedSegments, segments);
+    }
+
+    @Test
+    public void testStepSegments_withZeroSegmentBeforeRepeat_fixesRepeat() {
+        List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+                new StepSegment(/* amplitude= */ 1, /* frequency= */ 0, /* duration= */ 10),
+                new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 50),
+                new StepSegment(/* amplitude= */ 0.8f, /* frequency= */ 0, /* duration= */ 100)));
+        List<VibrationEffectSegment> expectedSegments = Arrays.asList(
+                new StepSegment(/* amplitude= */ 1, /* frequency= */ 0, /* duration= */ 10),
+                new StepSegment(/* amplitude= */ 0.75f, /* frequency= */ 0, /* duration= */ 5),
+                new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 0, /* duration= */ 5),
+                new StepSegment(/* amplitude= */ 0.25f, /* frequency= */ 0, /* duration= */ 5),
+                new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 35),
+                new StepSegment(/* amplitude= */ 0.8f, /* frequency= */ 0, /* duration= */ 100));
+
         // Repeat index fixed after intermediate steps added
         assertEquals(5, mAdapter.apply(segments, 2, TEST_VIBRATOR_INFO));
+        assertEquals(expectedSegments, segments);
+    }
 
+    @Test
+    public void testStepSegments_withZeroSegmentAfterRepeat_preservesRepeat() {
+        List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+                new StepSegment(/* amplitude= */ 1, /* frequency= */ 0, /* duration= */ 10),
+                new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 10),
+                new StepSegment(/* amplitude= */ 0.8f, /* frequency= */ 0, /* duration= */ 100)));
+        List<VibrationEffectSegment> expectedSegments = Arrays.asList(
+                new StepSegment(/* amplitude= */ 1, /* frequency= */ 0, /* duration= */ 10),
+                new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 0, /* duration= */ 5),
+                new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 5),
+                new StepSegment(/* amplitude= */ 0.8f, /* frequency= */ 0, /* duration= */ 100));
+
+        assertEquals(3, mAdapter.apply(segments, 2, TEST_VIBRATOR_INFO));
+        assertEquals(expectedSegments, segments);
+    }
+
+    @Test
+    public void testStepSegments_withZeroSegmentAtRepeat_fixesRepeatAndAppendOriginalToListEnd() {
+        List<VibrationEffectSegment> segments = new ArrayList<>(Arrays.asList(
+                new StepSegment(/* amplitude= */ 1, /* frequency= */ 0, /* duration= */ 10),
+                new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 50),
+                new StepSegment(/* amplitude= */ 1, /* frequency= */ 0, /* duration= */ 100)));
+        List<VibrationEffectSegment> expectedSegments = Arrays.asList(
+                new StepSegment(/* amplitude= */ 1, /* frequency= */ 0, /* duration= */ 10),
+                new StepSegment(/* amplitude= */ 0.75f, /* frequency= */ 0, /* duration= */ 5),
+                new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 0, /* duration= */ 5),
+                new StepSegment(/* amplitude= */ 0.25f, /* frequency= */ 0, /* duration= */ 5),
+                new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 35),
+                new StepSegment(/* amplitude= */ 1, /* frequency= */ 0, /* duration= */ 100),
+                // Original zero segment appended to the end of new looping vibration,
+                // then converted to ramp down as well.
+                new StepSegment(/* amplitude= */ 0.75f, /* frequency= */ 0, /* duration= */ 5),
+                new StepSegment(/* amplitude= */ 0.5f, /* frequency= */ 0, /* duration= */ 5),
+                new StepSegment(/* amplitude= */ 0.25f, /* frequency= */ 0, /* duration= */ 5),
+                new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ 35));
+
+        // Repeat index fixed after intermediate steps added
+        assertEquals(5, mAdapter.apply(segments, 1, TEST_VIBRATOR_INFO));
         assertEquals(expectedSegments, segments);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index f9e63d1..5e25bc5 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -1013,6 +1013,8 @@
             throws Exception {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        setRingerMode(AudioManager.RINGER_MODE_NORMAL);
+        setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1);
         createSystemReadyService();
 
         IExternalVibrationController firstController = mock(IExternalVibrationController.class);
@@ -1021,8 +1023,11 @@
                 firstController);
         int firstScale = mExternalVibratorService.onExternalVibrationStart(firstVibration);
 
-        ExternalVibration secondVibration = new ExternalVibration(UID, PACKAGE_NAME, AUDIO_ATTRS,
-                secondController);
+        AudioAttributes ringtoneAudioAttrs = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                .build();
+        ExternalVibration secondVibration = new ExternalVibration(UID, PACKAGE_NAME,
+                ringtoneAudioAttrs, secondController);
         int secondScale = mExternalVibratorService.onExternalVibrationStart(secondVibration);
 
         assertEquals(IExternalVibratorService.SCALE_NONE, firstScale);
@@ -1057,6 +1062,37 @@
                 mVibratorProviders.get(1).getExternalControlStates());
     }
 
+    @Test
+    public void onExternalVibration_withRingtone_usesRingerModeSettings() {
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
+        mVibrator.setDefaultRingVibrationIntensity(Vibrator.VIBRATION_INTENSITY_MEDIUM);
+        AudioAttributes audioAttrs = new AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                .build();
+        ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, audioAttrs,
+                mock(IExternalVibrationController.class));
+
+        setRingerMode(AudioManager.RINGER_MODE_NORMAL);
+        setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 0);
+        setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 0);
+        createSystemReadyService();
+        int scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+        assertEquals(IExternalVibratorService.SCALE_MUTE, scale);
+
+        setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 0);
+        setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 1);
+        createSystemReadyService();
+        scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+        assertEquals(IExternalVibratorService.SCALE_NONE, scale);
+
+        setUserSetting(Settings.System.VIBRATE_WHEN_RINGING, 1);
+        setGlobalSetting(Settings.Global.APPLY_RAMPING_RINGER, 0);
+        createSystemReadyService();
+        scale = mExternalVibratorService.onExternalVibrationStart(externalVibration);
+        assertEquals(IExternalVibratorService.SCALE_NONE, scale);
+    }
+
     private VibrationEffectSegment expectedPrebaked(int effectId) {
         return new PrebakedSegment(effectId, false, VibrationEffect.EFFECT_STRENGTH_MEDIUM);
     }
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 1ae219d..46b4f87 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -246,6 +246,8 @@
     private PackageManager mPackageManagerClient;
     @Mock
     private WindowManagerInternal mWindowManagerInternal;
+    @Mock
+    private PermissionHelper mPermissionHelper;
     private TestableContext mContext = spy(getContext());
     private final String PKG = mContext.getPackageName();
     private TestableLooper mTestableLooper;
@@ -334,83 +336,6 @@
 
     private NotificationManagerService.WorkerHandler mWorkerHandler;
 
-    // Use a Testable subclass so we can simulate calls from the system without failing.
-    private static class TestableNotificationManagerService extends NotificationManagerService {
-        int countSystemChecks = 0;
-        boolean isSystemUid = true;
-        int countLogSmartSuggestionsVisible = 0;
-        @Nullable
-        NotificationAssistantAccessGrantedCallback mNotificationAssistantAccessGrantedCallback;
-
-        TestableNotificationManagerService(Context context, NotificationRecordLogger logger,
-                InstanceIdSequence notificationInstanceIdSequence) {
-            super(context, logger, notificationInstanceIdSequence);
-        }
-
-        RankingHelper getRankingHelper() {
-            return mRankingHelper;
-        }
-
-        @Override
-        protected boolean isCallingUidSystem() {
-            countSystemChecks++;
-            return isSystemUid;
-        }
-
-        @Override
-        protected boolean isCallerSystemOrPhone() {
-            countSystemChecks++;
-            return isSystemUid;
-        }
-
-        @Override
-        protected ICompanionDeviceManager getCompanionManager() {
-            return null;
-        }
-
-        @Override
-        protected void reportUserInteraction(NotificationRecord r) {
-            return;
-        }
-
-        @Override
-        protected void handleSavePolicyFile() {
-            return;
-        }
-
-        @Override
-        void logSmartSuggestionsVisible(NotificationRecord r, int notificationLocation) {
-            super.logSmartSuggestionsVisible(r, notificationLocation);
-            countLogSmartSuggestionsVisible++;
-        }
-
-        @Override
-        protected void setNotificationAssistantAccessGrantedForUserInternal(
-                ComponentName assistant, int userId, boolean granted, boolean userSet) {
-            if (mNotificationAssistantAccessGrantedCallback != null) {
-                mNotificationAssistantAccessGrantedCallback.onGranted(assistant, userId, granted,
-                        userSet);
-                return;
-            }
-            super.setNotificationAssistantAccessGrantedForUserInternal(assistant, userId, granted,
-                    userSet);
-        }
-
-        @Override
-        protected String[] getStringArrayResource(int key) {
-            return new String[] {PKG_O};
-        }
-
-        private void setNotificationAssistantAccessGrantedCallback(
-                @Nullable NotificationAssistantAccessGrantedCallback callback) {
-            this.mNotificationAssistantAccessGrantedCallback = callback;
-        }
-
-        interface NotificationAssistantAccessGrantedCallback {
-            void onGranted(ComponentName assistant, int userId, boolean granted, boolean userSet);
-        }
-    }
-
     private class TestableToastCallback extends ITransientNotification.Stub {
         @Override
         public void show(IBinder windowToken) {
@@ -516,13 +441,16 @@
 
         when(mAssistants.isAdjustmentAllowed(anyString())).thenReturn(true);
 
+        // apps allowed as convos
+        mService.setStringArrayResourceValue(PKG_O);
+
         mWorkerHandler = spy(mService.new WorkerHandler(mTestableLooper.getLooper()));
         mService.init(mWorkerHandler, mRankingHandler, mPackageManager, mPackageManagerClient,
                 mockLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr,
                 mSnoozeHelper, mUsageStats, mPolicyFile, mActivityManager, mGroupHelper, mAm, mAtm,
                 mAppUsageStats, mock(DevicePolicyManagerInternal.class), mUgm, mUgmInternal,
                 mAppOpsManager, mUm, mHistoryManager, mStatsManager, mock(TelephonyManager.class),
-                mAmi, mToastRateLimiter);
+                mAmi, mToastRateLimiter, mPermissionHelper);
         // Return first true for RoleObserver main-thread check
         when(mMainLooper.isCurrentThread()).thenReturn(true).thenReturn(false);
         mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY, mMainLooper);
@@ -6685,6 +6613,8 @@
         enableInteractAcrossUsers();
         mBinderService.areNotificationsEnabledForPackage(mContext.getPackageName(),
                 mUid + UserHandle.PER_USER_RANGE);
+
+        verify(mPermissionHelper, never()).hasPermission(anyInt());
     }
 
     @Test
@@ -8335,4 +8265,9 @@
         assertTrue(captor.getValue().isPackageAllowed(new VersionedPackage("apples", 1001)));
         assertFalse(captor.getValue().isPackageAllowed(new VersionedPackage("test", 1002)));
     }
+
+    @Test
+    public void testMigrationDisabledByDefault() {
+        assertThat(mService.mEnableAppSettingMigration).isFalse();
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationPermissionMigrationTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationPermissionMigrationTest.java
new file mode 100755
index 0000000..01764ce
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationPermissionMigrationTest.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
+import static android.app.NotificationManager.IMPORTANCE_NONE;
+import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.os.UserHandle.USER_SYSTEM;
+
+import static com.android.server.notification.NotificationManagerService.ACTION_DISABLE_NAS;
+import static com.android.server.notification.NotificationManagerService.ACTION_ENABLE_NAS;
+import static com.android.server.notification.NotificationManagerService.ACTION_LEARNMORE_NAS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.fail;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.AlarmManager;
+import android.app.AppOpsManager;
+import android.app.IActivityManager;
+import android.app.INotificationManager;
+import android.app.IUriGrantsManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.StatsManager;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.app.usage.UsageStatsManagerInternal;
+import android.companion.ICompanionDeviceManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutServiceInternal;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.service.notification.NotificationListenerFilter;
+import android.service.notification.StatusBarNotification;
+import android.telephony.TelephonyManager;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableContext;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+import android.testing.TestablePermissions;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.logging.InstanceIdSequence;
+import com.android.internal.logging.InstanceIdSequenceFake;
+import com.android.server.DeviceIdleInternal;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.UiServiceTestCase;
+import com.android.server.lights.LightsManager;
+import com.android.server.lights.LogicalLight;
+import com.android.server.notification.NotificationManagerService.NotificationAssistants;
+import com.android.server.notification.NotificationManagerService.NotificationListeners;
+import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.uri.UriGrantsManagerInternal;
+import com.android.server.utils.quota.MultiRateLimiter;
+import com.android.server.wm.ActivityTaskManagerInternal;
+import com.android.server.wm.WindowManagerInternal;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+/**
+ * Tests that NMS reads/writes the app notification state from Package/PermissionManager when
+ * migration is enabled. Because the migration field is read onStart
+ * TODO (b/194833441): migrate these tests to NotificationManagerServiceTest when the migration is
+ * permanently enabled.
+ */
+public class NotificationPermissionMigrationTest extends UiServiceTestCase {
+    private static final String TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId";
+    private static final int UID_HEADLESS = 1000000;
+
+    private final int mUid = Binder.getCallingUid();
+    private TestableNotificationManagerService mService;
+    private INotificationManager mBinderService;
+    private NotificationManagerInternal mInternalService;
+    private ShortcutHelper mShortcutHelper;
+    @Mock
+    private IPackageManager mPackageManager;
+    @Mock
+    private PackageManager mPackageManagerClient;
+    @Mock
+    private WindowManagerInternal mWindowManagerInternal;
+    @Mock
+    private PermissionHelper mPermissionHelper;
+    private TestableContext mContext = spy(getContext());
+    private final String PKG = mContext.getPackageName();
+    private TestableLooper mTestableLooper;
+    @Mock
+    private RankingHelper mRankingHelper;
+    @Mock private PreferencesHelper mPreferencesHelper;
+    AtomicFile mPolicyFile;
+    File mFile;
+    @Mock
+    private NotificationUsageStats mUsageStats;
+    @Mock
+    private UsageStatsManagerInternal mAppUsageStats;
+    @Mock
+    private AudioManager mAudioManager;
+    @Mock
+    private LauncherApps mLauncherApps;
+    @Mock
+    private ShortcutServiceInternal mShortcutServiceInternal;
+    @Mock
+    private UserManager mUserManager;
+    @Mock
+    ActivityManager mActivityManager;
+    @Mock
+    Resources mResources;
+    @Mock
+    RankingHandler mRankingHandler;
+    @Mock
+    ActivityManagerInternal mAmi;
+    @Mock
+    private Looper mMainLooper;
+
+    @Mock
+    IIntentSender pi1;
+
+    private static final int MAX_POST_DELAY = 1000;
+
+    private NotificationChannel mTestNotificationChannel = new NotificationChannel(
+            TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT);
+
+    private static final String VALID_CONVO_SHORTCUT_ID = "shortcut";
+
+    @Mock
+    private NotificationListeners mListeners;
+    @Mock
+    private NotificationListenerFilter mNlf;
+    @Mock private NotificationAssistants mAssistants;
+    @Mock private ConditionProviders mConditionProviders;
+    private ManagedServices.ManagedServiceInfo mListener;
+    @Mock private ICompanionDeviceManager mCompanionMgr;
+    @Mock SnoozeHelper mSnoozeHelper;
+    @Mock GroupHelper mGroupHelper;
+    @Mock
+    IBinder mPermOwner;
+    @Mock
+    IActivityManager mAm;
+    @Mock
+    ActivityTaskManagerInternal mAtm;
+    @Mock
+    IUriGrantsManager mUgm;
+    @Mock
+    UriGrantsManagerInternal mUgmInternal;
+    @Mock
+    AppOpsManager mAppOpsManager;
+    @Mock
+    private TestableNotificationManagerService.NotificationAssistantAccessGrantedCallback
+            mNotificationAssistantAccessGrantedCallback;
+    @Mock
+    UserManager mUm;
+    @Mock
+    NotificationHistoryManager mHistoryManager;
+    @Mock
+    StatsManager mStatsManager;
+    @Mock
+    AlarmManager mAlarmManager;
+    @Mock
+    MultiRateLimiter mToastRateLimiter;
+    BroadcastReceiver mPackageIntentReceiver;
+    BroadcastReceiver mNASIntentReceiver;
+    NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
+    private InstanceIdSequence mNotificationInstanceIdSequence = new InstanceIdSequenceFake(
+            1 << 30);
+    @Mock
+    StatusBarManagerInternal mStatusBar;
+
+    private NotificationManagerService.WorkerHandler mWorkerHandler;
+
+    @Before
+    public void setUp() throws Exception {
+        // These should be the only difference in setup from NMSTest
+        Settings.Secure.putIntForUser(
+                getContext().getContentResolver(),
+                Settings.Secure.NOTIFICATION_PERMISSION_ENABLED, 1, USER_SYSTEM);
+        Settings.Global.putInt(getContext().getContentResolver(),
+                Settings.Global.SHOW_NOTIFICATION_CHANNEL_WARNINGS, 1);
+
+        // Shell permissions will override permissions of our app, so add all necessary permissions
+        // for this test here:
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                "android.permission.WRITE_DEVICE_CONFIG",
+                "android.permission.READ_DEVICE_CONFIG",
+                "android.permission.READ_CONTACTS");
+
+        MockitoAnnotations.initMocks(this);
+
+        DeviceIdleInternal deviceIdleInternal = mock(DeviceIdleInternal.class);
+        when(deviceIdleInternal.getNotificationAllowlistDuration()).thenReturn(3000L);
+
+        LocalServices.removeServiceForTest(UriGrantsManagerInternal.class);
+        LocalServices.addService(UriGrantsManagerInternal.class, mUgmInternal);
+        LocalServices.removeServiceForTest(WindowManagerInternal.class);
+        LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal);
+        LocalServices.removeServiceForTest(StatusBarManagerInternal.class);
+        LocalServices.addService(StatusBarManagerInternal.class, mStatusBar);
+        LocalServices.removeServiceForTest(DeviceIdleInternal.class);
+        LocalServices.addService(DeviceIdleInternal.class, deviceIdleInternal);
+        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+        LocalServices.addService(ActivityManagerInternal.class, mAmi);
+        mContext.addMockSystemService(Context.ALARM_SERVICE, mAlarmManager);
+
+        doNothing().when(mContext).sendBroadcastAsUser(any(), any(), any());
+
+        mService = new TestableNotificationManagerService(mContext, mNotificationRecordLogger,
+                mNotificationInstanceIdSequence);
+
+        // Use this testable looper.
+        mTestableLooper = TestableLooper.get(this);
+        // MockPackageManager - default returns ApplicationInfo with matching calling UID
+        mContext.setMockPackageManager(mPackageManagerClient);
+
+        when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt()))
+                .thenAnswer((Answer<ApplicationInfo>) invocation -> {
+                    Object[] args = invocation.getArguments();
+                    return getApplicationInfo((String) args[0], mUid);
+                });
+        when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenAnswer((Answer<ApplicationInfo>) invocation -> {
+                    Object[] args = invocation.getArguments();
+                    return getApplicationInfo((String) args[0], mUid);
+                });
+        when(mPackageManagerClient.getPackageUidAsUser(any(), anyInt())).thenReturn(mUid);
+        final LightsManager mockLightsManager = mock(LightsManager.class);
+        when(mockLightsManager.getLight(anyInt())).thenReturn(mock(LogicalLight.class));
+        when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
+        when(mPackageManagerClient.hasSystemFeature(FEATURE_WATCH)).thenReturn(false);
+        when(mUgmInternal.newUriPermissionOwner(anyString())).thenReturn(mPermOwner);
+        when(mPackageManager.getPackagesForUid(mUid)).thenReturn(new String[]{PKG});
+        when(mPackageManagerClient.getPackagesForUid(anyInt())).thenReturn(new String[]{PKG});
+        mContext.addMockSystemService(AppOpsManager.class, mock(AppOpsManager.class));
+
+        // write to a test file; the system file isn't readable from tests
+        mFile = new File(mContext.getCacheDir(), "test.xml");
+        mFile.createNewFile();
+        final String preupgradeXml = "<notification-policy></notification-policy>";
+        mPolicyFile = new AtomicFile(mFile);
+        FileOutputStream fos = mPolicyFile.startWrite();
+        fos.write(preupgradeXml.getBytes());
+        mPolicyFile.finishWrite(fos);
+
+        // Setup managed services
+        when(mNlf.isTypeAllowed(anyInt())).thenReturn(true);
+        when(mNlf.isPackageAllowed(any())).thenReturn(true);
+        when(mNlf.isPackageAllowed(null)).thenReturn(true);
+        when(mListeners.getNotificationListenerFilter(any())).thenReturn(mNlf);
+        mListener = mListeners.new ManagedServiceInfo(
+                null, new ComponentName(PKG, "test_class"),
+                UserHandle.getUserId(mUid), true, null, 0, 123);
+        ComponentName defaultComponent = ComponentName.unflattenFromString("config/device");
+        ArraySet<ComponentName> components = new ArraySet<>();
+        components.add(defaultComponent);
+        when(mListeners.getDefaultComponents()).thenReturn(components);
+        when(mConditionProviders.getDefaultPackages())
+                .thenReturn(new ArraySet<>(Arrays.asList("config")));
+        when(mAssistants.getDefaultComponents()).thenReturn(components);
+        when(mAssistants.queryPackageForServices(
+                anyString(), anyInt(), anyInt())).thenReturn(components);
+        when(mListeners.checkServiceTokenLocked(null)).thenReturn(mListener);
+        ManagedServices.Config listenerConfig = new ManagedServices.Config();
+        listenerConfig.xmlTag = NotificationListeners.TAG_ENABLED_NOTIFICATION_LISTENERS;
+        when(mListeners.getConfig()).thenReturn(listenerConfig);
+        ManagedServices.Config assistantConfig = new ManagedServices.Config();
+        assistantConfig.xmlTag = NotificationAssistants.TAG_ENABLED_NOTIFICATION_ASSISTANTS;
+        when(mAssistants.getConfig()).thenReturn(assistantConfig);
+        ManagedServices.Config dndConfig = new ManagedServices.Config();
+        dndConfig.xmlTag = ConditionProviders.TAG_ENABLED_DND_APPS;
+        when(mConditionProviders.getConfig()).thenReturn(dndConfig);
+
+        when(mAssistants.isAdjustmentAllowed(anyString())).thenReturn(true);
+
+        // apps allowed as convos
+        mService.setStringArrayResourceValue(PKG_O);
+
+        mWorkerHandler = spy(mService.new WorkerHandler(mTestableLooper.getLooper()));
+        mService.init(mWorkerHandler, mRankingHandler, mPackageManager, mPackageManagerClient,
+                mockLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr,
+                mSnoozeHelper, mUsageStats, mPolicyFile, mActivityManager, mGroupHelper, mAm, mAtm,
+                mAppUsageStats, mock(DevicePolicyManagerInternal.class), mUgm, mUgmInternal,
+                mAppOpsManager, mUm, mHistoryManager, mStatsManager, mock(TelephonyManager.class),
+                mAmi, mToastRateLimiter, mPermissionHelper);
+        // Return first true for RoleObserver main-thread check
+        when(mMainLooper.isCurrentThread()).thenReturn(true).thenReturn(false);
+        mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY, mMainLooper);
+
+        mService.setAudioManager(mAudioManager);
+
+        mShortcutHelper = mService.getShortcutHelper();
+        mShortcutHelper.setLauncherApps(mLauncherApps);
+        mShortcutHelper.setShortcutServiceInternal(mShortcutServiceInternal);
+        mShortcutHelper.setUserManager(mUserManager);
+
+        // Capture PackageIntentReceiver
+        ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        ArgumentCaptor<IntentFilter> intentFilterCaptor =
+                ArgumentCaptor.forClass(IntentFilter.class);
+
+        verify(mContext, atLeastOnce()).registerReceiverAsUser(broadcastReceiverCaptor.capture(),
+                any(), intentFilterCaptor.capture(), any(), any());
+        verify(mContext, atLeastOnce()).registerReceiver(broadcastReceiverCaptor.capture(),
+                intentFilterCaptor.capture());
+        List<BroadcastReceiver> broadcastReceivers = broadcastReceiverCaptor.getAllValues();
+        List<IntentFilter> intentFilters = intentFilterCaptor.getAllValues();
+
+        for (int i = 0; i < intentFilters.size(); i++) {
+            final IntentFilter filter = intentFilters.get(i);
+            if (filter.hasAction(Intent.ACTION_DISTRACTING_PACKAGES_CHANGED)
+                    && filter.hasAction(Intent.ACTION_PACKAGES_UNSUSPENDED)
+                    && filter.hasAction(Intent.ACTION_PACKAGES_SUSPENDED)) {
+                mPackageIntentReceiver = broadcastReceivers.get(i);
+            } else if (filter.hasAction(ACTION_ENABLE_NAS)
+                    && filter.hasAction(ACTION_DISABLE_NAS)
+                    && filter.hasAction(ACTION_LEARNMORE_NAS)) {
+                mNASIntentReceiver = broadcastReceivers.get(i);
+            }
+        }
+        assertNotNull("package intent receiver should exist", mPackageIntentReceiver);
+        assertNotNull("nas intent receiver should exist", mNASIntentReceiver);
+
+        // Pretend the shortcut exists
+        List<ShortcutInfo> shortcutInfos = new ArrayList<>();
+        ShortcutInfo info = mock(ShortcutInfo.class);
+        when(info.getPackage()).thenReturn(PKG);
+        when(info.getId()).thenReturn(VALID_CONVO_SHORTCUT_ID);
+        when(info.getUserId()).thenReturn(USER_SYSTEM);
+        when(info.isLongLived()).thenReturn(true);
+        when(info.isEnabled()).thenReturn(true);
+        shortcutInfos.add(info);
+        when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcutInfos);
+        when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any())).thenReturn(true);
+        when(mUserManager.isUserUnlocked(any(UserHandle.class))).thenReturn(true);
+
+        // Set the testable bubble extractor
+        RankingHelper rankingHelper = mService.getRankingHelper();
+        BubbleExtractor extractor = rankingHelper.findExtractor(BubbleExtractor.class);
+        extractor.setActivityManager(mActivityManager);
+
+        // Tests call directly into the Binder.
+        mBinderService = mService.getBinderService();
+        mInternalService = mService.getInternalService();
+
+        mBinderService.createNotificationChannels(
+                PKG, new ParceledListSlice(Arrays.asList(mTestNotificationChannel)));
+        mBinderService.createNotificationChannels(
+                PKG_P, new ParceledListSlice(Arrays.asList(mTestNotificationChannel)));
+        mBinderService.createNotificationChannels(
+                PKG_O, new ParceledListSlice(Arrays.asList(mTestNotificationChannel)));
+        assertNotNull(mBinderService.getNotificationChannel(
+                PKG, mContext.getUserId(), PKG, TEST_CHANNEL_ID));
+        clearInvocations(mRankingHandler);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mFile != null) mFile.delete();
+
+        try {
+            mService.onDestroy();
+        } catch (IllegalStateException | IllegalArgumentException e) {
+            // can throw if a broadcast receiver was never registered
+        }
+
+        InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation().dropShellPermissionIdentity();
+        // Remove scheduled messages that would be processed when the test is already done, and
+        // could cause issues, for example, messages that remove/cancel shown toasts (this causes
+        // problematic interactions with mocks when they're no longer working as expected).
+        mWorkerHandler.removeCallbacksAndMessages(null);
+    }
+
+    private ApplicationInfo getApplicationInfo(String pkg, int uid) {
+        final ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.uid = uid;
+        switch (pkg) {
+            case PKG_N_MR1:
+                applicationInfo.targetSdkVersion = Build.VERSION_CODES.N_MR1;
+                break;
+            case PKG_O:
+                applicationInfo.targetSdkVersion = Build.VERSION_CODES.O;
+                break;
+            case PKG_P:
+                applicationInfo.targetSdkVersion = Build.VERSION_CODES.P;
+                break;
+            default:
+                applicationInfo.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
+                break;
+        }
+        return applicationInfo;
+    }
+
+    public void waitForIdle() {
+        mTestableLooper.processAllMessages();
+    }
+
+    private NotificationRecord generateNotificationRecord(NotificationChannel channel) {
+        return generateNotificationRecord(channel, null);
+    }
+
+    private NotificationRecord generateNotificationRecord(NotificationChannel channel,
+            Notification.TvExtender extender) {
+        if (channel == null) {
+            channel = mTestNotificationChannel;
+        }
+        Notification.Builder nb = new Notification.Builder(mContext, channel.getId())
+                .setContentTitle("foo")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .addAction(new Notification.Action.Builder(null, "test", null).build());
+        if (extender != null) {
+            nb.extend(extender);
+        }
+        StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 8, "tag", mUid, 0,
+                nb.build(), UserHandle.getUserHandleForUid(mUid), null, 0);
+        return new NotificationRecord(mContext, sbn, channel);
+    }
+
+    private void enableInteractAcrossUsers() {
+        TestablePermissions perms = mContext.getTestablePermissions();
+        perms.setPermission(android.Manifest.permission.INTERACT_ACROSS_USERS, PERMISSION_GRANTED);
+    }
+
+    @Test
+    public void testAreNotificationsEnabledForPackage() throws Exception {
+        mBinderService.areNotificationsEnabledForPackage(mContext.getPackageName(),
+                mUid);
+
+        verify(mPermissionHelper).hasPermission(mUid);
+    }
+
+    @Test
+    public void testAreNotificationsEnabledForPackage_crossUser() throws Exception {
+        try {
+            mBinderService.areNotificationsEnabledForPackage(mContext.getPackageName(),
+                    mUid + UserHandle.PER_USER_RANGE);
+            fail("Cannot call cross user without permission");
+        } catch (SecurityException e) {
+            // pass
+        }
+        verify(mPermissionHelper, never()).hasPermission(anyInt());
+
+        // cross user, with permission, no problem
+        enableInteractAcrossUsers();
+        mBinderService.areNotificationsEnabledForPackage(mContext.getPackageName(),
+                mUid + UserHandle.PER_USER_RANGE);
+
+        verify(mPermissionHelper).hasPermission(mUid + UserHandle.PER_USER_RANGE);
+    }
+
+    @Test
+    public void testGetPackageImportance() throws Exception {
+        when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
+        assertThat(mBinderService.getPackageImportance(mContext.getPackageName()))
+                .isEqualTo(IMPORTANCE_DEFAULT);
+
+        when(mPermissionHelper.hasPermission(mUid)).thenReturn(false);
+        assertThat(mBinderService.getPackageImportance(mContext.getPackageName()))
+                .isEqualTo(IMPORTANCE_NONE);
+    }
+
+    @Test
+    public void testEnqueueNotificationInternal_noChannel() throws Exception {
+        when(mPermissionHelper.hasPermission(mUid)).thenReturn(false);
+        NotificationRecord nr = generateNotificationRecord(
+                new NotificationChannel("did not create", "", IMPORTANCE_DEFAULT));
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
+                nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+        waitForIdle();
+
+        verify(mPermissionHelper).hasPermission(mUid);
+        verify(mPermissionHelper, never()).hasPermission(Process.SYSTEM_UID);
+
+        reset(mPermissionHelper);
+        when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
+
+        mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(),
+                nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId());
+        waitForIdle();
+
+        verify(mPermissionHelper).hasPermission(mUid);
+        assertThat(mService.mChannelToastsSent).contains(mUid);
+    }
+
+    @Test
+    public void testSetNotificationsEnabledForPackage_noChange() throws Exception {
+        when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
+        mBinderService.setNotificationsEnabledForPackage(mContext.getPackageName(), mUid, true);
+
+        verify(mPermissionHelper, never()).setNotificationPermission(
+                anyString(), anyInt(), anyBoolean(), anyBoolean());
+    }
+
+    @Test
+    public void testSetNotificationsEnabledForPackage() throws Exception {
+        when(mPermissionHelper.hasPermission(mUid)).thenReturn(true);
+        mBinderService.setNotificationsEnabledForPackage(mContext.getPackageName(), mUid, false);
+
+        verify(mPermissionHelper, never()).setNotificationPermission(
+                mContext.getPackageName(), UserHandle.getUserId(mUid), false, true);
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java
new file mode 100644
index 0000000..f72d39a
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PermissionHelperTest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.notification;
+
+import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET;
+import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static android.permission.PermissionManager.PERMISSION_GRANTED;
+import static android.permission.PermissionManager.PERMISSION_SOFT_DENIED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.fail;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.ParceledListSlice;
+import android.permission.IPermissionManager;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.UiServiceTestCase;
+import com.android.server.pm.permission.PermissionManagerServiceInternal;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PermissionHelperTest extends UiServiceTestCase {
+
+    @Mock
+    private PermissionManagerServiceInternal mPmi;
+    @Mock
+    private IPackageManager mPackageManager;
+    @Mock
+    private IPermissionManager mPermManager;
+
+    private PermissionHelper mPermissionHelper;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mPermissionHelper = new PermissionHelper(mPmi, mPackageManager, mPermManager, true);
+    }
+
+    // TODO (b/194833441): Remove when the migration is enabled
+    @Test
+    public void testMethodsThrowIfMigrationDisabled() throws IllegalAccessException,
+            InvocationTargetException {
+        PermissionHelper permHelper =
+                new PermissionHelper(mPmi, mPackageManager, mPermManager, false);
+
+        Method[] allMethods = PermissionHelper.class.getDeclaredMethods();
+        for (Method method : allMethods) {
+            if (Modifier.isPublic(method.getModifiers())) {
+                Parameter[] params = method.getParameters();
+                List<Object> args = Lists.newArrayListWithCapacity(params.length);
+                for (int i = 0; i < params.length; i++) {
+                    Type type = params[i].getParameterizedType();
+                    if (type.getTypeName().equals("java.lang.String")) {
+                        args.add("");
+                    } else if (type.getTypeName().equals("boolean")){
+                        args.add(false);
+                    } else if (type.getTypeName().equals("int")) {
+                        args.add(1);
+                    }
+                }
+                try {
+                    method.invoke(permHelper, args.toArray());
+                    fail("Method should have thrown because migration flag is disabled");
+                } catch (InvocationTargetException e) {
+                    if (!(e.getTargetException() instanceof IllegalStateException)) {
+                        throw e;
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testHasPermission() throws Exception {
+        when(mPmi.checkUidPermission(anyInt(), eq(Manifest.permission.POST_NOTIFICATIONS)))
+                .thenReturn(PERMISSION_GRANTED);
+
+        assertThat(mPermissionHelper.hasPermission(1)).isTrue();
+
+        when(mPmi.checkUidPermission(anyInt(), eq(Manifest.permission.POST_NOTIFICATIONS)))
+                .thenReturn(PERMISSION_SOFT_DENIED);
+
+        assertThat(mPermissionHelper.hasPermission(1)).isFalse();
+    }
+
+    @Test
+    public void testGetAppsRequestingPermission() throws Exception {
+        // App that does not request permission
+        PackageInfo notThis = new PackageInfo();
+        notThis.packageName = "wrong.permission";
+        notThis.requestedPermissions = new String[] {"something else"};
+        // App that does not request any permissions (null check
+        PackageInfo none = new PackageInfo();
+        none.packageName = "no.permissions";
+        // 2 apps that request the permission
+        PackageInfo first = new PackageInfo();
+        first.packageName = "first";
+        first.requestedPermissions =
+                new String[] {"something else", Manifest.permission.POST_NOTIFICATIONS};
+        ApplicationInfo aiFirst = new ApplicationInfo();
+        aiFirst.uid = 1;
+        first.applicationInfo = aiFirst;
+        PackageInfo second = new PackageInfo();
+        second.packageName = "second";
+        second.requestedPermissions = new String[] {Manifest.permission.POST_NOTIFICATIONS};
+        ApplicationInfo aiSecond = new ApplicationInfo();
+        aiSecond.uid = 2;
+        second.applicationInfo = aiSecond;
+
+        Map<Integer, String> expected = ImmutableMap.of(1, "first", 2, "second");
+
+        ParceledListSlice<PackageInfo> infos = new ParceledListSlice<>(
+                ImmutableList.of(notThis, none, first, second));
+        when(mPackageManager.getInstalledPackages(eq(GET_PERMISSIONS), anyInt())).thenReturn(infos);
+
+        Map<Integer, String> actual = mPermissionHelper.getAppsRequestingPermission(0);
+
+        assertThat(actual).containsExactlyEntriesIn(expected);
+    }
+
+    @Test
+    public void testGetAppsGrantedPermission_noApps() throws Exception {
+        int userId = 1;
+        ParceledListSlice<PackageInfo> infos = ParceledListSlice.emptyList();
+        when(mPackageManager.getPackagesHoldingPermissions(
+                eq(new String[] {Manifest.permission.POST_NOTIFICATIONS}), anyInt(), eq(userId)))
+                .thenReturn(infos);
+        assertThat(mPermissionHelper.getAppsGrantedPermission(userId)).isNotNull();
+    }
+
+    @Test
+    public void testGetAppsGrantedPermission() throws Exception {
+        int userId = 1;
+        PackageInfo first = new PackageInfo();
+        first.packageName = "first";
+        first.requestedPermissions =
+                new String[] {"something else", Manifest.permission.POST_NOTIFICATIONS};
+        ApplicationInfo aiFirst = new ApplicationInfo();
+        aiFirst.uid = 1;
+        first.applicationInfo = aiFirst;
+        PackageInfo second = new PackageInfo();
+        second.packageName = "second";
+        second.requestedPermissions = new String[] {Manifest.permission.POST_NOTIFICATIONS};
+        ApplicationInfo aiSecond = new ApplicationInfo();
+        aiSecond.uid = 2;
+        second.applicationInfo = aiSecond;
+
+        ParceledListSlice<PackageInfo> infos = new ParceledListSlice<>(
+                ImmutableList.of(first, second));
+        when(mPackageManager.getPackagesHoldingPermissions(
+                eq(new String[] {Manifest.permission.POST_NOTIFICATIONS}), anyInt(), eq(userId)))
+                .thenReturn(infos);
+
+        Map<Integer, String> expected = ImmutableMap.of(1, "first", 2, "second");
+
+        assertThat(mPermissionHelper.getAppsGrantedPermission(userId))
+                .containsExactlyEntriesIn(expected);
+    }
+
+    @Test
+    public void testSetNotificationPermission_grantUserSet() throws Exception {
+        mPermissionHelper.setNotificationPermission("pkg", 10, true, true);
+
+        verify(mPermManager).grantRuntimePermission(
+                "pkg", Manifest.permission.POST_NOTIFICATIONS, 10);
+        verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
+                FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, 10);
+    }
+
+    @Test
+    public void testSetNotificationPermission_revokeUserSet() throws Exception {
+        mPermissionHelper.setNotificationPermission("pkg", 10, false, true);
+
+        verify(mPermManager).revokeRuntimePermission(
+                eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10), anyString());
+        verify(mPermManager).updatePermissionFlags("pkg", Manifest.permission.POST_NOTIFICATIONS,
+                FLAG_PERMISSION_USER_SET, FLAG_PERMISSION_USER_SET, true, 10);
+    }
+
+    @Test
+    public void testSetNotificationPermission_grantNotUserSet() throws Exception {
+        mPermissionHelper.setNotificationPermission("pkg", 10, true, false);
+
+        verify(mPermManager).grantRuntimePermission(
+                "pkg", Manifest.permission.POST_NOTIFICATIONS, 10);
+        verify(mPermManager, never()).updatePermissionFlags(
+                anyString(), anyString(), anyInt(), anyInt(), anyBoolean(), anyInt());
+    }
+
+    @Test
+    public void testSetNotificationPermission_revokeNotUserSet() throws Exception {
+        mPermissionHelper.setNotificationPermission("pkg", 10, false, false);
+
+        verify(mPermManager).revokeRuntimePermission(
+                eq("pkg"), eq(Manifest.permission.POST_NOTIFICATIONS), eq(10), anyString());
+        verify(mPermManager, never()).updatePermissionFlags(
+                anyString(), anyString(), anyInt(), anyInt(), anyBoolean(), anyInt());
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
index 27ae46c..94858d8 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RoleObserverTest.java
@@ -87,6 +87,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
+// TODO (b/194833441): remove when notification permission is enabled
 public class RoleObserverTest extends UiServiceTestCase {
     private TestableNotificationManagerService mService;
     private NotificationManagerService.RoleObserver mRoleObserver;
@@ -162,7 +163,7 @@
                     mock(AppOpsManager.class), mUm, mock(NotificationHistoryManager.class),
                     mock(StatsManager.class), mock(TelephonyManager.class),
                     mock(ActivityManagerInternal.class),
-                    mock(MultiRateLimiter.class));
+                    mock(MultiRateLimiter.class), mock(PermissionHelper.class));
         } catch (SecurityException e) {
             if (!e.getMessage().contains("Permission Denial: not allowed to send broadcast")) {
                 throw e;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
new file mode 100644
index 0000000..bde0485
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import android.companion.ICompanionDeviceManager;
+import android.content.ComponentName;
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.logging.InstanceIdSequence;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class TestableNotificationManagerService extends NotificationManagerService {
+    int countSystemChecks = 0;
+    boolean isSystemUid = true;
+    int countLogSmartSuggestionsVisible = 0;
+    Set<Integer> mChannelToastsSent = new HashSet<>();
+
+    String stringArrayResourceValue;
+    @Nullable
+    NotificationAssistantAccessGrantedCallback mNotificationAssistantAccessGrantedCallback;
+
+    TestableNotificationManagerService(Context context, NotificationRecordLogger logger,
+            InstanceIdSequence notificationInstanceIdSequence) {
+        super(context, logger, notificationInstanceIdSequence);
+    }
+
+    RankingHelper getRankingHelper() {
+        return mRankingHelper;
+    }
+
+    @Override
+    protected boolean isCallingUidSystem() {
+        countSystemChecks++;
+        return isSystemUid;
+    }
+
+    @Override
+    protected boolean isCallerSystemOrPhone() {
+        countSystemChecks++;
+        return isSystemUid;
+    }
+
+    @Override
+    protected ICompanionDeviceManager getCompanionManager() {
+        return null;
+    }
+
+    @Override
+    protected void reportUserInteraction(NotificationRecord r) {
+        return;
+    }
+
+    @Override
+    protected void handleSavePolicyFile() {
+        return;
+    }
+
+    @Override
+    void logSmartSuggestionsVisible(NotificationRecord r, int notificationLocation) {
+        super.logSmartSuggestionsVisible(r, notificationLocation);
+        countLogSmartSuggestionsVisible++;
+    }
+
+    @Override
+    protected void setNotificationAssistantAccessGrantedForUserInternal(
+            ComponentName assistant, int userId, boolean granted, boolean userSet) {
+        if (mNotificationAssistantAccessGrantedCallback != null) {
+            mNotificationAssistantAccessGrantedCallback.onGranted(assistant, userId, granted,
+                    userSet);
+            return;
+        }
+        super.setNotificationAssistantAccessGrantedForUserInternal(assistant, userId, granted,
+                userSet);
+    }
+
+    @Override
+    protected String[] getStringArrayResource(int key) {
+        return new String[] {stringArrayResourceValue};
+    }
+
+    protected void setStringArrayResourceValue(String value) {
+        stringArrayResourceValue = value;
+    }
+
+    void setNotificationAssistantAccessGrantedCallback(
+            @Nullable NotificationAssistantAccessGrantedCallback callback) {
+        this.mNotificationAssistantAccessGrantedCallback = callback;
+    }
+
+    interface NotificationAssistantAccessGrantedCallback {
+        void onGranted(ComponentName assistant, int userId, boolean granted, boolean userSet);
+    }
+
+    @Override
+    protected void doChannelWarningToast(int uid, CharSequence toastText) {
+        mChannelToastsSent.add(uid);
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 65733d7..d7702bb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -508,7 +508,7 @@
         final ActivityConfigurationChangeItem expected =
                 ActivityConfigurationChangeItem.obtain(newConfig);
         verify(mAtm.getLifecycleManager()).scheduleTransaction(eq(activity.app.getThread()),
-                eq(activity.appToken), eq(expected));
+                eq(activity.token), eq(expected));
     }
 
     @Test
@@ -723,7 +723,7 @@
             final ActivityConfigurationChangeItem expected =
                     ActivityConfigurationChangeItem.obtain(newConfig);
             verify(mAtm.getLifecycleManager()).scheduleTransaction(
-                    eq(activity.app.getThread()), eq(activity.appToken), eq(expected));
+                    eq(activity.app.getThread()), eq(activity.token), eq(expected));
         } finally {
             stack.getDisplayArea().removeChild(stack);
         }
@@ -787,7 +787,7 @@
         final ActivityRecord activity = createActivityWithTask();
         assertTrue(activity.hasSavedState());
 
-        ActivityRecord.activityResumedLocked(activity.appToken, false /* handleSplashScreenExit */);
+        ActivityRecord.activityResumedLocked(activity.token, false /* handleSplashScreenExit */);
         assertFalse(activity.hasSavedState());
         assertNull(activity.getSavedState());
     }
@@ -1610,7 +1610,7 @@
             setup.accept(activity);
             activity.getTask().removeImmediately("test");
             try {
-                verify(mAtm.getLifecycleManager()).scheduleTransaction(any(), eq(activity.appToken),
+                verify(mAtm.getLifecycleManager()).scheduleTransaction(any(), eq(activity.token),
                         isA(DestroyActivityItem.class));
             } catch (RemoteException ignored) {
             }
@@ -2463,7 +2463,7 @@
         final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
         activity.addStartingWindow(mPackageName,
                 android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
-                false, false);
+                false, false, false);
         waitUntilHandlersIdle();
         assertHasStartingWindow(activity);
         activity.removeStartingWindow();
@@ -2476,7 +2476,7 @@
         activity.mTargetSdk = targetSdk;
         activity.addStartingWindow(mPackageName,
                 android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
-                false, false);
+                false, false, false);
         waitUntilHandlersIdle();
         assertHasStartingWindow(activity);
         assertEquals(activity.mStartingData.mTypeParams & TYPE_PARAMETER_LEGACY_SPLASH_SCREEN,
@@ -2514,11 +2514,11 @@
                 .setVisible(false).build();
         activity1.addStartingWindow(mPackageName,
                 android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
-                false, false);
+                false, false, false);
         waitUntilHandlersIdle();
         activity2.addStartingWindow(mPackageName,
                 android.R.style.Theme, null, "Test", 0, 0, 0, 0, activity1,
-                true, true, false, true, false, false);
+                true, true, false, true, false, false, false);
         waitUntilHandlersIdle();
         assertFalse(mDisplayContent.mSkipAppTransitionAnimation);
         assertNoStartingWindow(activity1);
@@ -2536,11 +2536,11 @@
                     activity2.addStartingWindow(mPackageName,
                             android.R.style.Theme, null, "Test", 0, 0, 0, 0,
                             activity1, true, true, false,
-                            true, false, false);
+                            true, false, false, false);
                 });
         activity1.addStartingWindow(mPackageName,
                 android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
-                false, false);
+                false, false, false);
         waitUntilHandlersIdle();
         assertNoStartingWindow(activity1);
         assertHasStartingWindow(activity2);
@@ -2553,11 +2553,11 @@
         final ActivityRecord activity2 = new ActivityBuilder(mAtm).setCreateTask(true).build();
         activity1.addStartingWindow(mPackageName,
                 android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
-                false, false);
+                false, false, false);
         waitUntilHandlersIdle();
         activity2.addStartingWindow(mPackageName,
                 android.R.style.Theme, null, "Test", 0, 0, 0, 0, activity1,
-                true, true, false, true, false, false);
+                true, true, false, true, false, false, false);
         waitUntilHandlersIdle();
         assertNoStartingWindow(activity1);
         assertHasStartingWindow(activity2);
@@ -2599,7 +2599,7 @@
                 "Test", 0 /* labelRes */, 0 /* icon */, 0 /* logo */, 0 /* windowFlags */,
                 null /* transferFrom */, true /* newTask */, true /* taskSwitch */,
                 false /* processRunning */, false /* allowTaskSnapshot */,
-                false /* activityCreate */, false /* suggestEmpty */);
+                false /* activityCreate */, false /* suggestEmpty */, false /* activityAllDrawn */);
         waitUntilHandlersIdle();
         assertHasStartingWindow(activity);
 
@@ -2647,7 +2647,7 @@
         task.positionChildAt(topActivity, POSITION_TOP);
         activity.addStartingWindow(mPackageName,
                 android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
-                false, false);
+                false, false, false);
         waitUntilHandlersIdle();
 
         // Make activities to have different rotation from it display and set fixed rotation
@@ -2664,7 +2664,7 @@
         // on activity2.
         topActivity.addStartingWindow(mPackageName,
                 android.R.style.Theme, null, "Test", 0, 0, 0, 0, activity,
-                false, false, false, true, false, false);
+                false, false, false, true, false, false, false);
         waitUntilHandlersIdle();
         assertTrue(topActivity.hasFixedRotationTransform());
     }
@@ -2680,7 +2680,7 @@
         // Add a starting window.
         activityTop.addStartingWindow(mPackageName,
                 android.R.style.Theme, null, "Test", 0, 0, 0, 0, null, true, true, false, true,
-                false, false);
+                false, false, false);
         waitUntilHandlersIdle();
 
         // Make the top one invisible, and try transferring the starting window from the top to the
@@ -2999,7 +2999,7 @@
         assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput);
 
         // Expect IME insets frozen state will reset when the activity has no IME focusable window.
-        app.mActivityRecord.forAllWindowsUnchecked(w -> {
+        app.mActivityRecord.forAllWindows(w -> {
             w.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
             return true;
         }, true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java
index a3ad09a..c103bc6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStartInterceptorTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
 
+import android.annotation.Nullable;
 import android.app.ActivityManagerInternal;
 import android.app.KeyguardManager;
 import android.app.admin.DevicePolicyManagerInternal;
@@ -42,6 +43,7 @@
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
 import android.testing.DexmakerShareClassLoaderRule;
+import android.util.SparseArray;
 
 import androidx.test.filters.SmallTest;
 
@@ -64,7 +66,7 @@
  * Unit tests for {@link ActivityStartInterceptorTest}.
  *
  * Build/Install/Run:
- *  atest WmTests:ActivityStartInterceptorTest
+ * atest WmTests:ActivityStartInterceptorTest
  */
 @SmallTest
 @Presubmit
@@ -114,6 +116,9 @@
     private ActivityStartInterceptor mInterceptor;
     private ActivityInfo mAInfo = new ActivityInfo();
 
+    private SparseArray<ActivityInterceptorCallback> mActivityInterceptorCallbacks =
+            new SparseArray<>();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -157,6 +162,9 @@
                 TEST_USER_ID, TEST_PACKAGE_NAME, LOCK_TASK_LAUNCH_MODE_DEFAULT))
                 .thenReturn(true);
 
+        // Mock the activity start callbacks
+        when(mService.getActivityInterceptorCallbacks()).thenReturn(mActivityInterceptorCallbacks);
+
         // Initialise activity info
         mAInfo.applicationInfo = new ApplicationInfo();
         mAInfo.packageName = mAInfo.applicationInfo.packageName = TEST_PACKAGE_NAME;
@@ -285,4 +293,38 @@
         // THEN calling intercept returns false
         assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
     }
+
+    public void addMockInterceptorCallback(@Nullable Intent intent) {
+        int size = mActivityInterceptorCallbacks.size();
+        mActivityInterceptorCallbacks.put(size, new ActivityInterceptorCallback() {
+            @Override
+            public Intent intercept(ActivityInterceptorInfo info) {
+                return intent;
+            }
+        });
+    }
+
+    @Test
+    public void testInterceptionCallback_singleCallback() {
+        addMockInterceptorCallback(new Intent("android.test.foo"));
+
+        assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
+        assertEquals("android.test.foo", mInterceptor.mIntent.getAction());
+    }
+
+    @Test
+    public void testInterceptionCallback_singleCallbackReturnsNull() {
+        addMockInterceptorCallback(null);
+
+        assertFalse(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
+    }
+
+    @Test
+    public void testInterceptionCallback_fallbackToSecondCallback() {
+        addMockInterceptorCallback(null);
+        addMockInterceptorCallback(new Intent("android.test.second"));
+
+        assertTrue(mInterceptor.intercept(null, null, mAInfo, null, null, 0, 0, null));
+        assertEquals("android.test.second", mInterceptor.mIntent.getAction());
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 3e8a2e9..0cab911 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -264,7 +264,7 @@
 
         final IBinder resultTo = containsConditions(preconditions, PRECONDITION_SOURCE_PRESENT)
                 || containsConditions(preconditions, PRECONDITION_SOURCE_VOICE_SESSION)
-                ? source.appToken : null;
+                ? source.token : null;
 
         final int requestCode = containsConditions(preconditions, PRECONDITION_REQUEST_CODE)
                 ? 1 : 0;
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
index 5de4fcb..4c3c318 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskManagerServiceTests.java
@@ -26,6 +26,8 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.server.wm.ActivityInterceptorCallback.FIRST_ORDERED_ID;
+import static com.android.server.wm.ActivityInterceptorCallback.LAST_ORDERED_ID;
 import static com.android.server.wm.ActivityRecord.State.PAUSED;
 import static com.android.server.wm.ActivityRecord.State.PAUSING;
 import static com.android.server.wm.ActivityRecord.State.RESUMED;
@@ -42,12 +44,14 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.when;
 
+import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.IApplicationThread;
 import android.app.PictureInPictureParams;
 import android.app.servertransaction.ClientTransaction;
 import android.app.servertransaction.EnterPipRequestedItem;
+import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.res.Configuration;
@@ -100,12 +104,12 @@
         final Task stack = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
         final ActivityRecord activity = stack.getBottomMostTask().getTopNonFinishingActivity();
         assertTrue("Activity must be finished", mAtm.mActivityClientController.finishActivity(
-                activity.appToken, 0 /* resultCode */, null /* resultData */,
+                activity.token, 0 /* resultCode */, null /* resultData */,
                 Activity.DONT_FINISH_TASK_WITH_ACTIVITY));
         assertTrue(activity.finishing);
 
         assertTrue("Duplicate activity finish request must also return 'true'",
-                mAtm.mActivityClientController.finishActivity(activity.appToken, 0 /* resultCode */,
+                mAtm.mActivityClientController.finishActivity(activity.token, 0 /* resultCode */,
                         null /* resultData */, Activity.DONT_FINISH_TASK_WITH_ACTIVITY));
     }
 
@@ -846,7 +850,64 @@
         return wpc;
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void testRegisterActivityStartInterceptor_IndexTooSmall() {
+        mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID - 1,
+                new ActivityInterceptorCallback() {
+                    @Nullable
+                    @Override
+                    public Intent intercept(ActivityInterceptorInfo info) {
+                        return null;
+                    }
+                });
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testRegisterActivityStartInterceptor_IndexTooLarge() {
+        mAtm.mInternal.registerActivityStartInterceptor(LAST_ORDERED_ID + 1,
+                new ActivityInterceptorCallback() {
+                    @Nullable
+                    @Override
+                    public Intent intercept(ActivityInterceptorInfo info) {
+                        return null;
+                    }
+                });
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testRegisterActivityStartInterceptor_DuplicateId() {
+        mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID,
+                new ActivityInterceptorCallback() {
+                    @Nullable
+                    @Override
+                    public Intent intercept(ActivityInterceptorInfo info) {
+                        return null;
+                    }
+                });
+        mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID,
+                new ActivityInterceptorCallback() {
+                    @Nullable
+                    @Override
+                    public Intent intercept(ActivityInterceptorInfo info) {
+                        return null;
+                    }
+                });
+    }
+
+    @Test
+    public void testRegisterActivityStartInterceptor() {
+        assertEquals(0, mAtm.getActivityInterceptorCallbacks().size());
+
+        mAtm.mInternal.registerActivityStartInterceptor(FIRST_ORDERED_ID,
+                new ActivityInterceptorCallback() {
+                    @Nullable
+                    @Override
+                    public Intent intercept(ActivityInterceptorInfo info) {
+                        return null;
+                    }
+                });
+
+        assertEquals(1, mAtm.getActivityInterceptorCallbacks().size());
+        assertTrue(mAtm.getActivityInterceptorCallbacks().contains(FIRST_ORDERED_ID));
+    }
 }
-
-
-
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index 82140f4..5d0e34a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -43,6 +43,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
@@ -893,6 +894,33 @@
     }
 
     @Test
+    public void testOverrideTaskFragmentAdapter_noOverrideWithWallpaper() {
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
+                new TestRemoteAnimationRunner(), 10, 1);
+        setupTaskFragmentRemoteAnimation(organizer, adapter);
+
+        // Create a TaskFragment with embedded activity.
+        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(
+                createTask(mDisplayContent), organizer);
+        final ActivityRecord activity = taskFragment.getTopMostActivity();
+        activity.allDrawn = true;
+        // Set wallpaper as visible.
+        final WallpaperWindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm,
+                mock(IBinder.class), true, mDisplayContent, true /* ownerCanManageAppTokens */);
+        spyOn(mDisplayContent.mWallpaperController);
+        doReturn(true).when(mDisplayContent.mWallpaperController).isWallpaperVisible();
+        spyOn(mDisplayContent.mAppTransition);
+
+        // Prepare a transition.
+        prepareAndTriggerAppTransition(activity, null /* closingActivity */, taskFragment);
+
+        // Should not be overridden when there is wallpaper in the transition.
+        verify(mDisplayContent.mAppTransition, never())
+                .overridePendingAppTransitionRemote(adapter, false /* sync */);
+    }
+
+    @Test
     public void testTransitionGoodToGoForTaskFragments() {
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final Task task = createTask(mDisplayContent);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
index fd562c3..ebefeaf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
@@ -75,13 +75,19 @@
 
     @Test
     public void clipAfterAnim_boundsLayerZBoosted() {
+        final Task task = mActivity.getTask();
+        final ActivityRecord topActivity = createActivityRecord(task);
+        task.assignChildLayers(mTransaction);
+
+        assertThat(topActivity.getLastLayer()).isGreaterThan(mActivity.getLastLayer());
+
         mActivity.mNeedsAnimationBoundsLayer = true;
         mActivity.mNeedsZBoost = true;
-
         mActivity.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
                 ANIMATION_TYPE_APP_TRANSITION);
+
         verify(mTransaction).setLayer(eq(mActivity.mAnimationBoundsLayer),
-                intThat(layer -> layer >= ActivityRecord.Z_BOOST_BASE));
+                intThat(layer -> layer > topActivity.getLastLayer()));
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java
index 1f123cc..d94e6c9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayAreaTest.java
@@ -24,7 +24,6 @@
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
 import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_FIRST;
-import static android.window.DisplayAreaOrganizer.FEATURE_VENDOR_LAST;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -76,6 +75,7 @@
 import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 /**
  * Tests for the {@link DisplayArea} container.
@@ -153,7 +153,7 @@
     public void testForAllTaskDisplayAreas_onlyTraversesDisplayAreaOfTypeAny() {
         final RootDisplayArea root =
                 new DisplayAreaPolicyBuilderTest.SurfacelessDisplayAreaRoot(mWm);
-        final Function<TaskDisplayArea, Boolean> callback0 = tda -> false;
+        final Predicate<TaskDisplayArea> callback0 = tda -> false;
         final Consumer<TaskDisplayArea> callback1 = tda -> { };
         final BiFunction<TaskDisplayArea, Integer, Integer> callback2 = (tda, result) -> result;
         final Function<TaskDisplayArea, TaskDisplayArea> callback3 = tda -> null;
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 24bbf46..663d45b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -73,7 +73,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.same;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP;
 import static com.android.server.wm.DisplayContent.IME_TARGET_INPUT;
@@ -175,25 +174,31 @@
         final WindowState exitingAppWindow = createWindow(null, TYPE_BASE_APPLICATION,
                 mDisplayContent, "exiting app");
         final ActivityRecord exitingApp = exitingAppWindow.mActivityRecord;
-        // Wait until everything in animation handler get executed to prevent the exiting window
-        // from being removed during WindowSurfacePlacer Traversal.
-        waitUntilHandlersIdle();
-
+        exitingApp.startAnimation(exitingApp.getPendingTransaction(), mock(AnimationAdapter.class),
+                false /* hidden */, SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION);
         exitingApp.mIsExiting = true;
-        exitingApp.getTask().getRootTask().mExitingActivities.add(exitingApp);
+        // If the activity is animating, its window should not be removed.
+        mDisplayContent.handleCompleteDeferredRemoval();
 
-        assertForAllWindowsOrder(Arrays.asList(
+        final ArrayList<WindowState> windows = new ArrayList<>(Arrays.asList(
                 mWallpaperWindow,
-                exitingAppWindow,
                 mChildAppWindowBelow,
                 mAppWindow,
                 mChildAppWindowAbove,
+                exitingAppWindow,
                 mDockedDividerWindow,
                 mImeWindow,
                 mImeDialogWindow,
                 mStatusBarWindow,
                 mNotificationShadeWindow,
                 mNavBarWindow));
+        assertForAllWindowsOrder(windows);
+
+        exitingApp.cancelAnimation();
+        // The exiting window will be removed because its parent is no longer animating.
+        mDisplayContent.handleCompleteDeferredRemoval();
+        windows.remove(exitingAppWindow);
+        assertForAllWindowsOrder(windows);
     }
 
     @UseTestDisplay(addAllCommonWindows = true)
@@ -871,18 +876,6 @@
     }
 
     @Test
-    public void testDisableDisplayInfoOverrideFromWindowManager() {
-        final DisplayContent dc = createNewDisplay();
-
-        assertTrue(dc.mShouldOverrideDisplayConfiguration);
-        mWm.dontOverrideDisplayInfo(dc.getDisplayId());
-
-        assertFalse(dc.mShouldOverrideDisplayConfiguration);
-        verify(mWm.mDisplayManagerInternal, times(1))
-                .setDisplayInfoOverrideFromWindowManager(dc.getDisplayId(), null);
-    }
-
-    @Test
     public void testGetPreferredOptionsPanelGravityFromDifferentDisplays() {
         final DisplayContent portraitDisplay = createNewDisplay();
         portraitDisplay.mInitialDisplayHeight = 2000;
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 6407c92..e04cff4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -149,7 +149,7 @@
         final Rect originalOverrideBounds = new Rect(mActivity.getBounds());
         resizeDisplay(mTask.mDisplayContent, 600, 1200);
         // The visible activity should recompute configuration according to the last parent bounds.
-        mAtm.mActivityClientController.restartActivityProcessIfVisible(mActivity.appToken);
+        mAtm.mActivityClientController.restartActivityProcessIfVisible(mActivity.token);
 
         assertEquals(RESTARTING_PROCESS, mActivity.getState());
         assertNotEquals(originalOverrideBounds, mActivity.getBounds());
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 d475c46e..cbdf4fe 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -411,7 +411,7 @@
                 .build();
         mAtm.mWindowOrganizerController.mLaunchTaskFragments
                 .put(mFragmentToken, mTaskFragment);
-        mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.appToken);
+        mTransaction.reparentActivityToTaskFragment(mFragmentToken, activity.token);
         clearInvocations(mAtm.mRootWindowContainer);
 
         mAtm.getWindowOrganizerController().applyTransaction(mTransaction);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
index e528a4a..168c250 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
@@ -1321,6 +1321,50 @@
     }
 
     @Test
+    public void testDefaultFreeformSizeRespectsMinAspectRatio() {
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM);
+
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+
+        mActivity.info.applicationInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
+        mActivity.info.setMinAspectRatio(5f);
+
+        assertEquals(RESULT_CONTINUE,
+                new CalculateRequestBuilder()
+                        .setOptions(options).calculate());
+
+        final float aspectRatio =
+                (float) Math.max(mResult.mBounds.width(), mResult.mBounds.height())
+                        / (float) Math.min(mResult.mBounds.width(), mResult.mBounds.height());
+        assertTrue("Bounds aspect ratio should be at least 5.0, but was " + aspectRatio,
+                aspectRatio >= 5f);
+    }
+
+    @Test
+    public void testDefaultFreeformSizeRespectsMaxAspectRatio() {
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM);
+
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+
+        mActivity.info.applicationInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
+        mActivity.info.setMaxAspectRatio(1.5f);
+
+        assertEquals(RESULT_CONTINUE,
+                new CalculateRequestBuilder()
+                        .setOptions(options).calculate());
+
+        final float aspectRatio =
+                (float) Math.max(mResult.mBounds.width(), mResult.mBounds.height())
+                        / (float) Math.min(mResult.mBounds.width(), mResult.mBounds.height());
+        assertTrue("Bounds aspect ratio should be at most 1.5, but was " + aspectRatio,
+                aspectRatio <= 1.5f);
+    }
+
+    @Test
     public void testCascadesToSourceSizeForFreeform() {
         final TestDisplayContent freeformDisplay = createNewDisplayContent(
                 WINDOWING_MODE_FREEFORM);
@@ -1348,6 +1392,72 @@
     }
 
     @Test
+    public void testCascadesToSourceSizeForFreeformRespectingMinAspectRatio() {
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM);
+
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+
+        final ActivityRecord source = createSourceActivity(freeformDisplay);
+        source.setBounds(0, 0, 412, 732);
+
+        mActivity.info.applicationInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
+        mActivity.info.setMinAspectRatio(5f);
+
+        assertEquals(RESULT_CONTINUE,
+                new CalculateRequestBuilder().setSource(source).setOptions(options).calculate());
+
+        final Rect displayBounds = freeformDisplay.getBounds();
+        assertTrue("Left bounds should be larger than 0.", mResult.mBounds.left > 0);
+        assertTrue("Top bounds should be larger than 0.", mResult.mBounds.top > 0);
+        assertTrue("Bounds should be centered at somewhere in the left half, but it's "
+                        + "centerX is " + mResult.mBounds.centerX(),
+                mResult.mBounds.centerX() < displayBounds.centerX());
+        assertTrue("Bounds should be centered at somewhere in the top half, but it's "
+                        + "centerY is " + mResult.mBounds.centerY(),
+                mResult.mBounds.centerY() < displayBounds.centerY());
+        final float aspectRatio =
+                (float) Math.max(mResult.mBounds.width(), mResult.mBounds.height())
+                        / (float) Math.min(mResult.mBounds.width(), mResult.mBounds.height());
+        assertTrue("Bounds aspect ratio should be at least 5.0, but was " + aspectRatio,
+                aspectRatio >= 5f);
+    }
+
+    @Test
+    public void testCascadesToSourceSizeForFreeformRespectingMaxAspectRatio() {
+        final TestDisplayContent freeformDisplay = createNewDisplayContent(
+                WINDOWING_MODE_FREEFORM);
+
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+
+        final ActivityRecord source = createSourceActivity(freeformDisplay);
+        source.setBounds(0, 0, 412, 732);
+
+        mActivity.info.applicationInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
+        mActivity.info.setMaxAspectRatio(1.5f);
+
+        assertEquals(RESULT_CONTINUE,
+                new CalculateRequestBuilder().setSource(source).setOptions(options).calculate());
+
+        final Rect displayBounds = freeformDisplay.getBounds();
+        assertTrue("Left bounds should be larger than 0.", mResult.mBounds.left > 0);
+        assertTrue("Top bounds should be larger than 0.", mResult.mBounds.top > 0);
+        assertTrue("Bounds should be centered at somewhere in the left half, but it's "
+                        + "centerX is " + mResult.mBounds.centerX(),
+                mResult.mBounds.centerX() < displayBounds.centerX());
+        assertTrue("Bounds should be centered at somewhere in the top half, but it's "
+                        + "centerY is " + mResult.mBounds.centerY(),
+                mResult.mBounds.centerY() < displayBounds.centerY());
+        final float aspectRatio =
+                (float) Math.max(mResult.mBounds.width(), mResult.mBounds.height())
+                        / (float) Math.min(mResult.mBounds.width(), mResult.mBounds.height());
+        assertTrue("Bounds aspect ratio should be at most 1.5, but was " + aspectRatio,
+                aspectRatio <= 1.5f);
+    }
+
+    @Test
     public void testAdjustBoundsToFitDisplay_TopLeftOutOfDisplay() {
         final TestDisplayContent freeformDisplay = createNewDisplayContent(
                 WINDOWING_MODE_FREEFORM);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
index b5219fd..d09ba29 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotPersisterTestBase.java
@@ -91,7 +91,7 @@
     @Before
     public void setUp() {
         final UserManager um = UserManager.get(getInstrumentation().getTargetContext());
-        mTestUserId = um.getUserHandle();
+        mTestUserId = um.getProcessUserId();
 
         final UserManagerInternal userManagerInternal =
                 LocalServices.getService(UserManagerInternal.class);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index ce568f1..5fde7eb 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -1056,9 +1056,9 @@
         final ActivityRecord activity1 = task1.getBottomMostActivity();
 
         assertEquals(task0.mTaskId,
-                ActivityRecord.getTaskForActivityLocked(activity0.appToken, false /* onlyRoot */));
+                ActivityRecord.getTaskForActivityLocked(activity0.token, false /* onlyRoot */));
         assertEquals(task1.mTaskId,
-                ActivityRecord.getTaskForActivityLocked(activity1.appToken,  false /* onlyRoot */));
+                ActivityRecord.getTaskForActivityLocked(activity1.token,  false /* onlyRoot */));
     }
 
     /**
@@ -1077,11 +1077,11 @@
         final ActivityRecord activity2 = new ActivityBuilder(mAtm).setTask(task).build();
 
         assertEquals(task.mTaskId,
-                ActivityRecord.getTaskForActivityLocked(activity0.appToken, true /* onlyRoot */));
+                ActivityRecord.getTaskForActivityLocked(activity0.token, true /* onlyRoot */));
         assertEquals(task.mTaskId,
-                ActivityRecord.getTaskForActivityLocked(activity1.appToken, true /* onlyRoot */));
+                ActivityRecord.getTaskForActivityLocked(activity1.token, true /* onlyRoot */));
         assertEquals("No task must be reported for activity that is above root", INVALID_TASK_ID,
-                ActivityRecord.getTaskForActivityLocked(activity2.appToken, true /* onlyRoot */));
+                ActivityRecord.getTaskForActivityLocked(activity2.token, true /* onlyRoot */));
     }
 
     /**
@@ -1100,11 +1100,11 @@
         final ActivityRecord activity2 = new ActivityBuilder(mAtm).setTask(task).build();
 
         assertEquals(task.mTaskId,
-                ActivityRecord.getTaskForActivityLocked(activity0.appToken, true /* onlyRoot */));
+                ActivityRecord.getTaskForActivityLocked(activity0.token, true /* onlyRoot */));
         assertEquals("No task must be reported for activity that is above root", INVALID_TASK_ID,
-                ActivityRecord.getTaskForActivityLocked(activity1.appToken, true /* onlyRoot */));
+                ActivityRecord.getTaskForActivityLocked(activity1.token, true /* onlyRoot */));
         assertEquals("No task must be reported for activity that is above root", INVALID_TASK_ID,
-                ActivityRecord.getTaskForActivityLocked(activity2.appToken, true /* onlyRoot */));
+                ActivityRecord.getTaskForActivityLocked(activity2.token, true /* onlyRoot */));
     }
 
     /**
@@ -1126,11 +1126,11 @@
         final ActivityRecord activity2 = new ActivityBuilder(mAtm).setTask(task).build();
 
         assertEquals(task.mTaskId,
-                ActivityRecord.getTaskForActivityLocked(activity0.appToken, false /* onlyRoot */));
+                ActivityRecord.getTaskForActivityLocked(activity0.token, false /* onlyRoot */));
         assertEquals(task.mTaskId,
-                ActivityRecord.getTaskForActivityLocked(activity1.appToken, false /* onlyRoot */));
+                ActivityRecord.getTaskForActivityLocked(activity1.token, false /* onlyRoot */));
         assertEquals(task.mTaskId,
-                ActivityRecord.getTaskForActivityLocked(activity2.appToken, false /* onlyRoot */));
+                ActivityRecord.getTaskForActivityLocked(activity2.token, false /* onlyRoot */));
     }
 
     /**
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index 4c31ee2..b6701dd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -16,7 +16,6 @@
 
 package com.android.server.wm;
 
-import android.graphics.Point;
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -49,10 +48,6 @@
     }
 
     @Override
-    public void locationInParentDisplayChanged(Point offset) throws RemoteException {
-    }
-
-    @Override
     public void insetsChanged(InsetsState insetsState, boolean willMove, boolean willResize) {
     }
 
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 6d60bcf..a1c24c2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -34,8 +34,11 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
@@ -448,7 +451,8 @@
 
     @Test
     public void testIntermediateVisibility() {
-        final TransitionController controller = new TransitionController(mAtm);
+        final TaskSnapshotController snapshotController = mock(TaskSnapshotController.class);
+        final TransitionController controller = new TransitionController(mAtm, snapshotController);
         final ITransitionPlayer player = new ITransitionPlayer.Default();
         controller.registerTransitionPlayer(player);
         ITaskOrganizer mockOrg = mock(ITaskOrganizer.class);
@@ -511,6 +515,71 @@
         assertTrue(activity2.isVisible());
     }
 
+    @Test
+    public void testTransientLaunch() {
+        final TaskSnapshotController snapshotController = mock(TaskSnapshotController.class);
+        final TransitionController controller = new TransitionController(mAtm, snapshotController);
+        final ITransitionPlayer player = new ITransitionPlayer.Default();
+        controller.registerTransitionPlayer(player);
+        ITaskOrganizer mockOrg = mock(ITaskOrganizer.class);
+        final Transition openTransition = controller.createTransition(TRANSIT_OPEN);
+
+        // Start out with task2 visible and set up a transition that closes task2 and opens task1
+        final Task task1 = createTask(mDisplayContent);
+        task1.mTaskOrganizer = mockOrg;
+        final ActivityRecord activity1 = createActivityRecord(task1);
+        activity1.mVisibleRequested = false;
+        activity1.setVisible(false);
+        final Task task2 = createTask(mDisplayContent);
+        task2.mTaskOrganizer = mockOrg;
+        final ActivityRecord activity2 = createActivityRecord(task2);
+        activity2.mVisibleRequested = true;
+        activity2.setVisible(true);
+
+        openTransition.collectExistenceChange(task1);
+        openTransition.collectExistenceChange(activity1);
+        openTransition.collectExistenceChange(task2);
+        openTransition.collectExistenceChange(activity2);
+
+        activity1.mVisibleRequested = true;
+        activity1.setVisible(true);
+        activity2.mVisibleRequested = false;
+
+        // Using abort to force-finish the sync (since we can't wait for drawing in unit test).
+        // We didn't call abort on the transition itself, so it will still run onTransactionReady
+        // normally.
+        mWm.mSyncEngine.abort(openTransition.getSyncId());
+
+        verify(snapshotController, times(1)).recordTaskSnapshot(eq(task2), eq(false));
+
+        openTransition.finishTransition();
+
+        // We are now going to simulate closing task1 to return back to (open) task2.
+        final Transition closeTransition = controller.createTransition(TRANSIT_CLOSE);
+
+        closeTransition.collectExistenceChange(task1);
+        closeTransition.collectExistenceChange(activity1);
+        closeTransition.collectExistenceChange(task2);
+        closeTransition.collectExistenceChange(activity2);
+        closeTransition.setTransientLaunch(activity2);
+
+        activity1.mVisibleRequested = false;
+        activity2.mVisibleRequested = true;
+
+        // Using abort to force-finish the sync (since we obviously can't wait for drawing).
+        // We didn't call abort on the actual transition, so it will still run onTransactionReady
+        // normally.
+        mWm.mSyncEngine.abort(closeTransition.getSyncId());
+
+        // Make sure we haven't called recordSnapshot (since we are transient, it shouldn't be
+        // called until finish).
+        verify(snapshotController, times(0)).recordTaskSnapshot(eq(task1), eq(false));
+
+        closeTransition.finishTransition();
+
+        verify(snapshotController, times(1)).recordTaskSnapshot(eq(task1), eq(false));
+    }
+
     /** Fill the change map with all the parents of top. Change maps are usually fully populated */
     private static void fillChangeMap(ArrayMap<WindowContainer, Transition.ChangeInfo> changes,
             WindowContainer top) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 68053eb..db7def8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -25,6 +25,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
@@ -42,6 +43,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.wm.DisplayArea.Type.ANY;
+import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATION;
 import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
@@ -52,6 +54,7 @@
 
 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.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -440,7 +443,7 @@
         assertTrue(window.isAnimating());
         assertFalse(window.isAnimating(0, ANIMATION_TYPE_SCREEN_ROTATION));
         assertTrue(window.isAnimating(0, ANIMATION_TYPE_APP_TRANSITION));
-        assertFalse(window.isAnimatingExcluding(0, ANIMATION_TYPE_APP_TRANSITION));
+        assertFalse(window.isAnimating(0, ANIMATION_TYPE_ALL & ~ANIMATION_TYPE_APP_TRANSITION));
 
         final TestWindowContainer child = window.addChildWindow();
         assertFalse(child.isAnimating());
@@ -1106,6 +1109,71 @@
         verify(surfaceAnimator, never()).setRelativeLayer(any(), any(), anyInt());
     }
 
+    @Test
+    public void testStartChangeTransitionWhenPreviousIsNotFinished() {
+        final WindowContainer container = createTaskFragmentWithParentTask(
+                createTask(mDisplayContent), false);
+        container.mSurfaceControl = mock(SurfaceControl.class);
+        final SurfaceAnimator surfaceAnimator = container.mSurfaceAnimator;
+        final SurfaceFreezer surfaceFreezer = container.mSurfaceFreezer;
+        final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        spyOn(container);
+        spyOn(surfaceAnimator);
+        spyOn(surfaceFreezer);
+        doReturn(t).when(container).getPendingTransaction();
+        doReturn(t).when(container).getSyncTransaction();
+
+        // Leash and snapshot created for change transition.
+        container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
+        // Can't really take a snapshot, manually set one.
+        surfaceFreezer.mSnapshot = mock(SurfaceFreezer.Snapshot.class);
+
+        assertNotNull(surfaceFreezer.mLeash);
+        assertEquals(surfaceFreezer.mLeash, container.getAnimationLeash());
+
+        // Start animation: surfaceAnimator take over the leash and snapshot from surfaceFreezer.
+        container.applyAnimationUnchecked(null /* lp */, true /* enter */,
+                TRANSIT_OLD_TASK_FRAGMENT_CHANGE, false /* isVoiceInteraction */,
+                null /* sources */);
+
+        assertNull(surfaceFreezer.mLeash);
+        assertNull(surfaceFreezer.mSnapshot);
+        assertNotNull(surfaceAnimator.mLeash);
+        assertNotNull(surfaceAnimator.mSnapshot);
+        final SurfaceControl prevLeash = surfaceAnimator.mLeash;
+        final SurfaceFreezer.Snapshot prevSnapshot = surfaceAnimator.mSnapshot;
+
+        // Prepare another change transition.
+        container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
+        surfaceFreezer.mSnapshot = mock(SurfaceFreezer.Snapshot.class);
+
+        assertNotNull(surfaceFreezer.mLeash);
+        assertEquals(surfaceFreezer.mLeash, container.getAnimationLeash());
+        assertNotEquals(prevLeash, container.getAnimationLeash());
+
+        // Start another animation before the previous one is finished, it should reset the previous
+        // one, but not change the current one.
+        container.applyAnimationUnchecked(null /* lp */, true /* enter */,
+                TRANSIT_OLD_TASK_FRAGMENT_CHANGE, false /* isVoiceInteraction */,
+                null /* sources */);
+
+        verify(container, never()).onAnimationLeashLost(any());
+        verify(surfaceFreezer, never()).unfreeze(any());
+        assertNotNull(surfaceAnimator.mLeash);
+        assertNotNull(surfaceAnimator.mSnapshot);
+        assertEquals(surfaceAnimator.mLeash, container.getAnimationLeash());
+        assertNotEquals(prevLeash, surfaceAnimator.mLeash);
+        assertNotEquals(prevSnapshot, surfaceAnimator.mSnapshot);
+
+        // Clean up after animation finished.
+        surfaceAnimator.mInnerAnimationFinishedCallback.onAnimationFinished(
+                ANIMATION_TYPE_APP_TRANSITION, surfaceAnimator.getAnimation());
+
+        verify(container).onAnimationLeashLost(any());
+        assertNull(surfaceAnimator.mLeash);
+        assertNull(surfaceAnimator.mSnapshot);
+    }
+
     /* Used so we can gain access to some protected members of the {@link WindowContainer} class */
     private static class TestWindowContainer extends WindowContainer<TestWindowContainer> {
         private final int mLayer;
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 17ae2e8..a482bda 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -80,6 +80,7 @@
 import android.window.ITaskOrganizer;
 import android.window.IWindowContainerTransactionCallback;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskAppearedInfo;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
@@ -779,8 +780,7 @@
         @Override
         public void addStartingWindow(StartingWindowInfo info, IBinder appToken) { }
         @Override
-        public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-                boolean playRevealAnimation) { }
+        public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { }
         @Override
         public void copySplashScreenView(int taskId) { }
         @Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index e6ad68a..a8a9188 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -278,26 +278,6 @@
     }
 
     @Test
-    public void testCanWindowWithEmbeddedDisplayBeImeTarget() {
-        final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
-        final WindowState imeWindow = createWindow(null, TYPE_INPUT_METHOD, "imeWindow");
-
-        imeWindow.setHasSurface(true);
-        appWindow.setHasSurface(true);
-
-        appWindow.mAttrs.flags |= FLAG_NOT_FOCUSABLE;
-        assertFalse(appWindow.canBeImeTarget());
-
-        DisplayContent secondDisplay = createNewDisplay();
-        final WindowState embeddedWindow = createWindow(null, TYPE_APPLICATION, secondDisplay,
-                "embeddedWindow");
-        appWindow.addEmbeddedDisplayContent(secondDisplay);
-        embeddedWindow.setHasSurface(true);
-        embeddedWindow.mAttrs.flags &= ~FLAG_NOT_FOCUSABLE;
-        assertTrue(appWindow.canBeImeTarget());
-    }
-
-    @Test
     public void testGetWindow() {
         final WindowState root = createWindow(null, TYPE_APPLICATION, "root");
         final WindowState mediaChild = createWindow(root, TYPE_APPLICATION_MEDIA, "mediaChild");
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 8ec1bd6c..b2d4eea 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -77,7 +77,6 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
-import android.graphics.Rect;
 import android.hardware.display.DisplayManager;
 import android.os.Build;
 import android.os.Bundle;
@@ -101,6 +100,7 @@
 import android.view.WindowManager.DisplayImePolicy;
 import android.window.ITransitionPlayer;
 import android.window.StartingWindowInfo;
+import android.window.StartingWindowRemovalInfo;
 import android.window.TaskFragmentOrganizer;
 import android.window.TransitionInfo;
 import android.window.TransitionRequestInfo;
@@ -1457,12 +1457,11 @@
             }
         }
         @Override
-        public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame,
-                boolean playRevealAnimation) {
+        public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) {
             synchronized (mWMService.mGlobalLock) {
-                final IBinder appToken = mTaskAppMap.get(taskId);
+                final IBinder appToken = mTaskAppMap.get(removalInfo.taskId);
                 if (appToken != null) {
-                    mTaskAppMap.remove(taskId);
+                    mTaskAppMap.remove(removalInfo.taskId);
                     final ActivityRecord activity = mWMService.mRoot.getActivityRecord(
                             appToken);
                     WindowState win = mAppWindowMap.remove(appToken);
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index cf4ef58..f0ceff1 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -381,8 +381,8 @@
             UsageStatsIdleService.scheduleUpdateMappingsJob(getContext());
         }
         synchronized (mLock) {
-            // This should be safe to add this early. Other than reportEventOrAddToQueue, every
-            // other user grabs the lock before accessing
+            // This should be safe to add this early. Other than reportEventOrAddToQueue and
+            // getBackupPayload, every other user grabs the lock before accessing
             // mUserUnlockedStates. reportEventOrAddToQueue does not depend on anything other than
             // mUserUnlockedStates, and the lock will protect the handler.
             mUserUnlockedStates.add(userId);
@@ -1438,8 +1438,8 @@
                     }
                 } else if ("mappings".equals(arg)) {
                     final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ");
-                    final int userId = parseUserIdFromArgs(args, i, ipw);
                     synchronized (mLock) {
+                        final int userId = parseUserIdFromArgs(args, i, ipw);
                         if (userId != UserHandle.USER_NULL) {
                             mUserState.get(userId).dumpMappings(ipw);
                         }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index 52c5b6bb..f4f06fd 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -190,7 +190,8 @@
                 ServiceManager.getService(Context.WINDOW_SERVICE));
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
-        mContext.registerReceiver(mBroadcastReceiver, filter, null, handler);
+        mContext.registerReceiver(mBroadcastReceiver, filter, null, handler,
+                Context.RECEIVER_NOT_EXPORTED);
     }
 
     public boolean showSessionLocked(Bundle args, int flags,
diff --git a/startop/OWNERS b/startop/OWNERS
index 2d1eb38..11d5ad0 100644
--- a/startop/OWNERS
+++ b/startop/OWNERS
@@ -1,7 +1,2 @@
-# mailing list: startop-eng@google.com
-calin@google.com
-chriswailes@google.com
-eholk@google.com
-iam@google.com
-mathieuc@google.com
-yawanng@google.com
+include platform/art:/OWNERS
+keunyoung@google.com
diff --git a/telecomm/TEST_MAPPING b/telecomm/TEST_MAPPING
index 391dce1..775f1b8 100644
--- a/telecomm/TEST_MAPPING
+++ b/telecomm/TEST_MAPPING
@@ -25,14 +25,6 @@
       ]
     },
     {
-      "name": "CtsTelephonySdk28TestCases",
-      "options": [
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        }
-      ]
-    },
-    {
       "name": "CtsTelephony2TestCases",
       "options": [
         {
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
old mode 100755
new mode 100644
index f0d43fa..d94fafc
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -564,8 +564,14 @@
          */
         public static final int CAPABILITY_TRANSFER_CONSULTATIVE = 0x08000000;
 
+        /**
+         * Indicates whether the remote party supports RTT or not to the UI.
+         */
+
+        public static final int CAPABILITY_REMOTE_PARTY_SUPPORTS_RTT = 0x10000000;
+
         //******************************************************************************************
-        // Next CAPABILITY value: 0x10000000
+        // Next CAPABILITY value: 0x20000000
         //******************************************************************************************
 
         /**
@@ -817,6 +823,9 @@
             if (can(capabilities, CAPABILITY_TRANSFER_CONSULTATIVE)) {
                 builder.append(" CAPABILITY_TRANSFER_CONSULTATIVE");
             }
+            if (can(capabilities, CAPABILITY_REMOTE_PARTY_SUPPORTS_RTT)) {
+                builder.append(" CAPABILITY_REMOTE_PARTY_SUPPORTS_RTT");
+            }
             builder.append("]");
             return builder.toString();
         }
diff --git a/telecomm/java/android/telecom/CallerInfoAsyncQuery.java b/telecomm/java/android/telecom/CallerInfoAsyncQuery.java
index bf49f3c..93cd291 100644
--- a/telecomm/java/android/telecom/CallerInfoAsyncQuery.java
+++ b/telecomm/java/android/telecom/CallerInfoAsyncQuery.java
@@ -112,7 +112,7 @@
         if (DBG) Log.d(LOG_TAG, "Trying to get current content resolver...");
 
         final int currentUser = ActivityManager.getCurrentUser();
-        final int myUser = UserManager.get(context).getUserHandle();
+        final int myUser = UserManager.get(context).getProcessUserId();
 
         if (DBG) Log.d(LOG_TAG, "myUser=" + myUser + "currentUser=" + currentUser);
 
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 30fd528..9fec96b 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -422,8 +422,14 @@
      */
     public static final int CAPABILITY_TRANSFER_CONSULTATIVE = 0x10000000;
 
+    /**
+     * Indicates whether the remote party supports RTT or not to the UI.
+     */
+
+    public static final int CAPABILITY_REMOTE_PARTY_SUPPORTS_RTT = 0x20000000;
+
     //**********************************************************************************************
-    // Next CAPABILITY value: 0x20000000
+    // Next CAPABILITY value: 0x40000000
     //**********************************************************************************************
 
     /**
@@ -1138,6 +1144,10 @@
                 == CAPABILITY_TRANSFER_CONSULTATIVE) {
             builder.append(isLong ? " CAPABILITY_TRANSFER_CONSULTATIVE" : " sup_cTrans");
         }
+        if ((capabilities & CAPABILITY_REMOTE_PARTY_SUPPORTS_RTT)
+                == CAPABILITY_REMOTE_PARTY_SUPPORTS_RTT) {
+            builder.append(isLong ? " CAPABILITY_REMOTE_PARTY_SUPPORTS_RTT" : " sup_rtt");
+        }
         builder.append("]");
         return builder.toString();
     }
diff --git a/telephony/TEST_MAPPING b/telephony/TEST_MAPPING
index 02d4eb3..73e3dcd 100644
--- a/telephony/TEST_MAPPING
+++ b/telephony/TEST_MAPPING
@@ -33,14 +33,6 @@
       ]
     },    
     {
-      "name": "CtsTelephonySdk28TestCases",
-      "options": [
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        }
-      ]
-    },
-    {
       "name": "CtsTelephony3TestCases",
       "options": [
         {
diff --git a/telephony/java/android/telephony/BarringInfo.java b/telephony/java/android/telephony/BarringInfo.java
index e9698ad..0aa4b58 100644
--- a/telephony/java/android/telephony/BarringInfo.java
+++ b/telephony/java/android/telephony/BarringInfo.java
@@ -28,7 +28,6 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.List;
 import java.util.Objects;
 
 /**
@@ -269,42 +268,6 @@
         mBarringServiceInfos = barringServiceInfos;
     }
 
-    /** @hide */
-    public static BarringInfo create(
-            @NonNull android.hardware.radio.V1_5.CellIdentity halBarringCellId,
-            @NonNull List<android.hardware.radio.V1_5.BarringInfo> halBarringInfos) {
-        CellIdentity ci = CellIdentity.create(halBarringCellId);
-        SparseArray<BarringServiceInfo> serviceInfos = new SparseArray<>();
-
-        for (android.hardware.radio.V1_5.BarringInfo halBarringInfo : halBarringInfos) {
-            if (halBarringInfo.barringType
-                    == android.hardware.radio.V1_5.BarringInfo.BarringType.CONDITIONAL) {
-                if (halBarringInfo.barringTypeSpecificInfo.getDiscriminator()
-                        != android.hardware.radio.V1_5.BarringInfo.BarringTypeSpecificInfo
-                                .hidl_discriminator.conditional) {
-                    // this is an error case where the barring info is conditional but the
-                    // conditional barring fields weren't included
-                    continue;
-                }
-                android.hardware.radio.V1_5.BarringInfo.BarringTypeSpecificInfo
-                        .Conditional conditionalInfo =
-                        halBarringInfo.barringTypeSpecificInfo.conditional();
-                serviceInfos.put(
-                        halBarringInfo.serviceType, new BarringServiceInfo(
-                                halBarringInfo.barringType, // will always be CONDITIONAL here
-                                conditionalInfo.isBarred,
-                                conditionalInfo.factor,
-                                conditionalInfo.timeSeconds));
-            } else {
-                // Barring type is either NONE or UNCONDITIONAL
-                serviceInfos.put(
-                        halBarringInfo.serviceType, new BarringServiceInfo(
-                                halBarringInfo.barringType, false, 0, 0));
-            }
-        }
-        return new BarringInfo(ci, serviceInfos);
-    }
-
     /**
      * Get the BarringServiceInfo for a specified service.
      *
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 7d354a8..edfb7bf 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -27,6 +27,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Context;
+import android.net.NetworkCapabilities;
 import android.net.ipsec.ike.SaProposal;
 import android.os.Build;
 import android.os.PersistableBundle;
@@ -1318,9 +1319,14 @@
     /**
      * Determines whether a maximum size limit for IMS conference calls is enforced on the device.
      * When {@code true}, IMS conference calls will be limited to at most
-     * {@link #KEY_IMS_CONFERENCE_SIZE_LIMIT_INT} participants.  When {@code false}, no attempt is made
-     * to limit the number of participants in a conference (the carrier will raise an error when an
-     * attempt is made to merge too many participants into a conference).
+     * {@link #KEY_IMS_CONFERENCE_SIZE_LIMIT_INT} participants.  When {@code false}, no attempt is
+     * made to limit the number of participants in a conference (the carrier will raise an error
+     * when an attempt is made to merge too many participants into a conference).
+     * <p>
+     * Note: The maximum size of a conference can ONLY be supported where
+     * {@link #KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL} is {@code true} since the platform
+     * needs conference event package data to accurately know the number of participants in the
+     * conference.
      */
     public static final String KEY_IS_IMS_CONFERENCE_SIZE_ENFORCED_BOOL =
             "is_ims_conference_size_enforced_bool";
@@ -5002,6 +5008,50 @@
     public static final String KEY_APN_PRIORITY_STRING_ARRAY = "apn_priority_string_array";
 
     /**
+     * Network capability priority for determine the satisfy order in telephony. This is used when
+     * the network only allows single PDN. The priority is from the lowest 0 to the highest 100.
+     * The long-lived network request usually has the lowest priority. This allows other short-lived
+     * requests like MMS requests to be established. Emergency request always has the highest
+     * priority.
+     *
+     * // TODO: Remove KEY_APN_PRIORITY_STRING_ARRAY
+     * @hide
+     */
+    public static final String KEY_TELEPHONY_NETWORK_CAPABILITY_PRIORITIES_STRING_ARRAY =
+            "telephony_network_capability_priorities_string_array";
+
+    /**
+     * Defines the rules for data retry.
+     *
+     * The syntax of the retry rule:
+     * 1. Retry based on {@link NetworkCapabilities}
+     * "capabilities=[netCaps1|netCaps2|...], [retry_interval=x], [backoff=[true|false]],
+     *     [maximum_retries=y]"
+     *
+     * 2. Retry based on {@link DataFailCause}
+     * "fail_causes=[cause1|cause2|cause3|...], [retry_interval=x], [backoff=[true|false]],
+     *     [maximum_retries=y]"
+     *
+     * 3. Retry based on {@link NetworkCapabilities} and {@link DataFailCause}
+     * "capabilities=[netCaps1|netCaps2|...], fail_causes=[cause1|cause2|cause3|...],
+     *     [retry_interval=x], [backoff=[true|false]], [maximum_retries=y]"
+     *
+     * For example,
+     * "capabilities=eims, retry_interval=1000, maximum_retries=20" means if the attached
+     * network request is emergency, then retry data network setup every 1 second for up to 20
+     * times.
+     *
+     * "fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|-3|2253|2254
+     * , maximum_retries=0" means for those fail causes, never retry with timers. Note that
+     * when environment changes, retry can still happens.
+     *
+     * // TODO: remove KEY_CARRIER_DATA_CALL_RETRY_CONFIG_STRINGS
+     * @hide
+     */
+    public static final String KEY_TELEPHONY_DATA_RETRY_RULES_STRING_ARRAY =
+            "telephony_data_retry_rules_string_array";
+
+    /**
      * The patterns of missed incoming call sms. This is the regular expression used for
      * matching the missed incoming call's date, time, and caller id. The pattern should match
      * fields for at least month, day, hour, and minute. Year is optional although it is encouraged.
@@ -5772,6 +5822,20 @@
                 "enterprise:0", "default:1", "mms:2", "supl:2", "dun:2", "hipri:3", "fota:2",
                 "ims:2", "cbs:2", "ia:2", "emergency:2", "mcx:3", "xcap:3"
         });
+        sDefaults.putStringArray(
+                KEY_TELEPHONY_NETWORK_CAPABILITY_PRIORITIES_STRING_ARRAY, new String[] {
+                        "eims:90", "supl:80", "mms:70", "xcap:70", "cbs:50", "mcx:50", "fota:50",
+                        "ims:40", "dun:30", "enterprise:20", "internet:20"
+                });
+        sDefaults.putStringArray(
+                KEY_TELEPHONY_DATA_RETRY_RULES_STRING_ARRAY, new String[] {
+                        "capabilities=eims, retry_interval=1000, maximum_retries=20",
+                        "fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|-3|2253|"
+                                + "2254, maximum_retries=0", // No retry for those causes
+                        "capabilities=internet|enterprise|dun|ims|fota, retry_interval=2000, "
+                                + "backoff=true, maximum_retries=13",
+                        "capabilities=mms|supl|cbs, retry_interval=2000"
+                });
         sDefaults.putStringArray(KEY_MISSED_INCOMING_CALL_SMS_PATTERN_STRING_ARRAY, new String[0]);
         sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false);
         sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, "");
diff --git a/telephony/java/android/telephony/CellConfigLte.java b/telephony/java/android/telephony/CellConfigLte.java
index 4b57d71..3e4e244 100644
--- a/telephony/java/android/telephony/CellConfigLte.java
+++ b/telephony/java/android/telephony/CellConfigLte.java
@@ -34,11 +34,6 @@
     }
 
     /** @hide */
-    public CellConfigLte(android.hardware.radio.V1_4.CellConfigLte cellConfig) {
-        mIsEndcAvailable = cellConfig.isEndcAvailable;
-    }
-
-    /** @hide */
     public CellConfigLte(boolean isEndcAvailable) {
         mIsEndcAvailable = isEndcAvailable;
     }
diff --git a/telephony/java/android/telephony/CellIdentity.java b/telephony/java/android/telephony/CellIdentity.java
index 15147da..06cfd67 100644
--- a/telephony/java/android/telephony/CellIdentity.java
+++ b/telephony/java/android/telephony/CellIdentity.java
@@ -20,7 +20,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
-import android.hardware.radio.V1_0.CellInfoType;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
@@ -359,104 +358,4 @@
 
         return true;
     }
-
-    /** @hide */
-    public static CellIdentity create(android.hardware.radio.V1_0.CellIdentity cellIdentity) {
-        if (cellIdentity == null)  return null;
-        switch(cellIdentity.cellInfoType) {
-            case CellInfoType.GSM: {
-                if (cellIdentity.cellIdentityGsm.size() == 1) {
-                    return new CellIdentityGsm(cellIdentity.cellIdentityGsm.get(0));
-                }
-                break;
-            }
-            case CellInfoType.WCDMA: {
-                if (cellIdentity.cellIdentityWcdma.size() == 1) {
-                    return new CellIdentityWcdma(cellIdentity.cellIdentityWcdma.get(0));
-                }
-                break;
-            }
-            case CellInfoType.TD_SCDMA: {
-                if (cellIdentity.cellIdentityTdscdma.size() == 1) {
-                    return new  CellIdentityTdscdma(cellIdentity.cellIdentityTdscdma.get(0));
-                }
-                break;
-            }
-            case CellInfoType.LTE: {
-                if (cellIdentity.cellIdentityLte.size() == 1) {
-                    return new CellIdentityLte(cellIdentity.cellIdentityLte.get(0));
-                }
-                break;
-            }
-            case CellInfoType.CDMA: {
-                if (cellIdentity.cellIdentityCdma.size() == 1) {
-                    return new CellIdentityCdma(cellIdentity.cellIdentityCdma.get(0));
-                }
-                break;
-            }
-            case CellInfoType.NONE: break;
-            default: break;
-        }
-        return null;
-    }
-
-    /** @hide */
-    public static CellIdentity create(android.hardware.radio.V1_2.CellIdentity cellIdentity) {
-        if (cellIdentity == null)  return null;
-        switch(cellIdentity.cellInfoType) {
-            case CellInfoType.GSM: {
-                if (cellIdentity.cellIdentityGsm.size() == 1) {
-                    return new CellIdentityGsm(cellIdentity.cellIdentityGsm.get(0));
-                }
-                break;
-            }
-            case CellInfoType.WCDMA: {
-                if (cellIdentity.cellIdentityWcdma.size() == 1) {
-                    return new CellIdentityWcdma(cellIdentity.cellIdentityWcdma.get(0));
-                }
-                break;
-            }
-            case CellInfoType.TD_SCDMA: {
-                if (cellIdentity.cellIdentityTdscdma.size() == 1) {
-                    return new  CellIdentityTdscdma(cellIdentity.cellIdentityTdscdma.get(0));
-                }
-                break;
-            }
-            case CellInfoType.LTE: {
-                if (cellIdentity.cellIdentityLte.size() == 1) {
-                    return new CellIdentityLte(cellIdentity.cellIdentityLte.get(0));
-                }
-                break;
-            }
-            case CellInfoType.CDMA: {
-                if (cellIdentity.cellIdentityCdma.size() == 1) {
-                    return new CellIdentityCdma(cellIdentity.cellIdentityCdma.get(0));
-                }
-                break;
-            }
-            case CellInfoType.NONE: break;
-            default: break;
-        }
-        return null;
-    }
-
-    /** @hide */
-    public static CellIdentity create(android.hardware.radio.V1_5.CellIdentity ci) {
-        if (ci == null) return null;
-        switch (ci.getDiscriminator()) {
-            case android.hardware.radio.V1_5.CellIdentity.hidl_discriminator.gsm:
-                return new CellIdentityGsm(ci.gsm());
-            case android.hardware.radio.V1_5.CellIdentity.hidl_discriminator.cdma:
-                return new CellIdentityCdma(ci.cdma());
-            case android.hardware.radio.V1_5.CellIdentity.hidl_discriminator.lte:
-                return new CellIdentityLte(ci.lte());
-            case android.hardware.radio.V1_5.CellIdentity.hidl_discriminator.wcdma:
-                return new CellIdentityWcdma(ci.wcdma());
-            case android.hardware.radio.V1_5.CellIdentity.hidl_discriminator.tdscdma:
-                return new CellIdentityTdscdma(ci.tdscdma());
-            case android.hardware.radio.V1_5.CellIdentity.hidl_discriminator.nr:
-                return new CellIdentityNr(ci.nr());
-            default: return null;
-        }
-    }
 }
diff --git a/telephony/java/android/telephony/CellIdentityCdma.java b/telephony/java/android/telephony/CellIdentityCdma.java
index 58a01e9..ba3a192 100644
--- a/telephony/java/android/telephony/CellIdentityCdma.java
+++ b/telephony/java/android/telephony/CellIdentityCdma.java
@@ -112,17 +112,6 @@
         updateGlobalCellId();
     }
 
-    /** @hide */
-    public CellIdentityCdma(@NonNull android.hardware.radio.V1_0.CellIdentityCdma cid) {
-        this(cid.networkId, cid.systemId, cid.baseStationId, cid.longitude, cid.latitude, "", "");
-    }
-
-    /** @hide */
-    public CellIdentityCdma(@NonNull android.hardware.radio.V1_2.CellIdentityCdma cid) {
-        this(cid.base.networkId, cid.base.systemId, cid.base.baseStationId, cid.base.longitude,
-                cid.base.latitude, cid.operatorNames.alphaLong, cid.operatorNames.alphaShort);
-    }
-
     private CellIdentityCdma(@NonNull CellIdentityCdma cid) {
         this(cid.mNetworkId, cid.mSystemId, cid.mBasestationId, cid.mLongitude, cid.mLatitude,
                 cid.mAlphaLong, cid.mAlphaShort);
diff --git a/telephony/java/android/telephony/CellIdentityGsm.java b/telephony/java/android/telephony/CellIdentityGsm.java
index a3bec33..2516a79 100644
--- a/telephony/java/android/telephony/CellIdentityGsm.java
+++ b/telephony/java/android/telephony/CellIdentityGsm.java
@@ -101,30 +101,6 @@
         updateGlobalCellId();
     }
 
-    /** @hide */
-    public CellIdentityGsm(@NonNull android.hardware.radio.V1_0.CellIdentityGsm cid) {
-        this(cid.lac, cid.cid, cid.arfcn,
-                cid.bsic == (byte) 0xFF ? CellInfo.UNAVAILABLE : cid.bsic,
-                cid.mcc, cid.mnc, "", "", new ArraySet<>());
-    }
-
-    /** @hide */
-    public CellIdentityGsm(@NonNull android.hardware.radio.V1_2.CellIdentityGsm cid) {
-        this(cid.base.lac, cid.base.cid, cid.base.arfcn,
-                cid.base.bsic == (byte) 0xFF ? CellInfo.UNAVAILABLE : cid.base.bsic, cid.base.mcc,
-                cid.base.mnc, cid.operatorNames.alphaLong, cid.operatorNames.alphaShort,
-                new ArraySet<>());
-    }
-
-    /** @hide */
-    public CellIdentityGsm(@NonNull android.hardware.radio.V1_5.CellIdentityGsm cid) {
-        this(cid.base.base.lac, cid.base.base.cid, cid.base.base.arfcn,
-                cid.base.base.bsic == (byte) 0xFF ? CellInfo.UNAVAILABLE
-                        : cid.base.base.bsic, cid.base.base.mcc,
-                cid.base.base.mnc, cid.base.operatorNames.alphaLong,
-                cid.base.operatorNames.alphaShort, cid.additionalPlmns);
-    }
-
     private CellIdentityGsm(@NonNull CellIdentityGsm cid) {
         this(cid.mLac, cid.mCid, cid.mArfcn, cid.mBsic, cid.mMccStr,
                 cid.mMncStr, cid.mAlphaLong, cid.mAlphaShort, cid.mAdditionalPlmns);
diff --git a/telephony/java/android/telephony/CellIdentityLte.java b/telephony/java/android/telephony/CellIdentityLte.java
index bd92d00a..4db00cf 100644
--- a/telephony/java/android/telephony/CellIdentityLte.java
+++ b/telephony/java/android/telephony/CellIdentityLte.java
@@ -136,31 +136,6 @@
         updateGlobalCellId();
     }
 
-    /** @hide */
-    public CellIdentityLte(@NonNull android.hardware.radio.V1_0.CellIdentityLte cid) {
-        this(cid.ci, cid.pci, cid.tac, cid.earfcn, new int[] {},
-                CellInfo.UNAVAILABLE, cid.mcc, cid.mnc, "", "", new ArraySet<>(), null);
-    }
-
-    /** @hide */
-    public CellIdentityLte(@NonNull android.hardware.radio.V1_2.CellIdentityLte cid) {
-        this(cid.base.ci, cid.base.pci, cid.base.tac, cid.base.earfcn, new int[] {},
-                cid.bandwidth, cid.base.mcc, cid.base.mnc, cid.operatorNames.alphaLong,
-                cid.operatorNames.alphaShort, new ArraySet<>(), null);
-    }
-
-    /** @hide */
-    public CellIdentityLte(@NonNull android.hardware.radio.V1_5.CellIdentityLte cid) {
-        this(cid.base.base.ci, cid.base.base.pci, cid.base.base.tac, cid.base.base.earfcn,
-                cid.bands.stream().mapToInt(Integer::intValue).toArray(), cid.base.bandwidth,
-                cid.base.base.mcc, cid.base.base.mnc, cid.base.operatorNames.alphaLong,
-                cid.base.operatorNames.alphaShort, cid.additionalPlmns,
-                cid.optionalCsgInfo.getDiscriminator()
-                        == android.hardware.radio.V1_5.OptionalCsgInfo.hidl_discriminator.csgInfo
-                                ? new ClosedSubscriberGroupInfo(cid.optionalCsgInfo.csgInfo())
-                                        : null);
-    }
-
     private CellIdentityLte(@NonNull CellIdentityLte cid) {
         this(cid.mCi, cid.mPci, cid.mTac, cid.mEarfcn, cid.mBands, cid.mBandwidth, cid.mMccStr,
                 cid.mMncStr, cid.mAlphaLong, cid.mAlphaShort, cid.mAdditionalPlmns, cid.mCsgInfo);
diff --git a/telephony/java/android/telephony/CellIdentityNr.java b/telephony/java/android/telephony/CellIdentityNr.java
index 4f50521..6aeb482 100644
--- a/telephony/java/android/telephony/CellIdentityNr.java
+++ b/telephony/java/android/telephony/CellIdentityNr.java
@@ -65,7 +65,6 @@
     }
 
     /**
-     *
      * @param pci Physical Cell Id in range [0, 1007].
      * @param tac 24-bit Tracking Area Code.
      * @param nrArfcn NR Absolute Radio Frequency Channel Number, in range [0, 3279165].
@@ -100,21 +99,6 @@
     }
 
     /** @hide */
-    public CellIdentityNr(@NonNull android.hardware.radio.V1_4.CellIdentityNr cid) {
-        this(cid.pci, cid.tac, cid.nrarfcn, new int[] {}, cid.mcc, cid.mnc, cid.nci,
-                cid.operatorNames.alphaLong, cid.operatorNames.alphaShort,
-                new ArraySet<>());
-    }
-
-    /** @hide */
-    public CellIdentityNr(@NonNull android.hardware.radio.V1_5.CellIdentityNr cid) {
-        this(cid.base.pci, cid.base.tac, cid.base.nrarfcn,
-                cid.bands.stream().mapToInt(Integer::intValue).toArray(), cid.base.mcc,
-                cid.base.mnc, cid.base.nci, cid.base.operatorNames.alphaLong,
-                cid.base.operatorNames.alphaShort, cid.additionalPlmns);
-    }
-
-    /** @hide */
     @Override
     public @NonNull CellIdentityNr sanitizeLocationInfo() {
         return new CellIdentityNr(CellInfo.UNAVAILABLE, CellInfo.UNAVAILABLE, mNrArfcn,
diff --git a/telephony/java/android/telephony/CellIdentityTdscdma.java b/telephony/java/android/telephony/CellIdentityTdscdma.java
index ec07d54..13d9373 100644
--- a/telephony/java/android/telephony/CellIdentityTdscdma.java
+++ b/telephony/java/android/telephony/CellIdentityTdscdma.java
@@ -113,31 +113,6 @@
     }
 
     /** @hide */
-    public CellIdentityTdscdma(@NonNull android.hardware.radio.V1_0.CellIdentityTdscdma cid) {
-        this(cid.mcc, cid.mnc, cid.lac, cid.cid, cid.cpid, CellInfo.UNAVAILABLE, "", "",
-                Collections.emptyList(), null);
-    }
-
-    /** @hide */
-    public CellIdentityTdscdma(@NonNull android.hardware.radio.V1_2.CellIdentityTdscdma cid) {
-        this(cid.base.mcc, cid.base.mnc, cid.base.lac, cid.base.cid, cid.base.cpid,
-                cid.uarfcn, cid.operatorNames.alphaLong, cid.operatorNames.alphaShort,
-                Collections.emptyList(), null);
-    }
-
-    /** @hide */
-    public CellIdentityTdscdma(@NonNull android.hardware.radio.V1_5.CellIdentityTdscdma cid) {
-        this(cid.base.base.mcc, cid.base.base.mnc, cid.base.base.lac, cid.base.base.cid,
-                cid.base.base.cpid, cid.base.uarfcn, cid.base.operatorNames.alphaLong,
-                cid.base.operatorNames.alphaShort,
-                cid.additionalPlmns,
-                cid.optionalCsgInfo.getDiscriminator()
-                        == android.hardware.radio.V1_5.OptionalCsgInfo.hidl_discriminator.csgInfo
-                                ? new ClosedSubscriberGroupInfo(cid.optionalCsgInfo.csgInfo())
-                                        : null);
-    }
-
-    /** @hide */
     @Override
     public @NonNull CellIdentityTdscdma sanitizeLocationInfo() {
         return new CellIdentityTdscdma(mMccStr, mMncStr, CellInfo.UNAVAILABLE, CellInfo.UNAVAILABLE,
diff --git a/telephony/java/android/telephony/CellIdentityWcdma.java b/telephony/java/android/telephony/CellIdentityWcdma.java
index b04a51d..9b463da 100644
--- a/telephony/java/android/telephony/CellIdentityWcdma.java
+++ b/telephony/java/android/telephony/CellIdentityWcdma.java
@@ -107,30 +107,6 @@
         updateGlobalCellId();
     }
 
-    /** @hide */
-    public CellIdentityWcdma(@NonNull android.hardware.radio.V1_0.CellIdentityWcdma cid) {
-        this(cid.lac, cid.cid, cid.psc, cid.uarfcn, cid.mcc, cid.mnc, "", "",
-                new ArraySet<>(), null);
-    }
-
-    /** @hide */
-    public CellIdentityWcdma(@NonNull android.hardware.radio.V1_2.CellIdentityWcdma cid) {
-        this(cid.base.lac, cid.base.cid, cid.base.psc, cid.base.uarfcn,
-                cid.base.mcc, cid.base.mnc, cid.operatorNames.alphaLong,
-                cid.operatorNames.alphaShort, new ArraySet<>(), null);
-    }
-
-    /** @hide */
-    public CellIdentityWcdma(@NonNull android.hardware.radio.V1_5.CellIdentityWcdma cid) {
-        this(cid.base.base.lac, cid.base.base.cid, cid.base.base.psc, cid.base.base.uarfcn,
-                cid.base.base.mcc, cid.base.base.mnc, cid.base.operatorNames.alphaLong,
-                cid.base.operatorNames.alphaShort, cid.additionalPlmns,
-                cid.optionalCsgInfo.getDiscriminator()
-                        == android.hardware.radio.V1_5.OptionalCsgInfo.hidl_discriminator.csgInfo
-                                ? new ClosedSubscriberGroupInfo(cid.optionalCsgInfo.csgInfo())
-                                        : null);
-    }
-
     private CellIdentityWcdma(@NonNull CellIdentityWcdma cid) {
         this(cid.mLac, cid.mCid, cid.mPsc, cid.mUarfcn, cid.mMccStr,
                 cid.mMncStr, cid.mAlphaLong, cid.mAlphaShort, cid.mAdditionalPlmns, cid.mCsgInfo);
diff --git a/telephony/java/android/telephony/CellInfo.java b/telephony/java/android/telephony/CellInfo.java
index 189a4b8..2b2df24 100644
--- a/telephony/java/android/telephony/CellInfo.java
+++ b/telephony/java/android/telephony/CellInfo.java
@@ -20,7 +20,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.compat.annotation.UnsupportedAppUsage;
-import android.hardware.radio.V1_4.CellInfo.Info;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -150,6 +149,13 @@
     private long mTimeStamp;
 
     /** @hide */
+    protected CellInfo(int cellConnectionStatus, boolean registered, long timestamp) {
+        mCellConnectionStatus = cellConnectionStatus;
+        mRegistered = registered;
+        mTimeStamp = timestamp;
+    }
+
+    /** @hide */
     protected CellInfo() {
         this.mRegistered = false;
         this.mTimeStamp = Long.MAX_VALUE;
@@ -321,131 +327,4 @@
             return new CellInfo[size];
         }
     };
-
-    /** @hide */
-    protected CellInfo(android.hardware.radio.V1_0.CellInfo ci) {
-        this.mRegistered = ci.registered;
-        this.mTimeStamp = ci.timeStamp;
-        this.mCellConnectionStatus = CONNECTION_UNKNOWN;
-    }
-
-    /** @hide */
-    protected CellInfo(android.hardware.radio.V1_2.CellInfo ci) {
-        this.mRegistered = ci.registered;
-        this.mTimeStamp = ci.timeStamp;
-        this.mCellConnectionStatus = ci.connectionStatus;
-    }
-
-    /** @hide */
-    protected CellInfo(android.hardware.radio.V1_4.CellInfo ci, long timeStamp) {
-        this.mRegistered = ci.isRegistered;
-        this.mTimeStamp = timeStamp;
-        this.mCellConnectionStatus = ci.connectionStatus;
-    }
-
-    /** @hide */
-    protected CellInfo(android.hardware.radio.V1_5.CellInfo ci, long timeStamp) {
-        this.mRegistered = ci.registered;
-        this.mTimeStamp = timeStamp;
-        this.mCellConnectionStatus = ci.connectionStatus;
-    }
-
-    /** @hide */
-    protected CellInfo(android.hardware.radio.V1_6.CellInfo ci, long timeStamp) {
-        this.mRegistered = ci.registered;
-        this.mTimeStamp = timeStamp;
-        this.mCellConnectionStatus = ci.connectionStatus;
-    }
-
-    /** @hide */
-    public static CellInfo create(android.hardware.radio.V1_0.CellInfo ci) {
-        if (ci == null) return null;
-        switch(ci.cellInfoType) {
-            case android.hardware.radio.V1_0.CellInfoType.GSM: return new CellInfoGsm(ci);
-            case android.hardware.radio.V1_0.CellInfoType.CDMA: return new CellInfoCdma(ci);
-            case android.hardware.radio.V1_0.CellInfoType.LTE: return new CellInfoLte(ci);
-            case android.hardware.radio.V1_0.CellInfoType.WCDMA: return new CellInfoWcdma(ci);
-            case android.hardware.radio.V1_0.CellInfoType.TD_SCDMA: return new CellInfoTdscdma(ci);
-            default: return null;
-        }
-    }
-
-    /** @hide */
-    public static CellInfo create(android.hardware.radio.V1_2.CellInfo ci) {
-        if (ci == null) return null;
-        switch(ci.cellInfoType) {
-            case android.hardware.radio.V1_0.CellInfoType.GSM: return new CellInfoGsm(ci);
-            case android.hardware.radio.V1_0.CellInfoType.CDMA: return new CellInfoCdma(ci);
-            case android.hardware.radio.V1_0.CellInfoType.LTE: return new CellInfoLte(ci);
-            case android.hardware.radio.V1_0.CellInfoType.WCDMA: return new CellInfoWcdma(ci);
-            case android.hardware.radio.V1_0.CellInfoType.TD_SCDMA: return new CellInfoTdscdma(ci);
-            default: return null;
-        }
-    }
-
-    /** @hide */
-    public static CellInfo create(android.hardware.radio.V1_4.CellInfo ci, long timeStamp) {
-        if (ci == null) return null;
-        switch (ci.info.getDiscriminator()) {
-            case Info.hidl_discriminator.gsm: return new CellInfoGsm(ci, timeStamp);
-            case Info.hidl_discriminator.cdma: return new CellInfoCdma(ci, timeStamp);
-            case Info.hidl_discriminator.lte: return new CellInfoLte(ci, timeStamp);
-            case Info.hidl_discriminator.wcdma: return new CellInfoWcdma(ci, timeStamp);
-            case Info.hidl_discriminator.tdscdma: return new CellInfoTdscdma(ci, timeStamp);
-            case Info.hidl_discriminator.nr: return new CellInfoNr(ci, timeStamp);
-            default: return null;
-        }
-    }
-
-    /** @hide */
-    public static CellInfo create(android.hardware.radio.V1_5.CellInfo ci, long timeStamp) {
-        if (ci == null) return null;
-        switch (ci.ratSpecificInfo.getDiscriminator()) {
-            case android.hardware.radio.V1_5.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.gsm:
-                return new CellInfoGsm(ci, timeStamp);
-            case android.hardware.radio.V1_5.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.cdma:
-                return new CellInfoCdma(ci, timeStamp);
-            case android.hardware.radio.V1_5.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.lte:
-                return new CellInfoLte(ci, timeStamp);
-            case android.hardware.radio.V1_5.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.wcdma:
-                return new CellInfoWcdma(ci, timeStamp);
-            case android.hardware.radio.V1_5.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.tdscdma:
-                return new CellInfoTdscdma(ci, timeStamp);
-            case android.hardware.radio.V1_5.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.nr:
-                return new CellInfoNr(ci, timeStamp);
-            default: return null;
-        }
-    }
-
-    /** @hide */
-    public static CellInfo create(android.hardware.radio.V1_6.CellInfo ci, long timeStamp) {
-        if (ci == null) return null;
-        switch (ci.ratSpecificInfo.getDiscriminator()) {
-            case android.hardware.radio.V1_6.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.gsm:
-                return new CellInfoGsm(ci, timeStamp);
-            case android.hardware.radio.V1_6.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.cdma:
-                return new CellInfoCdma(ci, timeStamp);
-            case android.hardware.radio.V1_6.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.lte:
-                return new CellInfoLte(ci, timeStamp);
-            case android.hardware.radio.V1_6.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.wcdma:
-                return new CellInfoWcdma(ci, timeStamp);
-            case android.hardware.radio.V1_6.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.tdscdma:
-                return new CellInfoTdscdma(ci, timeStamp);
-            case android.hardware.radio.V1_6.CellInfo
-                    .CellInfoRatSpecificInfo.hidl_discriminator.nr:
-                return new CellInfoNr(ci, timeStamp);
-            default: return null;
-        }
-    }
 }
diff --git a/telephony/java/android/telephony/CellInfoCdma.java b/telephony/java/android/telephony/CellInfoCdma.java
index dbb30d2..aa8cff5 100644
--- a/telephony/java/android/telephony/CellInfoCdma.java
+++ b/telephony/java/android/telephony/CellInfoCdma.java
@@ -52,48 +52,11 @@
     }
 
     /** @hide */
-    public CellInfoCdma(android.hardware.radio.V1_0.CellInfo ci) {
-        super(ci);
-        final android.hardware.radio.V1_0.CellInfoCdma cic = ci.cdma.get(0);
-        mCellIdentityCdma = new CellIdentityCdma(cic.cellIdentityCdma);
-        mCellSignalStrengthCdma =
-            new CellSignalStrengthCdma(cic.signalStrengthCdma, cic.signalStrengthEvdo);
-    }
-
-    /** @hide */
-    public CellInfoCdma(android.hardware.radio.V1_2.CellInfo ci) {
-        super(ci);
-        final android.hardware.radio.V1_2.CellInfoCdma cic = ci.cdma.get(0);
-        mCellIdentityCdma = new CellIdentityCdma(cic.cellIdentityCdma);
-        mCellSignalStrengthCdma =
-            new CellSignalStrengthCdma(cic.signalStrengthCdma, cic.signalStrengthEvdo);
-    }
-
-    /** @hide */
-    public CellInfoCdma(android.hardware.radio.V1_4.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_2.CellInfoCdma cic = ci.info.cdma();
-        mCellIdentityCdma = new CellIdentityCdma(cic.cellIdentityCdma);
-        mCellSignalStrengthCdma =
-                new CellSignalStrengthCdma(cic.signalStrengthCdma, cic.signalStrengthEvdo);
-    }
-
-    /** @hide */
-    public CellInfoCdma(android.hardware.radio.V1_5.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_2.CellInfoCdma cic = ci.ratSpecificInfo.cdma();
-        mCellIdentityCdma = new CellIdentityCdma(cic.cellIdentityCdma);
-        mCellSignalStrengthCdma =
-                new CellSignalStrengthCdma(cic.signalStrengthCdma, cic.signalStrengthEvdo);
-    }
-
-    /** @hide */
-    public CellInfoCdma(android.hardware.radio.V1_6.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_2.CellInfoCdma cic = ci.ratSpecificInfo.cdma();
-        mCellIdentityCdma = new CellIdentityCdma(cic.cellIdentityCdma);
-        mCellSignalStrengthCdma =
-                new CellSignalStrengthCdma(cic.signalStrengthCdma, cic.signalStrengthEvdo);
+    public CellInfoCdma(int connectionStatus, boolean registered, long timeStamp,
+            CellIdentityCdma cellIdentityCdma, CellSignalStrengthCdma cellSignalStrengthCdma) {
+        super(connectionStatus, registered, timeStamp);
+        mCellIdentityCdma = cellIdentityCdma;
+        mCellSignalStrengthCdma = cellSignalStrengthCdma;
     }
 
     /**
diff --git a/telephony/java/android/telephony/CellInfoGsm.java b/telephony/java/android/telephony/CellInfoGsm.java
index e1d996e..76e825b 100644
--- a/telephony/java/android/telephony/CellInfoGsm.java
+++ b/telephony/java/android/telephony/CellInfoGsm.java
@@ -51,43 +51,11 @@
     }
 
     /** @hide */
-    public CellInfoGsm(android.hardware.radio.V1_0.CellInfo ci) {
-        super(ci);
-        final android.hardware.radio.V1_0.CellInfoGsm cig = ci.gsm.get(0);
-        mCellIdentityGsm = new CellIdentityGsm(cig.cellIdentityGsm);
-        mCellSignalStrengthGsm = new CellSignalStrengthGsm(cig.signalStrengthGsm);
-    }
-
-    /** @hide */
-    public CellInfoGsm(android.hardware.radio.V1_2.CellInfo ci) {
-        super(ci);
-        final android.hardware.radio.V1_2.CellInfoGsm cig = ci.gsm.get(0);
-        mCellIdentityGsm = new CellIdentityGsm(cig.cellIdentityGsm);
-        mCellSignalStrengthGsm = new CellSignalStrengthGsm(cig.signalStrengthGsm);
-    }
-
-    /** @hide */
-    public CellInfoGsm(android.hardware.radio.V1_4.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_2.CellInfoGsm cig = ci.info.gsm();
-        mCellIdentityGsm = new CellIdentityGsm(cig.cellIdentityGsm);
-        mCellSignalStrengthGsm = new CellSignalStrengthGsm(cig.signalStrengthGsm);
-    }
-
-    /** @hide */
-    public CellInfoGsm(android.hardware.radio.V1_5.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_5.CellInfoGsm cig = ci.ratSpecificInfo.gsm();
-        mCellIdentityGsm = new CellIdentityGsm(cig.cellIdentityGsm);
-        mCellSignalStrengthGsm = new CellSignalStrengthGsm(cig.signalStrengthGsm);
-    }
-
-    /** @hide */
-    public CellInfoGsm(android.hardware.radio.V1_6.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_5.CellInfoGsm cig = ci.ratSpecificInfo.gsm();
-        mCellIdentityGsm = new CellIdentityGsm(cig.cellIdentityGsm);
-        mCellSignalStrengthGsm = new CellSignalStrengthGsm(cig.signalStrengthGsm);
+    public CellInfoGsm(int cellConnectionStatus, boolean registered, long timeStamp,
+            CellIdentityGsm cellIdentityGsm, CellSignalStrengthGsm cellSignalStrengthGsm) {
+        super(cellConnectionStatus, registered, timeStamp);
+        mCellIdentityGsm = cellIdentityGsm;
+        mCellSignalStrengthGsm = cellSignalStrengthGsm;
     }
 
     /**
diff --git a/telephony/java/android/telephony/CellInfoLte.java b/telephony/java/android/telephony/CellInfoLte.java
index 39b320a..2d176d5 100644
--- a/telephony/java/android/telephony/CellInfoLte.java
+++ b/telephony/java/android/telephony/CellInfoLte.java
@@ -56,48 +56,13 @@
     }
 
     /** @hide */
-    public CellInfoLte(android.hardware.radio.V1_0.CellInfo ci) {
-        super(ci);
-        final android.hardware.radio.V1_0.CellInfoLte cil = ci.lte.get(0);
-        mCellIdentityLte = new CellIdentityLte(cil.cellIdentityLte);
-        mCellSignalStrengthLte = new CellSignalStrengthLte(cil.signalStrengthLte);
-        mCellConfig = new CellConfigLte();
-    }
-
-    /** @hide */
-    public CellInfoLte(android.hardware.radio.V1_2.CellInfo ci) {
-        super(ci);
-        final android.hardware.radio.V1_2.CellInfoLte cil = ci.lte.get(0);
-        mCellIdentityLte = new CellIdentityLte(cil.cellIdentityLte);
-        mCellSignalStrengthLte = new CellSignalStrengthLte(cil.signalStrengthLte);
-        mCellConfig = new CellConfigLte();
-    }
-
-    /** @hide */
-    public CellInfoLte(android.hardware.radio.V1_4.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_4.CellInfoLte cil = ci.info.lte();
-        mCellIdentityLte = new CellIdentityLte(cil.base.cellIdentityLte);
-        mCellSignalStrengthLte = new CellSignalStrengthLte(cil.base.signalStrengthLte);
-        mCellConfig = new CellConfigLte(cil.cellConfig);
-    }
-
-    /** @hide */
-    public CellInfoLte(android.hardware.radio.V1_5.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_5.CellInfoLte cil = ci.ratSpecificInfo.lte();
-        mCellIdentityLte = new CellIdentityLte(cil.cellIdentityLte);
-        mCellSignalStrengthLte = new CellSignalStrengthLte(cil.signalStrengthLte);
-        mCellConfig = new CellConfigLte();
-    }
-
-    /** @hide */
-    public CellInfoLte(android.hardware.radio.V1_6.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_6.CellInfoLte cil = ci.ratSpecificInfo.lte();
-        mCellIdentityLte = new CellIdentityLte(cil.cellIdentityLte);
-        mCellSignalStrengthLte = new CellSignalStrengthLte(cil.signalStrengthLte);
-        mCellConfig = new CellConfigLte();
+    public CellInfoLte(int connectionStatus, boolean registered, long timeStamp,
+            CellIdentityLte cellIdentityLte, CellSignalStrengthLte cellSignalStrengthLte,
+            CellConfigLte cellConfig) {
+        super(connectionStatus, registered, timeStamp);
+        mCellIdentityLte = cellIdentityLte;
+        mCellSignalStrengthLte = cellSignalStrengthLte;
+        mCellConfig = cellConfig;
     }
 
     /**
diff --git a/telephony/java/android/telephony/CellInfoNr.java b/telephony/java/android/telephony/CellInfoNr.java
index 12e6a38..37fac24 100644
--- a/telephony/java/android/telephony/CellInfoNr.java
+++ b/telephony/java/android/telephony/CellInfoNr.java
@@ -53,27 +53,11 @@
     }
 
     /** @hide */
-    public CellInfoNr(android.hardware.radio.V1_4.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_4.CellInfoNr cil = ci.info.nr();
-        mCellIdentity = new CellIdentityNr(cil.cellidentity);
-        mCellSignalStrength = new CellSignalStrengthNr(cil.signalStrength);
-    }
-
-    /** @hide */
-    public CellInfoNr(android.hardware.radio.V1_5.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_5.CellInfoNr cil = ci.ratSpecificInfo.nr();
-        mCellIdentity = new CellIdentityNr(cil.cellIdentityNr);
-        mCellSignalStrength = new CellSignalStrengthNr(cil.signalStrengthNr);
-    }
-
-    /** @hide */
-    public CellInfoNr(android.hardware.radio.V1_6.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_6.CellInfoNr cil = ci.ratSpecificInfo.nr();
-        mCellIdentity = new CellIdentityNr(cil.cellIdentityNr);
-        mCellSignalStrength = new CellSignalStrengthNr(cil.signalStrengthNr);
+    public CellInfoNr(int connectionStatus, boolean registered, long timeStamp,
+            CellIdentityNr cellIdentityNr, CellSignalStrengthNr cellSignalStrengthNr) {
+        super(connectionStatus, registered, timeStamp);
+        mCellIdentity = cellIdentityNr;
+        mCellSignalStrength = cellSignalStrengthNr;
     }
 
     /**
diff --git a/telephony/java/android/telephony/CellInfoTdscdma.java b/telephony/java/android/telephony/CellInfoTdscdma.java
index 994b317..d8db429 100644
--- a/telephony/java/android/telephony/CellInfoTdscdma.java
+++ b/telephony/java/android/telephony/CellInfoTdscdma.java
@@ -54,43 +54,12 @@
     }
 
     /** @hide */
-    public CellInfoTdscdma(android.hardware.radio.V1_0.CellInfo ci) {
-        super(ci);
-        final android.hardware.radio.V1_0.CellInfoTdscdma cit = ci.tdscdma.get(0);
-        mCellIdentityTdscdma = new CellIdentityTdscdma(cit.cellIdentityTdscdma);
-        mCellSignalStrengthTdscdma = new CellSignalStrengthTdscdma(cit.signalStrengthTdscdma);
-    }
-
-    /** @hide */
-    public CellInfoTdscdma(android.hardware.radio.V1_2.CellInfo ci) {
-        super(ci);
-        final android.hardware.radio.V1_2.CellInfoTdscdma cit = ci.tdscdma.get(0);
-        mCellIdentityTdscdma = new CellIdentityTdscdma(cit.cellIdentityTdscdma);
-        mCellSignalStrengthTdscdma = new CellSignalStrengthTdscdma(cit.signalStrengthTdscdma);
-    }
-
-    /** @hide */
-    public CellInfoTdscdma(android.hardware.radio.V1_4.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_2.CellInfoTdscdma cit = ci.info.tdscdma();
-        mCellIdentityTdscdma = new CellIdentityTdscdma(cit.cellIdentityTdscdma);
-        mCellSignalStrengthTdscdma = new CellSignalStrengthTdscdma(cit.signalStrengthTdscdma);
-    }
-
-    /** @hide */
-    public CellInfoTdscdma(android.hardware.radio.V1_5.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_5.CellInfoTdscdma cit = ci.ratSpecificInfo.tdscdma();
-        mCellIdentityTdscdma = new CellIdentityTdscdma(cit.cellIdentityTdscdma);
-        mCellSignalStrengthTdscdma = new CellSignalStrengthTdscdma(cit.signalStrengthTdscdma);
-    }
-
-    /** @hide */
-    public CellInfoTdscdma(android.hardware.radio.V1_6.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_5.CellInfoTdscdma cit = ci.ratSpecificInfo.tdscdma();
-        mCellIdentityTdscdma = new CellIdentityTdscdma(cit.cellIdentityTdscdma);
-        mCellSignalStrengthTdscdma = new CellSignalStrengthTdscdma(cit.signalStrengthTdscdma);
+    public CellInfoTdscdma(int connectionStatus, boolean registered, long timeStamp,
+            CellIdentityTdscdma cellIdentityTdscdma,
+            CellSignalStrengthTdscdma cellSignalStrengthTdscdma) {
+        super(connectionStatus, registered, timeStamp);
+        mCellIdentityTdscdma = cellIdentityTdscdma;
+        mCellSignalStrengthTdscdma = cellSignalStrengthTdscdma;
     }
 
     /**
diff --git a/telephony/java/android/telephony/CellInfoWcdma.java b/telephony/java/android/telephony/CellInfoWcdma.java
index 62ac0b8..dc8e1fe 100644
--- a/telephony/java/android/telephony/CellInfoWcdma.java
+++ b/telephony/java/android/telephony/CellInfoWcdma.java
@@ -49,43 +49,11 @@
     }
 
     /** @hide */
-    public CellInfoWcdma(android.hardware.radio.V1_0.CellInfo ci) {
-        super(ci);
-        final android.hardware.radio.V1_0.CellInfoWcdma ciw = ci.wcdma.get(0);
-        mCellIdentityWcdma = new CellIdentityWcdma(ciw.cellIdentityWcdma);
-        mCellSignalStrengthWcdma = new CellSignalStrengthWcdma(ciw.signalStrengthWcdma);
-    }
-
-    /** @hide */
-    public CellInfoWcdma(android.hardware.radio.V1_2.CellInfo ci) {
-        super(ci);
-        final android.hardware.radio.V1_2.CellInfoWcdma ciw = ci.wcdma.get(0);
-        mCellIdentityWcdma = new CellIdentityWcdma(ciw.cellIdentityWcdma);
-        mCellSignalStrengthWcdma = new CellSignalStrengthWcdma(ciw.signalStrengthWcdma);
-    }
-
-    /** @hide */
-    public CellInfoWcdma(android.hardware.radio.V1_4.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_2.CellInfoWcdma ciw = ci.info.wcdma();
-        mCellIdentityWcdma = new CellIdentityWcdma(ciw.cellIdentityWcdma);
-        mCellSignalStrengthWcdma = new CellSignalStrengthWcdma(ciw.signalStrengthWcdma);
-    }
-
-    /** @hide */
-    public CellInfoWcdma(android.hardware.radio.V1_5.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_5.CellInfoWcdma ciw = ci.ratSpecificInfo.wcdma();
-        mCellIdentityWcdma = new CellIdentityWcdma(ciw.cellIdentityWcdma);
-        mCellSignalStrengthWcdma = new CellSignalStrengthWcdma(ciw.signalStrengthWcdma);
-    }
-
-    /** @hide */
-    public CellInfoWcdma(android.hardware.radio.V1_6.CellInfo ci, long timeStamp) {
-        super(ci, timeStamp);
-        final android.hardware.radio.V1_5.CellInfoWcdma ciw = ci.ratSpecificInfo.wcdma();
-        mCellIdentityWcdma = new CellIdentityWcdma(ciw.cellIdentityWcdma);
-        mCellSignalStrengthWcdma = new CellSignalStrengthWcdma(ciw.signalStrengthWcdma);
+    public CellInfoWcdma(int connectionStatus, boolean registered, long timeStamp,
+            CellIdentityWcdma cellIdentityWcdma, CellSignalStrengthWcdma cellSignalStrengthWcdma) {
+        super(connectionStatus, registered, timeStamp);
+        mCellIdentityWcdma = cellIdentityWcdma;
+        mCellSignalStrengthWcdma = cellSignalStrengthWcdma;
     }
 
     /**
diff --git a/telephony/java/android/telephony/CellSignalStrength.java b/telephony/java/android/telephony/CellSignalStrength.java
index e089657..9727ab7 100644
--- a/telephony/java/android/telephony/CellSignalStrength.java
+++ b/telephony/java/android/telephony/CellSignalStrength.java
@@ -108,7 +108,7 @@
 
     // Range for RSSI in ASU (0-31, 99) as defined in TS 27.007 8.69
     /** @hide */
-    protected static final int getRssiDbmFromAsu(int asu) {
+    public static final int getRssiDbmFromAsu(int asu) {
         if (asu > 31 || asu < 0) return CellInfo.UNAVAILABLE;
         return -113 + (2 * asu);
     }
@@ -122,7 +122,7 @@
 
     // Range for RSCP in ASU (0-96, 255) as defined in TS 27.007 8.69
     /** @hide */
-    protected static final int getRscpDbmFromAsu(int asu) {
+    public static final int getRscpDbmFromAsu(int asu) {
         if (asu > 96 || asu < 0) return CellInfo.UNAVAILABLE;
         return asu - 120;
     }
@@ -136,7 +136,7 @@
 
     // Range for SNR in ASU (0-49, 255) as defined in TS 27.007 8.69
     /** @hide */
-    protected static final int getEcNoDbFromAsu(int asu) {
+    public static final int getEcNoDbFromAsu(int asu) {
         if (asu > 49 || asu < 0) return CellInfo.UNAVAILABLE;
         return -24 + (asu / 2);
     }
diff --git a/telephony/java/android/telephony/CellSignalStrengthCdma.java b/telephony/java/android/telephony/CellSignalStrengthCdma.java
index d00049c..5298e67 100644
--- a/telephony/java/android/telephony/CellSignalStrengthCdma.java
+++ b/telephony/java/android/telephony/CellSignalStrengthCdma.java
@@ -78,13 +78,6 @@
     }
 
     /** @hide */
-    public CellSignalStrengthCdma(android.hardware.radio.V1_0.CdmaSignalStrength cdma,
-            android.hardware.radio.V1_0.EvdoSignalStrength evdo) {
-        // Convert from HAL values as part of construction.
-        this(-cdma.dbm, -cdma.ecio, -evdo.dbm, -evdo.ecio, evdo.signalNoiseRatio);
-    }
-
-    /** @hide */
     public CellSignalStrengthCdma(CellSignalStrengthCdma s) {
         copyFrom(s);
     }
diff --git a/telephony/java/android/telephony/CellSignalStrengthGsm.java b/telephony/java/android/telephony/CellSignalStrengthGsm.java
index 51e1ebc..7b78084 100644
--- a/telephony/java/android/telephony/CellSignalStrengthGsm.java
+++ b/telephony/java/android/telephony/CellSignalStrengthGsm.java
@@ -67,16 +67,6 @@
     }
 
     /** @hide */
-    public CellSignalStrengthGsm(android.hardware.radio.V1_0.GsmSignalStrength gsm) {
-        // Convert from HAL values as part of construction.
-        this(getRssiDbmFromAsu(gsm.signalStrength), gsm.bitErrorRate, gsm.timingAdvance);
-
-        if (mRssi == CellInfo.UNAVAILABLE) {
-            setDefaultValues();
-        }
-    }
-
-    /** @hide */
     public CellSignalStrengthGsm(CellSignalStrengthGsm s) {
         copyFrom(s);
     }
diff --git a/telephony/java/android/telephony/CellSignalStrengthLte.java b/telephony/java/android/telephony/CellSignalStrengthLte.java
index 9211482..e8633dd 100644
--- a/telephony/java/android/telephony/CellSignalStrengthLte.java
+++ b/telephony/java/android/telephony/CellSignalStrengthLte.java
@@ -166,25 +166,6 @@
     }
 
     /** @hide */
-    public CellSignalStrengthLte(android.hardware.radio.V1_0.LteSignalStrength lte) {
-        // Convert from HAL values as part of construction.
-        this(convertRssiAsuToDBm(lte.signalStrength),
-                lte.rsrp != CellInfo.UNAVAILABLE ? -lte.rsrp : lte.rsrp,
-                lte.rsrq != CellInfo.UNAVAILABLE ? -lte.rsrq : lte.rsrq,
-                convertRssnrUnitFromTenDbToDB(lte.rssnr), lte.cqi, lte.timingAdvance);
-    }
-
-    /** @hide */
-    public CellSignalStrengthLte(android.hardware.radio.V1_6.LteSignalStrength lte) {
-        // Convert from HAL values as part of construction.
-        this(convertRssiAsuToDBm(lte.base.signalStrength),
-                lte.base.rsrp != CellInfo.UNAVAILABLE ? -lte.base.rsrp : lte.base.rsrp,
-                lte.base.rsrq != CellInfo.UNAVAILABLE ? -lte.base.rsrq : lte.base.rsrq,
-                convertRssnrUnitFromTenDbToDB(lte.base.rssnr), lte.cqiTableIndex, lte.base.cqi,
-                lte.base.timingAdvance);
-    }
-
-    /** @hide */
     public CellSignalStrengthLte(CellSignalStrengthLte s) {
         copyFrom(s);
     }
@@ -617,11 +598,13 @@
         Rlog.w(LOG_TAG, s);
     }
 
-    private static int convertRssnrUnitFromTenDbToDB(int rssnr) {
+    /** @hide */
+    public static int convertRssnrUnitFromTenDbToDB(int rssnr) {
         return rssnr / 10;
     }
 
-    private static int convertRssiAsuToDBm(int rssiAsu) {
+    /** @hide */
+    public static int convertRssiAsuToDBm(int rssiAsu) {
         if (rssiAsu == SIGNAL_STRENGTH_LTE_RSSI_ASU_UNKNOWN) {
             return CellInfo.UNAVAILABLE;
         }
diff --git a/telephony/java/android/telephony/CellSignalStrengthNr.java b/telephony/java/android/telephony/CellSignalStrengthNr.java
index 6ada32e..cd22abd 100644
--- a/telephony/java/android/telephony/CellSignalStrengthNr.java
+++ b/telephony/java/android/telephony/CellSignalStrengthNr.java
@@ -202,29 +202,12 @@
     }
 
     /**
-     * @hide
-     * @param ss signal strength from modem.
-     */
-    public CellSignalStrengthNr(android.hardware.radio.V1_4.NrSignalStrength ss) {
-        this(flip(ss.csiRsrp), flip(ss.csiRsrq), ss.csiSinr, flip(ss.ssRsrp), flip(ss.ssRsrq),
-                ss.ssSinr);
-    }
-
-    /**
-     * @hide
-     * @param ss signal strength from modem.
-     */
-    public CellSignalStrengthNr(android.hardware.radio.V1_6.NrSignalStrength ss) {
-        this(flip(ss.base.csiRsrp), flip(ss.base.csiRsrq), ss.base.csiSinr, ss.csiCqiTableIndex,
-                ss.csiCqiReport, flip(ss.base.ssRsrp), flip(ss.base.ssRsrq), ss.base.ssSinr);
-    }
-
-    /**
      * Flip sign cell strength value when taking in the value from hal
      * @param val cell strength value
      * @return flipped value
+     * @hide
      */
-    private static int flip(int val) {
+    public static int flip(int val) {
         return val != CellInfo.UNAVAILABLE ? -val : val;
     }
 
diff --git a/telephony/java/android/telephony/CellSignalStrengthTdscdma.java b/telephony/java/android/telephony/CellSignalStrengthTdscdma.java
index e96f200..8a7c70e 100644
--- a/telephony/java/android/telephony/CellSignalStrengthTdscdma.java
+++ b/telephony/java/android/telephony/CellSignalStrengthTdscdma.java
@@ -75,28 +75,6 @@
     }
 
     /** @hide */
-    public CellSignalStrengthTdscdma(android.hardware.radio.V1_0.TdScdmaSignalStrength tdscdma) {
-        // Convert from HAL values as part of construction.
-        this(CellInfo.UNAVAILABLE, CellInfo.UNAVAILABLE,
-                tdscdma.rscp != CellInfo.UNAVAILABLE ? -tdscdma.rscp : tdscdma.rscp);
-
-        if (mRssi == CellInfo.UNAVAILABLE && mRscp == CellInfo.UNAVAILABLE) {
-            setDefaultValues();
-        }
-    }
-
-    /** @hide */
-    public CellSignalStrengthTdscdma(android.hardware.radio.V1_2.TdscdmaSignalStrength tdscdma) {
-        // Convert from HAL values as part of construction.
-        this(getRssiDbmFromAsu(tdscdma.signalStrength),
-                tdscdma.bitErrorRate, getRscpDbmFromAsu(tdscdma.rscp));
-
-        if (mRssi == CellInfo.UNAVAILABLE && mRscp == CellInfo.UNAVAILABLE) {
-            setDefaultValues();
-        }
-    }
-
-    /** @hide */
     public CellSignalStrengthTdscdma(CellSignalStrengthTdscdma s) {
         copyFrom(s);
     }
diff --git a/telephony/java/android/telephony/CellSignalStrengthWcdma.java b/telephony/java/android/telephony/CellSignalStrengthWcdma.java
index 8b14b74..f30440d 100644
--- a/telephony/java/android/telephony/CellSignalStrengthWcdma.java
+++ b/telephony/java/android/telephony/CellSignalStrengthWcdma.java
@@ -95,30 +95,6 @@
     }
 
     /** @hide */
-    public CellSignalStrengthWcdma(android.hardware.radio.V1_0.WcdmaSignalStrength wcdma) {
-        // Convert from HAL values as part of construction.
-        this(getRssiDbmFromAsu(wcdma.signalStrength), wcdma.bitErrorRate,
-                CellInfo.UNAVAILABLE, CellInfo.UNAVAILABLE);
-
-        if (mRssi == CellInfo.UNAVAILABLE && mRscp == CellInfo.UNAVAILABLE) {
-            setDefaultValues();
-        }
-    }
-
-    /** @hide */
-    public CellSignalStrengthWcdma(android.hardware.radio.V1_2.WcdmaSignalStrength wcdma) {
-        // Convert from HAL values as part of construction.
-        this(getRssiDbmFromAsu(wcdma.base.signalStrength),
-                    wcdma.base.bitErrorRate,
-                    getRscpDbmFromAsu(wcdma.rscp),
-                    getEcNoDbFromAsu(wcdma.ecno));
-
-        if (mRssi == CellInfo.UNAVAILABLE && mRscp == CellInfo.UNAVAILABLE) {
-            setDefaultValues();
-        }
-    }
-
-    /** @hide */
     public CellSignalStrengthWcdma(CellSignalStrengthWcdma s) {
         copyFrom(s);
     }
diff --git a/telephony/java/android/telephony/ClosedSubscriberGroupInfo.java b/telephony/java/android/telephony/ClosedSubscriberGroupInfo.java
index e926272..bf418ab 100644
--- a/telephony/java/android/telephony/ClosedSubscriberGroupInfo.java
+++ b/telephony/java/android/telephony/ClosedSubscriberGroupInfo.java
@@ -44,12 +44,6 @@
         mCsgIdentity = csgIdentity;
     }
 
-    /** @hide */
-    public ClosedSubscriberGroupInfo(
-            @NonNull android.hardware.radio.V1_5.ClosedSubscriberGroupInfo csgInfo) {
-        this(csgInfo.csgIndication, csgInfo.homeNodebName, csgInfo.csgIdentity);
-    }
-
     /**
      * Indicates whether the cell is restricted to only CSG members.
      *
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index 484eb1f..5affb62 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -612,7 +612,7 @@
     /**
      * Get the channel number of the current primary serving cell, or -1 if unknown
      *
-     * <p>This is EARFCN for LTE, UARFCN for UMTS, and ARFCN for GSM.
+     * <p>This is NRARFCN for NR, EARFCN for LTE, UARFCN for UMTS, and ARFCN for GSM.
      *
      * @return Channel number of primary serving cell
      */
diff --git a/telephony/java/android/telephony/SignalStrength.java b/telephony/java/android/telephony/SignalStrength.java
index b317c55..b7bc467 100644
--- a/telephony/java/android/telephony/SignalStrength.java
+++ b/telephony/java/android/telephony/SignalStrength.java
@@ -144,64 +144,6 @@
         mTimestampMillis = SystemClock.elapsedRealtime();
     }
 
-    /**
-     * Constructor for Radio HAL V1.0
-     *
-     * @hide
-     */
-    public SignalStrength(android.hardware.radio.V1_0.SignalStrength signalStrength) {
-        this(new CellSignalStrengthCdma(signalStrength.cdma, signalStrength.evdo),
-                new CellSignalStrengthGsm(signalStrength.gw),
-                new CellSignalStrengthWcdma(),
-                new CellSignalStrengthTdscdma(signalStrength.tdScdma),
-                new CellSignalStrengthLte(signalStrength.lte),
-                new CellSignalStrengthNr());
-    }
-
-    /**
-     * Constructor for Radio HAL V1.2
-     *
-     * @hide
-     */
-    public SignalStrength(android.hardware.radio.V1_2.SignalStrength signalStrength) {
-        this(new CellSignalStrengthCdma(signalStrength.cdma, signalStrength.evdo),
-                new CellSignalStrengthGsm(signalStrength.gsm),
-                new CellSignalStrengthWcdma(signalStrength.wcdma),
-                new CellSignalStrengthTdscdma(signalStrength.tdScdma),
-                new CellSignalStrengthLte(signalStrength.lte),
-                new CellSignalStrengthNr());
-    }
-
-    /**
-     * Constructor for Radio HAL V1.4.
-     *
-     * @param signalStrength signal strength reported from modem.
-     * @hide
-     */
-    public SignalStrength(android.hardware.radio.V1_4.SignalStrength signalStrength) {
-        this(new CellSignalStrengthCdma(signalStrength.cdma, signalStrength.evdo),
-                new CellSignalStrengthGsm(signalStrength.gsm),
-                new CellSignalStrengthWcdma(signalStrength.wcdma),
-                new CellSignalStrengthTdscdma(signalStrength.tdscdma),
-                new CellSignalStrengthLte(signalStrength.lte),
-                new CellSignalStrengthNr(signalStrength.nr));
-    }
-
-    /**
-     * Constructor for Radio HAL V1.6.
-     *
-     * @param signalStrength signal strength reported from modem.
-     * @hide
-     */
-    public SignalStrength(android.hardware.radio.V1_6.SignalStrength signalStrength) {
-        this(new CellSignalStrengthCdma(signalStrength.cdma, signalStrength.evdo),
-                new CellSignalStrengthGsm(signalStrength.gsm),
-                new CellSignalStrengthWcdma(signalStrength.wcdma),
-                new CellSignalStrengthTdscdma(signalStrength.tdscdma),
-                new CellSignalStrengthLte(signalStrength.lte),
-                new CellSignalStrengthNr(signalStrength.nr));
-    }
-
     private CellSignalStrength getPrimary() {
         // This behavior is intended to replicate the legacy behavior of getLevel() by prioritizing
         // newer faster RATs for default/for display purposes.
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index e251322..a4afea1 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -315,6 +315,12 @@
      */
     public static final int UNINITIALIZED_CARD_ID = -2;
 
+    /**
+     * Default port index for the UICC Card
+     * @hide
+     */
+    public static final int DEFAULT_PORT_INDEX = 0;
+
     private final Context mContext;
     private final int mSubId;
     @UnsupportedAppUsage
diff --git a/telephony/java/android/telephony/data/EpsQos.java b/telephony/java/android/telephony/data/EpsQos.java
index 22c8b0a..64c956f 100644
--- a/telephony/java/android/telephony/data/EpsQos.java
+++ b/telephony/java/android/telephony/data/EpsQos.java
@@ -32,15 +32,9 @@
 
     int qosClassId;
 
-    public EpsQos() {
-        super(Qos.QOS_TYPE_EPS,
-                new android.hardware.radio.V1_6.QosBandwidth(),
-                new android.hardware.radio.V1_6.QosBandwidth());
-    }
-
-    public EpsQos(@NonNull android.hardware.radio.V1_6.EpsQos qos) {
-        super(Qos.QOS_TYPE_EPS, qos.downlink, qos.uplink);
-        qosClassId = qos.qci;
+    public EpsQos(QosBandwidth downlink, QosBandwidth uplink, int qosClassId) {
+        super(Qos.QOS_TYPE_EPS, downlink, uplink);
+        this.qosClassId = qosClassId;
     }
 
     private EpsQos(Parcel source) {
diff --git a/telephony/java/android/telephony/data/NetworkSlicingConfig.java b/telephony/java/android/telephony/data/NetworkSlicingConfig.java
index dec787f..024a3f5 100644
--- a/telephony/java/android/telephony/data/NetworkSlicingConfig.java
+++ b/telephony/java/android/telephony/data/NetworkSlicingConfig.java
@@ -33,39 +33,15 @@
     private final List<NetworkSliceInfo> mSliceInfo;
 
     public NetworkSlicingConfig() {
-        mUrspRules = new ArrayList<UrspRule>();
-        mSliceInfo = new ArrayList<NetworkSliceInfo>();
+        mUrspRules = new ArrayList<>();
+        mSliceInfo = new ArrayList<>();
     }
 
     /** @hide */
-    public NetworkSlicingConfig(android.hardware.radio.V1_6.SlicingConfig sc) {
-        this(sc.urspRules, sc.sliceInfo);
-    }
-
-    /** @hide */
-    public NetworkSlicingConfig(List<android.hardware.radio.V1_6.UrspRule> urspRules,
-            List<android.hardware.radio.V1_6.SliceInfo> sliceInfo) {
-        mUrspRules = new ArrayList<UrspRule>();
-        for (android.hardware.radio.V1_6.UrspRule ur : urspRules) {
-            mUrspRules.add(new UrspRule(ur.precedence, ur.trafficDescriptors,
-                    ur.routeSelectionDescriptor));
-        }
-        mSliceInfo = new ArrayList<NetworkSliceInfo>();
-        for (android.hardware.radio.V1_6.SliceInfo si : sliceInfo) {
-            mSliceInfo.add(sliceInfoBuilder(si));
-        }
-    }
-
-    private NetworkSliceInfo sliceInfoBuilder(android.hardware.radio.V1_6.SliceInfo si) {
-        NetworkSliceInfo.Builder builder = new NetworkSliceInfo.Builder()
-                .setSliceServiceType(si.sst)
-                .setMappedHplmnSliceServiceType(si.mappedHplmnSst);
-        if (si.sliceDifferentiator != NetworkSliceInfo.SLICE_DIFFERENTIATOR_NO_SLICE) {
-            builder
-                .setSliceDifferentiator(si.sliceDifferentiator)
-                .setMappedHplmnSliceDifferentiator(si.mappedHplmnSD);
-        }
-        return builder.build();
+    public NetworkSlicingConfig(List<UrspRule> urspRules, List<NetworkSliceInfo> sliceInfo) {
+        this();
+        mUrspRules.addAll(urspRules);
+        mSliceInfo.addAll(sliceInfo);
     }
 
     /** @hide */
diff --git a/telephony/java/android/telephony/data/NrQos.java b/telephony/java/android/telephony/data/NrQos.java
index fe124ac..a24d5fa 100644
--- a/telephony/java/android/telephony/data/NrQos.java
+++ b/telephony/java/android/telephony/data/NrQos.java
@@ -32,11 +32,12 @@
     int fiveQi;
     int averagingWindowMs;
 
-    public NrQos(@NonNull android.hardware.radio.V1_6.NrQos qos) {
-        super(Qos.QOS_TYPE_NR, qos.downlink, qos.uplink);
-        fiveQi = qos.fiveQi;
-        qosFlowId = qos.qfi;
-        averagingWindowMs = qos.averagingWindowMs;
+    public NrQos(QosBandwidth downlink, QosBandwidth uplink, int qosFlowId, int fiveQi,
+            int averagingWindowMs) {
+        super(Qos.QOS_TYPE_NR, downlink, uplink);
+        this.qosFlowId = qosFlowId;
+        this.fiveQi = fiveQi;
+        this.averagingWindowMs = averagingWindowMs;
     }
 
     private NrQos(Parcel source) {
diff --git a/telephony/java/android/telephony/data/Qos.java b/telephony/java/android/telephony/data/Qos.java
index c286c621..8c437c8 100644
--- a/telephony/java/android/telephony/data/Qos.java
+++ b/telephony/java/android/telephony/data/Qos.java
@@ -48,12 +48,10 @@
     final QosBandwidth downlink;
     final QosBandwidth uplink;
 
-    Qos(int type,
-            @NonNull android.hardware.radio.V1_6.QosBandwidth downlink,
-            @NonNull android.hardware.radio.V1_6.QosBandwidth uplink) {
+    Qos(int type, QosBandwidth downlink, QosBandwidth uplink) {
         this.type = type;
-        this.downlink = new QosBandwidth(downlink.maxBitrateKbps, downlink.guaranteedBitrateKbps);
-        this.uplink = new QosBandwidth(uplink.maxBitrateKbps, uplink.guaranteedBitrateKbps);
+        this.downlink = downlink;
+        this.uplink = uplink;
     }
 
     public QosBandwidth getDownlinkBandwidth() {
@@ -68,10 +66,7 @@
         int maxBitrateKbps;
         int guaranteedBitrateKbps;
 
-        QosBandwidth() {
-        }
-
-        QosBandwidth(int maxBitrateKbps, int guaranteedBitrateKbps) {
+        public QosBandwidth(int maxBitrateKbps, int guaranteedBitrateKbps) {
             this.maxBitrateKbps = maxBitrateKbps;
             this.guaranteedBitrateKbps = guaranteedBitrateKbps;
         }
@@ -158,18 +153,6 @@
     }
 
     /** @hide */
-    public static @NonNull Qos create(@NonNull android.hardware.radio.V1_6.Qos qos) {
-        switch (qos.getDiscriminator()) {
-            case android.hardware.radio.V1_6.Qos.hidl_discriminator.eps:
-                  return new EpsQos(qos.eps());
-            case android.hardware.radio.V1_6.Qos.hidl_discriminator.nr:
-                  return new NrQos(qos.nr());
-            default:
-                  return null;
-        }
-    }
-
-    /** @hide */
     public @QosType int getType() {
         return type;
     }
diff --git a/telephony/java/android/telephony/data/QosBearerFilter.java b/telephony/java/android/telephony/data/QosBearerFilter.java
index 54930d0..5f5762b 100644
--- a/telephony/java/android/telephony/data/QosBearerFilter.java
+++ b/telephony/java/android/telephony/data/QosBearerFilter.java
@@ -18,15 +18,12 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
-import android.net.InetAddresses;
 import android.net.LinkAddress;
 import android.os.Parcel;
 import android.os.Parcelable;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.net.InetAddress;
-import java.net.Inet4Address;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -97,20 +94,13 @@
      */
     private int precedence;
 
-    QosBearerFilter() {
-        localAddresses = new ArrayList<>();
-        remoteAddresses = new ArrayList<>();
-        localPort = new PortRange();
-        remotePort = new PortRange();
-        protocol = QOS_PROTOCOL_UNSPECIFIED;
-        filterDirection = QOS_FILTER_DIRECTION_BIDIRECTIONAL;
-    }
-
     public QosBearerFilter(List<LinkAddress> localAddresses, List<LinkAddress> remoteAddresses,
             PortRange localPort, PortRange remotePort, int protocol, int tos,
             long flowLabel, long spi, int direction, int precedence) {
-        this.localAddresses = localAddresses;
-        this.remoteAddresses = remoteAddresses;
+        this.localAddresses = new ArrayList<>();
+        this.localAddresses.addAll(localAddresses);
+        this.remoteAddresses = new ArrayList<>();
+        this.remoteAddresses.addAll(remoteAddresses);
         this.localPort = localPort;
         this.remotePort = remotePort;
         this.protocol = protocol;
@@ -141,82 +131,10 @@
         return precedence;
     }
 
-    /** @hide */
-    public static @NonNull QosBearerFilter create(
-            @NonNull android.hardware.radio.V1_6.QosFilter qosFilter) {
-        QosBearerFilter ret = new QosBearerFilter();
-
-        String[] localAddresses = qosFilter.localAddresses.stream().toArray(String[]::new);
-        if (localAddresses != null) {
-            for (String address : localAddresses) {
-                ret.localAddresses.add(createLinkAddressFromString(address));
-            }
-        }
-
-        String[] remoteAddresses = qosFilter.remoteAddresses.stream().toArray(String[]::new);
-        if (remoteAddresses != null) {
-            for (String address : remoteAddresses) {
-                ret.remoteAddresses.add(createLinkAddressFromString(address));
-            }
-        }
-
-        if (qosFilter.localPort != null) {
-            if (qosFilter.localPort.getDiscriminator()
-                    == android.hardware.radio.V1_6.MaybePort.hidl_discriminator.range) {
-                final android.hardware.radio.V1_6.PortRange portRange = qosFilter.localPort.range();
-                ret.localPort.start = portRange.start;
-                ret.localPort.end = portRange.end;
-            }
-        }
-
-        if (qosFilter.remotePort != null) {
-            if (qosFilter.remotePort.getDiscriminator()
-                    == android.hardware.radio.V1_6.MaybePort.hidl_discriminator.range) {
-                final android.hardware.radio.V1_6.PortRange portRange
-                        = qosFilter.remotePort.range();
-                ret.remotePort.start = portRange.start;
-                ret.remotePort.end = portRange.end;
-            }
-        }
-
-        ret.protocol = qosFilter.protocol;
-
-        if (qosFilter.tos != null) {
-            if (qosFilter.tos.getDiscriminator()
-                == android.hardware.radio.V1_6.QosFilter.TypeOfService.hidl_discriminator.value) {
-                ret.typeOfServiceMask = qosFilter.tos.value();
-            }
-        }
-
-        if (qosFilter.flowLabel != null) {
-            if (qosFilter.flowLabel.getDiscriminator()
-                == android.hardware.radio.V1_6.QosFilter.Ipv6FlowLabel.hidl_discriminator.value) {
-                ret.flowLabel = qosFilter.flowLabel.value();
-            }
-        }
-
-        if (qosFilter.spi != null) {
-            if (qosFilter.spi.getDiscriminator()
-                == android.hardware.radio.V1_6.QosFilter.IpsecSpi.hidl_discriminator.value) {
-                ret.securityParameterIndex = qosFilter.spi.value();
-            }
-        }
-
-        ret.filterDirection = qosFilter.direction;
-        ret.precedence = qosFilter.precedence;
-
-        return ret;
-    }
-
     public static class PortRange implements Parcelable {
         int start;
         int end;
 
-        PortRange() {
-            start = -1;
-            end = -1;
-        }
-
         private PortRange(Parcel source) {
             start = source.readInt();
             end = source.readInt();
@@ -337,32 +255,6 @@
                 && precedence == other.precedence;
     }
 
-    private static LinkAddress createLinkAddressFromString(String addressString) {
-        addressString = addressString.trim();
-        InetAddress address = null;
-        int prefixLength = -1;
-        try {
-            String[] pieces = addressString.split("/", 2);
-            address = InetAddresses.parseNumericAddress(pieces[0]);
-            if (pieces.length == 1) {
-                prefixLength = (address instanceof Inet4Address) ? 32 : 128;
-            } else if (pieces.length == 2) {
-                prefixLength = Integer.parseInt(pieces[1]);
-            }
-        } catch (NullPointerException e) {            // Null string.
-        } catch (ArrayIndexOutOfBoundsException e) {  // No prefix length.
-        } catch (NumberFormatException e) {           // Non-numeric prefix.
-        } catch (IllegalArgumentException e) {        // Invalid IP address.
-        }
-
-        if (address == null || prefixLength == -1) {
-            throw new IllegalArgumentException("Invalid link address " + addressString);
-        }
-
-        return new LinkAddress(address, prefixLength, 0, 0,
-                LinkAddress.LIFETIME_UNKNOWN, LinkAddress.LIFETIME_UNKNOWN);
-    }
-
     private QosBearerFilter(Parcel source) {
         localAddresses = new ArrayList<>();
         source.readList(localAddresses, LinkAddress.class.getClassLoader());
diff --git a/telephony/java/android/telephony/data/QosBearerSession.java b/telephony/java/android/telephony/data/QosBearerSession.java
index 30effc9..ffeb08a 100644
--- a/telephony/java/android/telephony/data/QosBearerSession.java
+++ b/telephony/java/android/telephony/data/QosBearerSession.java
@@ -36,10 +36,12 @@
     final Qos qos;
     final List<QosBearerFilter> qosBearerFilterList;
 
-    public QosBearerSession(int qosBearerSessionId, @NonNull Qos qos, @NonNull List<QosBearerFilter> qosBearerFilterList) {
+    public QosBearerSession(int qosBearerSessionId, @NonNull Qos qos,
+            @NonNull List<QosBearerFilter> qosBearerFilterList) {
         this.qosBearerSessionId = qosBearerSessionId;
         this.qos = qos;
-        this.qosBearerFilterList = qosBearerFilterList;
+        this.qosBearerFilterList = new ArrayList<>();
+        this.qosBearerFilterList.addAll(qosBearerFilterList);
     }
 
     private QosBearerSession(Parcel source) {
@@ -72,22 +74,6 @@
         dest.writeList(qosBearerFilterList);
     }
 
-    public static @NonNull QosBearerSession create(
-            @NonNull android.hardware.radio.V1_6.QosSession qosSession) {
-        List<QosBearerFilter> qosBearerFilters = new ArrayList<>();
-
-        if (qosSession.qosFilters != null) {
-            for (android.hardware.radio.V1_6.QosFilter filter : qosSession.qosFilters) {
-                qosBearerFilters.add(QosBearerFilter.create(filter));
-            }
-        }
-
-        return new QosBearerSession(
-                        qosSession.qosSessionId,
-                        Qos.create(qosSession.qos),
-                        qosBearerFilters);
-    }
-
     @Override
     public int describeContents() {
         return 0;
diff --git a/telephony/java/android/telephony/data/RouteSelectionDescriptor.java b/telephony/java/android/telephony/data/RouteSelectionDescriptor.java
index c2457f2..b3b8464 100644
--- a/telephony/java/android/telephony/data/RouteSelectionDescriptor.java
+++ b/telephony/java/android/telephony/data/RouteSelectionDescriptor.java
@@ -120,43 +120,23 @@
     private final List<String> mDnn;
 
     /** @hide */
-    RouteSelectionDescriptor(android.hardware.radio.V1_6.RouteSelectionDescriptor rsd) {
-        this(rsd.precedence, rsd.sessionType.value(), rsd.sscMode.value(), rsd.sliceInfo,
-                rsd.dnn);
-    }
-
-    /** @hide */
     public RouteSelectionDescriptor(int precedence, int sessionType, int sscMode,
-            List<android.hardware.radio.V1_6.SliceInfo> sliceInfo, List<String> dnn) {
+            List<NetworkSliceInfo> sliceInfo, List<String> dnn) {
         mPrecedence = precedence;
         mSessionType = sessionType;
         mSscMode = sscMode;
-        mSliceInfo = new ArrayList<NetworkSliceInfo>();
-        for (android.hardware.radio.V1_6.SliceInfo si : sliceInfo) {
-            mSliceInfo.add(sliceInfoBuilder(si));
-        }
-        mDnn = new ArrayList<String>();
+        mSliceInfo = new ArrayList<>();
+        mSliceInfo.addAll(sliceInfo);
+        mDnn = new ArrayList<>();
         mDnn.addAll(dnn);
     }
 
-    private NetworkSliceInfo sliceInfoBuilder(android.hardware.radio.V1_6.SliceInfo si) {
-        NetworkSliceInfo.Builder builder = new NetworkSliceInfo.Builder()
-                .setSliceServiceType(si.sst)
-                .setMappedHplmnSliceServiceType(si.mappedHplmnSst);
-        if (si.sliceDifferentiator != NetworkSliceInfo.SLICE_DIFFERENTIATOR_NO_SLICE) {
-            builder
-                .setSliceDifferentiator(si.sliceDifferentiator)
-                .setMappedHplmnSliceDifferentiator(si.mappedHplmnSD);
-        }
-        return builder.build();
-    }
-
     private RouteSelectionDescriptor(Parcel p) {
         mPrecedence = p.readInt();
         mSessionType = p.readInt();
         mSscMode = p.readInt();
         mSliceInfo = p.createTypedArrayList(NetworkSliceInfo.CREATOR);
-        mDnn = new ArrayList<String>();
+        mDnn = new ArrayList<>();
         p.readStringList(mDnn);
     }
 
diff --git a/telephony/java/android/telephony/data/UrspRule.java b/telephony/java/android/telephony/data/UrspRule.java
index fbe1999..afbd429 100644
--- a/telephony/java/android/telephony/data/UrspRule.java
+++ b/telephony/java/android/telephony/data/UrspRule.java
@@ -19,12 +19,9 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.hardware.radio.V1_6.OptionalDnn;
-import android.hardware.radio.V1_6.OptionalOsAppId;
 import android.os.Parcel;
 import android.os.Parcelable;
 
-
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -52,48 +49,14 @@
     private final List<TrafficDescriptor> mTrafficDescriptors;
     private final List<RouteSelectionDescriptor> mRouteSelectionDescriptor;
 
-    UrspRule(android.hardware.radio.V1_6.UrspRule ur) {
-        this(ur.precedence, ur.trafficDescriptors, ur.routeSelectionDescriptor);
-    }
-
     /** @hide */
-    public UrspRule(int precedence,
-            List<android.hardware.radio.V1_6.TrafficDescriptor> trafficDescriptors,
-            List<android.hardware.radio.V1_6.RouteSelectionDescriptor> routeSelectionDescriptor) {
+    public UrspRule(int precedence, List<TrafficDescriptor> trafficDescriptors,
+            List<RouteSelectionDescriptor> routeSelectionDescriptor) {
         mPrecedence = precedence;
-        mTrafficDescriptors = new ArrayList<TrafficDescriptor>();
-        for (android.hardware.radio.V1_6.TrafficDescriptor td : trafficDescriptors) {
-            mTrafficDescriptors.add(convertToTrafficDescriptor(td));
-        }
-        mRouteSelectionDescriptor = new ArrayList<RouteSelectionDescriptor>();
-        for (android.hardware.radio.V1_6.RouteSelectionDescriptor rsd : routeSelectionDescriptor) {
-            mRouteSelectionDescriptor.add(new RouteSelectionDescriptor(rsd));
-        }
-    }
-
-    /** Convert an ArrayList of Bytes to an exactly-sized primitive array */
-    private byte[] arrayListToPrimitiveArray(ArrayList<Byte> bytes) {
-        byte[] ret = new byte[bytes.size()];
-        for (int i = 0; i < ret.length; i++) {
-            ret[i] = bytes.get(i);
-        }
-        return ret;
-    }
-
-    private TrafficDescriptor convertToTrafficDescriptor(
-            android.hardware.radio.V1_6.TrafficDescriptor td) {
-        String dnn = td.dnn.getDiscriminator() == OptionalDnn.hidl_discriminator.noinit
-                ? null : td.dnn.value();
-        byte[] osAppId = td.osAppId.getDiscriminator() == OptionalOsAppId.hidl_discriminator.noinit
-                ? null : arrayListToPrimitiveArray(td.osAppId.value().osAppId);
-        TrafficDescriptor.Builder builder = new TrafficDescriptor.Builder();
-        if (dnn != null) {
-            builder.setDataNetworkName(dnn);
-        }
-        if (osAppId != null) {
-            builder.setOsAppId(osAppId);
-        }
-        return builder.build();
+        mTrafficDescriptors = new ArrayList<>();
+        mTrafficDescriptors.addAll(trafficDescriptors);
+        mRouteSelectionDescriptor = new ArrayList<>();
+        mRouteSelectionDescriptor.addAll(routeSelectionDescriptor);
     }
 
     private UrspRule(Parcel p) {
diff --git a/telephony/java/com/android/internal/telephony/RILConstants.java b/telephony/java/com/android/internal/telephony/RILConstants.java
index fadc23b..61c269c 100644
--- a/telephony/java/com/android/internal/telephony/RILConstants.java
+++ b/telephony/java/com/android/internal/telephony/RILConstants.java
@@ -568,7 +568,7 @@
     int RIL_UNSOL_RINGBACK_TONE = 1029;
     int RIL_UNSOL_RESEND_INCALL_MUTE = 1030;
     int RIL_UNSOL_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 1031;
-    int RIL_UNSOl_CDMA_PRL_CHANGED = 1032;
+    int RIL_UNSOL_CDMA_PRL_CHANGED = 1032;
     int RIL_UNSOL_EXIT_EMERGENCY_CALLBACK_MODE = 1033;
     int RIL_UNSOL_RIL_CONNECTED = 1034;
     int RIL_UNSOL_VOICE_RADIO_TECH_CHANGED = 1035;
diff --git a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
index 2e4b6da..fe2fe0b 100644
--- a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
+++ b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
@@ -159,7 +159,7 @@
 
     private static BatteryUsageStats buildBatteryUsageStats() {
         final BatteryUsageStats.Builder builder =
-                new BatteryUsageStats.Builder(new String[]{"FOO"}, true)
+                new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false)
                         .setBatteryCapacity(4000)
                         .setDischargePercentage(20)
                         .setDischargedPowerRange(1000, 2000)
diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/MediaDecoder.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/MediaDecoder.java
index aa57394..00fe6f2 100644
--- a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/MediaDecoder.java
+++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/MediaDecoder.java
@@ -24,10 +24,12 @@
 import android.net.Uri;
 import android.os.Build;
 import android.util.Log;
+
 import androidx.media.filterfw.FrameImage2D;
 import androidx.media.filterfw.FrameValue;
 import androidx.media.filterfw.RenderTarget;
 
+import java.io.IOException;
 import java.util.concurrent.LinkedBlockingQueue;
 
 @TargetApi(16)
@@ -276,12 +278,13 @@
     }
 
     @TargetApi(17)
-    private void retrieveDefaultRotation() {
+    private void retrieveDefaultRotation() throws IOException {
         MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
         metadataRetriever.setDataSource(mContext, mUri);
         String rotationString = metadataRetriever.extractMetadata(
                 MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
         mDefaultRotation = rotationString == null ? 0 : Integer.parseInt(rotationString);
+        metadataRetriever.release();
     }
 
     private void onStop(boolean notifyListener) {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
index 511fc26..5aa1e27 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt
@@ -17,7 +17,9 @@
 package com.android.server.wm.flicker.close
 
 import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
+import androidx.test.filters.FlakyTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.wm.flicker.FlickerBuilderProvider
 import com.android.server.wm.flicker.FlickerTestParameter
@@ -35,6 +37,8 @@
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
 import com.android.server.wm.flicker.statusBarWindowIsVisible
 import com.android.server.wm.flicker.replacesLayer
+import com.android.server.wm.flicker.rules.WMFlickerServiceRuleForTestSpec
+import org.junit.Rule
 import org.junit.Test
 
 /**
@@ -44,6 +48,9 @@
     protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     protected open val testApp: StandardAppHelper = SimpleAppHelper(instrumentation)
 
+    @get:Rule
+    val flickerRule = WMFlickerServiceRuleForTestSpec(testSpec)
+
     /**
      * Specification of the test transition to execute
      */
@@ -189,4 +196,22 @@
     open fun launcherLayerReplacesApp() {
         testSpec.replacesLayer(testApp.component, LAUNCHER_COMPONENT)
     }
+
+    @Postsubmit
+    @Test
+    fun runPresubmitAssertion() {
+        flickerRule.checkPresubmitAssertions()
+    }
+
+    @Postsubmit
+    @Test
+    fun runPostsubmitAssertion() {
+        flickerRule.checkPostsubmitAssertions()
+    }
+
+    @FlakyTest
+    @Test
+    fun runFlakyAssertion() {
+        flickerRule.checkFlakyAssertions()
+    }
 }
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
index 3678f33..a05b78c 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
@@ -17,16 +17,19 @@
 package com.android.server.wm.flicker.launch
 
 import android.platform.test.annotations.Presubmit
+import android.view.Display
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.LAUNCHER_COMPONENT
 import com.android.server.wm.flicker.annotation.Group1
 import com.android.server.wm.flicker.helpers.reopenAppFromOverview
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.startRotation
 import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.traces.common.WindowManagerConditionsFactory
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -72,12 +75,23 @@
                     device.pressHome()
                     wmHelper.waitForAppTransitionIdle()
                     device.pressRecentApps()
-                    wmHelper.waitForAppTransitionIdle()
+                    wmHelper.waitFor(
+                        WindowManagerConditionsFactory
+                            .isAppTransitionIdle(Display.DEFAULT_DISPLAY),
+                        WindowManagerConditionsFactory.isActivityVisible(LAUNCHER_COMPONENT),
+                        WindowManagerConditionsFactory.hasLayersAnimating().negate()
+                    )
                     this.setRotation(testSpec.config.startRotation)
                 }
             }
             transitions {
                 device.reopenAppFromOverview(wmHelper)
+                wmHelper.waitFor(
+                    WindowManagerConditionsFactory.hasLayersAnimating().negate(),
+                    WindowManagerConditionsFactory.isWMStateComplete(),
+                    WindowManagerConditionsFactory.isLayerVisible(LAUNCHER_COMPONENT).negate(),
+                    WindowManagerConditionsFactory.isActivityVisible(LAUNCHER_COMPONENT).negate()
+                )
                 wmHelper.waitForFullScreenApp(testApp.component)
             }
         }
diff --git a/tests/InputMethodStressTest/AndroidManifest.xml b/tests/InputMethodStressTest/AndroidManifest.xml
index e5d6518..f5fe8f2 100644
--- a/tests/InputMethodStressTest/AndroidManifest.xml
+++ b/tests/InputMethodStressTest/AndroidManifest.xml
@@ -19,7 +19,8 @@
     package="com.android.inputmethod.stresstest">
 
     <application>
-        <activity android:name=".TestActivity"/>
+        <activity android:name=".AutoShowTest$TestActivity"/>
+        <activity android:name=".ImeOpenCloseStressTest$TestActivity"/>
     </application>
 
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java
new file mode 100644
index 0000000..33cad78
--- /dev/null
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.stresstest;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED;
+
+import static com.android.inputmethod.stresstest.ImeStressTestUtil.waitOnMainUntil;
+import static com.android.inputmethod.stresstest.ImeStressTestUtil.waitOnMainUntilImeIsShown;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Bundle;
+import android.platform.test.annotations.RootPermissionTest;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RootPermissionTest
+@RunWith(AndroidJUnit4.class)
+public final class AutoShowTest {
+
+    @Test
+    public void autoShow() {
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        Intent intent = new Intent()
+                .setAction(Intent.ACTION_MAIN)
+                .setClass(instrumentation.getContext(), TestActivity.class)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        TestActivity activity = (TestActivity) instrumentation.startActivitySync(intent);
+        EditText editText = activity.getEditText();
+        waitOnMainUntil("activity should gain focus", editText::hasWindowFocus);
+        waitOnMainUntilImeIsShown(editText);
+    }
+
+    public static class TestActivity extends Activity {
+        private EditText mEditText;
+
+        @Override
+        protected void onCreate(@Nullable Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            // IME will be auto-shown if the following conditions are met:
+            // 1. SoftInputMode state is SOFT_INPUT_STATE_UNSPECIFIED.
+            // 2. SoftInputMode adjust is SOFT_INPUT_ADJUST_RESIZE.
+            getWindow().setSoftInputMode(SOFT_INPUT_STATE_UNSPECIFIED | SOFT_INPUT_ADJUST_RESIZE);
+            LinearLayout rootView = new LinearLayout(this);
+            rootView.setOrientation(LinearLayout.VERTICAL);
+            mEditText = new EditText(this);
+            rootView.addView(mEditText, new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+            setContentView(rootView);
+            // 3. The focused view is a text editor (View#onCheckIsTextEditor() returns true).
+            mEditText.requestFocus();
+        }
+
+        public EditText getEditText() {
+            return mEditText;
+        }
+    }
+}
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java
index 5427fd8..0e86bc8 100644
--- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java
@@ -16,64 +16,81 @@
 
 package com.android.inputmethod.stresstest;
 
-import static com.android.compatibility.common.util.SystemUtil.eventually;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
-import static com.google.common.truth.Truth.assertThat;
+import static com.android.inputmethod.stresstest.ImeStressTestUtil.waitOnMainUntil;
+import static com.android.inputmethod.stresstest.ImeStressTestUtil.waitOnMainUntilImeIsHidden;
+import static com.android.inputmethod.stresstest.ImeStressTestUtil.waitOnMainUntilImeIsShown;
 
+import android.app.Activity;
 import android.app.Instrumentation;
 import android.content.Intent;
+import android.os.Bundle;
 import android.platform.test.annotations.RootPermissionTest;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.LinearLayout;
 
+import androidx.annotation.Nullable;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.concurrent.Callable;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
 @RootPermissionTest
 @RunWith(AndroidJUnit4.class)
-public class ImeOpenCloseStressTest {
+public final class ImeOpenCloseStressTest {
 
-    private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
     private static final int NUM_TEST_ITERATIONS = 100;
 
-    private Instrumentation mInstrumentation;
-
     @Test
     public void test() {
-        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
         Intent intent = new Intent()
                 .setAction(Intent.ACTION_MAIN)
-                .setClass(mInstrumentation.getContext(), TestActivity.class)
+                .setClass(instrumentation.getContext(), TestActivity.class)
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        TestActivity activity = (TestActivity) mInstrumentation.startActivitySync(intent);
-        eventually(() -> assertThat(callOnMainSync(activity::hasWindowFocus)).isTrue(), TIMEOUT);
+        TestActivity activity = (TestActivity) instrumentation.startActivitySync(intent);
+        EditText editText = activity.getEditText();
+        waitOnMainUntil("activity should gain focus", editText::hasWindowFocus);
         for (int i = 0; i < NUM_TEST_ITERATIONS; i++) {
-            mInstrumentation.runOnMainSync(activity::showIme);
-            eventually(() -> assertThat(callOnMainSync(activity::isImeShown)).isTrue(), TIMEOUT);
-            mInstrumentation.runOnMainSync(activity::hideIme);
-            eventually(() -> assertThat(callOnMainSync(activity::isImeShown)).isFalse(), TIMEOUT);
+            instrumentation.runOnMainSync(activity::showIme);
+            waitOnMainUntilImeIsShown(editText);
+            instrumentation.runOnMainSync(activity::hideIme);
+            waitOnMainUntilImeIsHidden(editText);
         }
     }
 
-    private <V> V callOnMainSync(Callable<V> callable) {
-        AtomicReference<V> result = new AtomicReference<>();
-        AtomicReference<Exception> thrownException = new AtomicReference<>();
-        mInstrumentation.runOnMainSync(() -> {
-            try {
-                result.set(callable.call());
-            } catch (Exception e) {
-                thrownException.set(e);
-            }
-        });
-        if (thrownException.get() != null) {
-            throw new RuntimeException("Exception thrown from Main thread", thrownException.get());
+    public static class TestActivity extends Activity {
+
+        private EditText mEditText;
+
+        @Override
+        protected void onCreate(@Nullable Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            LinearLayout rootView = new LinearLayout(this);
+            rootView.setOrientation(LinearLayout.VERTICAL);
+            mEditText = new EditText(this);
+            rootView.addView(mEditText, new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
+            setContentView(rootView);
         }
-        return result.get();
+
+        public EditText getEditText() {
+            return mEditText;
+        }
+
+        public void showIme() {
+            mEditText.requestFocus();
+            InputMethodManager imm = getSystemService(InputMethodManager.class);
+            imm.showSoftInput(mEditText, 0);
+        }
+
+        public void hideIme() {
+            InputMethodManager imm = getSystemService(InputMethodManager.class);
+            imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
+        }
     }
 }
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java
new file mode 100644
index 0000000..ba2ba3c
--- /dev/null
+++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestUtil.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.stresstest;
+
+import static com.android.compatibility.common.util.SystemUtil.eventually;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.view.View;
+import android.view.WindowInsets;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Utility methods for IME stress test. */
+public final class ImeStressTestUtil {
+
+    private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
+
+    private ImeStressTestUtil() {
+    }
+
+    /** Checks if the IME is shown on the window that the given view belongs to. */
+    public static boolean isImeShown(View view) {
+        WindowInsets insets = view.getRootWindowInsets();
+        return insets.isVisible(WindowInsets.Type.ime());
+    }
+
+    /** Calls the callable on the main thread and returns the result. */
+    public static <V> V callOnMainSync(Callable<V> callable) {
+        AtomicReference<V> result = new AtomicReference<>();
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            try {
+                result.set(callable.call());
+            } catch (Exception e) {
+                throw new RuntimeException("Exception was thrown", e);
+            }
+        });
+        return result.get();
+    }
+
+    /**
+     * Waits until {@code pred} returns true, or throws on timeout.
+     *
+     * <p>The given {@code pred} will be called on the main thread.
+     */
+    public static void waitOnMainUntil(String message, Callable<Boolean> pred) {
+        eventually(() -> assertWithMessage(message).that(pred.call()).isTrue(), TIMEOUT);
+    }
+
+    /** Waits until IME is shown, or throws on timeout. */
+    public static void waitOnMainUntilImeIsShown(View view) {
+        eventually(() -> assertWithMessage("IME should be shown").that(
+                callOnMainSync(() -> isImeShown(view))).isTrue(), TIMEOUT);
+    }
+
+    /** Waits until IME is hidden, or throws on timeout. */
+    public static void waitOnMainUntilImeIsHidden(View view) {
+        //eventually(() -> assertThat(callOnMainSync(() -> isImeShown(view))).isFalse(), TIMEOUT);
+        eventually(() -> assertWithMessage("IME should be hidden").that(
+                callOnMainSync(() -> isImeShown(view))).isFalse(), TIMEOUT);
+    }
+}
diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/TestActivity.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/TestActivity.java
deleted file mode 100644
index 7baf037..0000000
--- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/TestActivity.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.stresstest;
-
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.view.WindowInsets;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-
-import androidx.annotation.Nullable;
-
-public class TestActivity extends Activity {
-
-    private EditText mEditText;
-
-    @Override
-    protected void onCreate(@Nullable Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        LinearLayout rootView = new LinearLayout(this);
-        rootView.setOrientation(LinearLayout.VERTICAL);
-        mEditText = new EditText(this);
-        rootView.addView(mEditText, new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
-        setContentView(rootView);
-    }
-
-    public boolean hasWindowFocus() {
-        return mEditText.hasWindowFocus();
-    }
-
-    public boolean isImeShown() {
-        WindowInsets insets = mEditText.getRootWindowInsets();
-        return insets.isVisible(WindowInsets.Type.ime());
-    }
-
-    public void showIme() {
-        mEditText.requestFocus();
-        InputMethodManager imm = getSystemService(InputMethodManager.class);
-        imm.showSoftInput(mEditText, 0);
-    }
-
-    public void hideIme() {
-        InputMethodManager imm = getSystemService(InputMethodManager.class);
-        imm.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
-    }
-}
diff --git a/tests/RollbackTest/Android.bp b/tests/RollbackTest/Android.bp
index 6e1cef4..9f6ce4e 100644
--- a/tests/RollbackTest/Android.bp
+++ b/tests/RollbackTest/Android.bp
@@ -31,7 +31,8 @@
     test_config: "RollbackTest.xml",
     java_resources: [
         ":com.android.apex.apkrollback.test_v2",
-        ":com.android.apex.apkrollback.test_v2Crashing"
+        ":com.android.apex.apkrollback.test_v2Crashing",
+        ":test.rebootless_apex_v2",
     ],
 }
 
@@ -47,7 +48,10 @@
     ],
     test_suites: ["general-tests"],
     test_config: "StagedRollbackTest.xml",
-    data: [":com.android.apex.apkrollback.test_v1"],
+    data: [
+        ":com.android.apex.apkrollback.test_v1",
+        ":test.rebootless_apex_v1",
+    ],
 }
 
 java_test_host {
diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
index 539090b..4512876 100644
--- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
+++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
@@ -241,6 +241,33 @@
     }
 
     @Test
+    public void testRollbackRebootlessApex() throws Exception {
+        final String packageName = "test.apex.rebootless";
+        assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1);
+
+        // install
+        TestApp apex1 = new TestApp("TestRebootlessApexV1", packageName, 1,
+                /* isApex= */ true, "test.rebootless_apex_v1.apex");
+        TestApp apex2 = new TestApp("TestRebootlessApexV2", packageName, 2,
+                /* isApex= */ true, "test.rebootless_apex_v2.apex");
+        Install.single(apex2).setEnableRollback(PackageManager.ROLLBACK_DATA_POLICY_RETAIN)
+                .commit();
+
+        // verify rollback
+        assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(2);
+        RollbackManager rm = RollbackUtils.getRollbackManager();
+        RollbackInfo rollback = getUniqueRollbackInfoForPackage(
+                rm.getAvailableRollbacks(), packageName);
+        assertThat(rollback).isNotNull();
+        assertThat(rollback).packagesContainsExactly(Rollback.from(apex2).to(apex1));
+        assertThat(rollback).isNotStaged();
+
+        // rollback
+        RollbackUtils.rollback(rollback.getRollbackId());
+        assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1);
+    }
+
+    @Test
     public void hasMainlineModule() throws Exception {
         String pkgName = getModuleMetadataPackageName();
         boolean existed =  InstrumentationRegistry.getInstrumentation().getContext()
diff --git a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java
index 293f159..3576a78 100644
--- a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java
+++ b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java
@@ -96,7 +96,9 @@
         deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
                 "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
                 apexDataDirDeSys(APK_IN_APEX_TESTAPEX_NAME) + "*",
-                apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "*");
+                apexDataDirCe(APK_IN_APEX_TESTAPEX_NAME, 0) + "*",
+                "/system/apex/test.rebootless_apex_v*.apex",
+                "/data/apex/active/test.apex.rebootless*.apex");
     }
 
     /**
@@ -160,7 +162,7 @@
      */
     @Test
     public void testRollbackApexWithApkCrashing() throws Exception {
-        pushTestApex();
+        pushTestApex(APK_IN_APEX_TESTAPEX_NAME + "_v1.apex");
 
         // Install an apex with apk that crashes
         runPhase("testRollbackApexWithApkCrashing_Phase1_Install");
@@ -181,6 +183,15 @@
     }
 
     /**
+     * Tests rollback is supported correctly for rebootless apex
+     */
+    @Test
+    public void testRollbackRebootlessApex() throws Exception {
+        pushTestApex("test.rebootless_apex_v1.apex");
+        runPhase("testRollbackRebootlessApex");
+    }
+
+    /**
      * Tests that packages are monitored across multiple reboots.
      */
     @Test
@@ -204,9 +215,8 @@
         runPhase("testWatchdogMonitorsAcrossReboots_Phase3_VerifyRollback");
     }
 
-    private void pushTestApex() throws Exception {
+    private void pushTestApex(String fileName) throws Exception {
         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild());
-        final String fileName = APK_IN_APEX_TESTAPEX_NAME + "_v1.apex";
         final File apex = buildHelper.getTestFile(fileName);
         try {
             getDevice().enableAdbRoot();
diff --git a/tests/StagedInstallTest/Android.bp b/tests/StagedInstallTest/Android.bp
index 7a564fc..ae62bec 100644
--- a/tests/StagedInstallTest/Android.bp
+++ b/tests/StagedInstallTest/Android.bp
@@ -59,6 +59,7 @@
         ":StagedInstallTestApexV2_WrongSha",
         ":TestAppAv1",
         ":test.rebootless_apex_v1",
+        ":test.rebootless_apex_v2",
     ],
     test_suites: ["general-tests"],
     test_config: "StagedInstallInternalTest.xml",
diff --git a/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java b/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java
index c610641..6e24448 100644
--- a/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java
+++ b/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java
@@ -476,6 +476,18 @@
         assertThat(captor.getValue().stagedApexModuleNames).hasLength(0);
     }
 
+    @Test
+    public void testRebootlessDowngrade() throws Exception {
+        final String packageName = "test.apex.rebootless";
+        assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(2);
+        TestApp apex1 = new TestApp("TestRebootlessApexV1", packageName, 1,
+                /* isApex= */ true, "test.rebootless_apex_v1.apex");
+        InstallUtils.commitExpectingFailure(AssertionError.class,
+                "INSTALL_FAILED_VERSION_DOWNGRADE", Install.single(apex1));
+        Install.single(apex1).setRequestDowngrade().commit();
+        assertThat(InstallUtils.getInstalledVersion(packageName)).isEqualTo(1);
+    }
+
     private IPackageManagerNative getPackageManagerNative() {
         IBinder binder = ServiceManager.waitForService("package_native");
         assertThat(binder).isNotNull();
diff --git a/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java b/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java
index 3102103..8d696f5 100644
--- a/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java
+++ b/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java
@@ -91,7 +91,7 @@
         deleteFiles("/system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
                 "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
                 "/data/apex/active/" + SHIM_APEX_PACKAGE_NAME + "*.apex",
-                "/system/apex/test.rebootless_apex_v1.apex",
+                "/system/apex/test.rebootless_apex_v*.apex",
                 "/data/apex/active/test.apex.rebootless*.apex",
                 TEST_VENDOR_APEX_ALLOW_LIST);
     }
@@ -493,6 +493,13 @@
         runPhase("testStagedApexObserver");
     }
 
+    @Test
+    public void testRebootlessDowngrade() throws Exception {
+        pushTestApex("test.rebootless_apex_v2.apex");
+        getDevice().reboot();
+        runPhase("testRebootlessDowngrade");
+    }
+
     private List<String> getStagingDirectories() throws DeviceNotAvailableException {
         String baseDir = "/data/app-staging";
         try {
diff --git a/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java
index e4880fd..1341c85 100644
--- a/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java
+++ b/tests/VoiceEnrollment/src/com/android/test/voiceenrollment/TestEnrollmentActivity.java
@@ -62,7 +62,7 @@
     public void onEnrollButtonClicked(View v) {
         Keyphrase kp = new Keyphrase(KEYPHRASE_ID, RECOGNITION_MODES,
                 Locale.forLanguageTag(BCP47_LOCALE), TEXT,
-                new int[] { UserManager.get(this).getUserHandle() /* current user */});
+                new int[] { UserManager.get(this).getProcessUserId() /* current user */});
         UUID modelUuid = UUID.randomUUID();
         // Generate a fake model to push.
         byte[] data = new byte[1024];
diff --git a/tests/componentalias/src/com/android/compatibility/common/util/BroadcastMessenger.java b/tests/componentalias/src/com/android/compatibility/common/util/BroadcastMessenger.java
deleted file mode 100644
index a3a2abe..0000000
--- a/tests/componentalias/src/com/android/compatibility/common/util/BroadcastMessenger.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.compatibility.common.util;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.HandlerThread;
-import android.os.Parcelable;
-import android.os.SystemClock;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Objects;
-
-/**
- * Provides a one-way communication mechanism using a Parcelable as a payload, via broadcasts.
- *
- * Use {@link #send(Context, String, Parcelable)} to send a message.
- * USe {@link Receiver} to receive a message.
- *
- * Pick a unique "suffix" for your test, and use it with both the sender and receiver, in order
- * to avoid "cross-talks" between different tests. (if they ever run at the same time.)
- *
- * TODO: Move it to compatibility-device-util-axt.
- */
-public final class BroadcastMessenger {
-    private static final String TAG = "BroadcastMessenger";
-
-    private static final String ACTION_MESSAGE =
-            "com.android.compatibility.common.util.BroadcastMessenger.ACTION_MESSAGE_";
-    private static final String ACTION_PING =
-            "com.android.compatibility.common.util.BroadcastMessenger.ACTION_PING_";
-    private static final String EXTRA_MESSAGE =
-            "com.android.compatibility.common.util.BroadcastMessenger.EXTRA_MESSAGE";
-
-    /**
-     * We need to drop messages that were sent before the receiver was created. We keep
-     * track of the message send time in this extra.
-     */
-    private static final String EXTRA_SENT_TIME =
-            "com.android.compatibility.common.util.BroadcastMessenger.EXTRA_SENT_TIME";
-
-    public static final int DEFAULT_TIMEOUT_MS = 10_000;
-
-    private static long getCurrentTime() {
-        return SystemClock.uptimeMillis();
-    }
-
-    private static void sendBroadcast(@NonNull Intent i, @NonNull Context context,
-            @NonNull String broadcastSuffix, @Nullable String receiverPackage) {
-        i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-        i.setPackage(receiverPackage);
-        i.putExtra(EXTRA_SENT_TIME, getCurrentTime());
-
-        context.sendBroadcast(i);
-    }
-
-    /** Send a message to the {@link Receiver} expecting a given "suffix". */
-    public static <T extends Parcelable> void send(@NonNull Context context,
-            @NonNull String broadcastSuffix, @NonNull T message) {
-        final Intent i = new Intent(ACTION_MESSAGE + Objects.requireNonNull(broadcastSuffix));
-        i.putExtra(EXTRA_MESSAGE, Objects.requireNonNull(message));
-
-        Log.i(TAG, "Sending: " + message);
-        sendBroadcast(i, context, broadcastSuffix, /*receiverPackage=*/ null);
-    }
-
-    private static void sendPing(@NonNull Context context,@NonNull String broadcastSuffix,
-            @NonNull String receiverPackage) {
-        final Intent i = new Intent(ACTION_PING + Objects.requireNonNull(broadcastSuffix));
-
-        Log.i(TAG, "Sending a ping");
-        sendBroadcast(i, context, broadcastSuffix, receiverPackage);
-    }
-
-    /**
-     * Receive messages sent with {@link #send}. Note it'll ignore all the messages that were
-     * sent before instantiated.
-     */
-    public static final class Receiver<T extends Parcelable> implements AutoCloseable {
-        private final Context mContext;
-        private final String mBroadcastSuffix;
-        private final HandlerThread mReceiverThread = new HandlerThread(TAG);
-
-        // @GuardedBy("mMessages")
-        private final ArrayList<T> mMessages = new ArrayList<>();
-        private final long mCreatedTime = getCurrentTime();
-        private boolean mRegistered;
-
-        private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                // Log.d(TAG, "Received intent: " + intent);
-                if (intent.getAction().equals(ACTION_MESSAGE + mBroadcastSuffix)
-                        || intent.getAction().equals(ACTION_PING + mBroadcastSuffix)) {
-                    // OK
-                } else {
-                    throw new RuntimeException("Unknown broadcast received: " + intent);
-                }
-                if (intent.getLongExtra(EXTRA_SENT_TIME, 0) < mCreatedTime) {
-                    Log.i(TAG, "Dropping stale broadcast: " + intent);
-                    return;
-                }
-
-                // Note for a PING, the message will be null.
-                final T message = intent.getParcelableExtra(EXTRA_MESSAGE);
-                if (message != null) {
-                    Log.i(TAG, "Received: " + message);
-                }
-
-                synchronized (mMessages) {
-                    mMessages.add(message);
-                    mMessages.notifyAll();
-                }
-            }
-        };
-
-        /**
-         * Constructor.
-         */
-        public Receiver(@NonNull Context context, @NonNull String broadcastSuffix) {
-            mContext = context;
-            mBroadcastSuffix = Objects.requireNonNull(broadcastSuffix);
-
-            mReceiverThread.start();
-
-            final IntentFilter fi = new IntentFilter(ACTION_MESSAGE + mBroadcastSuffix);
-            fi.addAction(ACTION_PING + mBroadcastSuffix);
-
-            context.registerReceiver(mReceiver, fi, /* permission=*/ null,
-                    mReceiverThread.getThreadHandler(), Context.RECEIVER_EXPORTED);
-            mRegistered = true;
-        }
-
-        @Override
-        public void close() {
-            if (mRegistered) {
-                mContext.unregisterReceiver(mReceiver);
-                mReceiverThread.quit();
-                mRegistered = false;
-            }
-        }
-
-        /**
-         * Receive the next message with a 60 second timeout.
-         */
-        @NonNull
-        public T waitForNextMessage() {
-            return waitForNextMessage(DEFAULT_TIMEOUT_MS);
-        }
-
-        /**
-         * Receive the next message.
-         */
-        @NonNull
-        public T waitForNextMessage(long timeoutMillis) {
-            synchronized (mMessages) {
-                final long timeout = System.currentTimeMillis() + timeoutMillis;
-                while (mMessages.size() == 0) {
-                    final long wait = timeout - System.currentTimeMillis();
-                    if (wait <= 0) {
-                        throw new RuntimeException("Timeout waiting for the next message");
-                    }
-                    try {
-                        mMessages.wait(wait);
-                    } catch (InterruptedException e) {
-                        throw new RuntimeException(e);
-                    }
-                }
-                return mMessages.remove(0);
-            }
-        }
-
-        /**
-         * Ensure that no further messages have been received.
-         *
-         * Call it before {@link #close()}.
-         */
-        public void ensureNoMoreMessages() {
-            // Send a ping to myself.
-            sendPing(mContext, mBroadcastSuffix, mContext.getPackageName());
-
-            final T m = waitForNextMessage();
-            if (m == null) {
-                return; // Okay. Ping will deliver a null message.
-            }
-            throw new RuntimeException("No more messages expected, but received: " + m);
-        }
-    }
-}
diff --git a/tests/notification/src/com/android/frameworks/tests/notification/NotificationTests.java b/tests/notification/src/com/android/frameworks/tests/notification/NotificationTests.java
index 7cda977..5d639f6 100644
--- a/tests/notification/src/com/android/frameworks/tests/notification/NotificationTests.java
+++ b/tests/notification/src/com/android/frameworks/tests/notification/NotificationTests.java
@@ -409,10 +409,10 @@
         sleepIfYouCan(500);
 
         L("Parceling notifications...");
-        // we want to be able to use this test on older OSes that do not have getBlobAshmemSize
-        Method getBlobAshmemSize = null;
+        // we want to be able to use this test on older OSes that do not have getOpenAshmemSize
+        Method getOpenAshmemSize = null;
         try {
-            getBlobAshmemSize = Parcel.class.getMethod("getBlobAshmemSize");
+            getOpenAshmemSize = Parcel.class.getMethod("getOpenAshmemSize");
         } catch (NoSuchMethodException ex) {
         }
         for (int i=0; i<mNotifications.size(); i++) {
@@ -424,8 +424,8 @@
                 time = SystemClock.currentThreadTimeMillis() - time;
                 L("  %s: write parcel=%dms size=%d ashmem=%s",
                         summarize(n), time, p.dataPosition(),
-                        (getBlobAshmemSize != null)
-                            ? getBlobAshmemSize.invoke(p)
+                        (getOpenAshmemSize != null)
+                            ? getOpenAshmemSize.invoke(p)
                             : "???");
                 p.setDataPosition(0);
             }
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index ad014a2..77f0ef0 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -436,12 +436,12 @@
       const size_t index = type_index_iter->second;
       if (new_packages.size() == index) {
         new_packages.emplace_back(ResourceTablePackageView{package.name, package.id});
-        type_new_package_index[type.type] = index + 1;
       }
 
       // Move the type into a new package
       auto& other_package = new_packages[index];
       type_inserter.Insert(other_package.types, std::move(type));
+      type_new_package_index[type.type] = index + 1;
       type_it = package.types.erase(type_it);
     }
   }
diff --git a/tools/aapt2/xml/XmlDom.cpp b/tools/aapt2/xml/XmlDom.cpp
index 8b7eadf9..dd60f17 100644
--- a/tools/aapt2/xml/XmlDom.cpp
+++ b/tools/aapt2/xml/XmlDom.cpp
@@ -349,7 +349,8 @@
         size_t len;
         const char16_t* str16 = tree.getText(&len);
         if (str16) {
-          text->text = util::Utf16ToUtf8(StringPiece16(str16, len));
+          text->text =
+              ResTable::normalizeForOutput(util::Utf16ToUtf8(StringPiece16(str16, len)).c_str());
         }
         CHECK(!node_stack.empty());
         node_stack.top()->AppendChild(std::move(text));
diff --git a/tools/aapt2/xml/XmlDom_test.cpp b/tools/aapt2/xml/XmlDom_test.cpp
index 6c717dc..3a2d656 100644
--- a/tools/aapt2/xml/XmlDom_test.cpp
+++ b/tools/aapt2/xml/XmlDom_test.cpp
@@ -139,6 +139,19 @@
   EXPECT_THAT(attr->value, Eq("\""));
 }
 
+TEST(XmlDomTest, XmlEscapeSingleQuotes) {
+  std::unique_ptr<XmlResource> doc = test::BuildXmlDom(R"(
+    <foo><![CDATA[oh no' (line=1001)
+E: this-is-not-an-element (line=88)
+    T: 'blah]]></foo>)");
+
+  Element* el = doc->root.get();
+  Text* text = xml::NodeCast<xml::Text>(el->children[0].get());
+  ASSERT_THAT(text, NotNull());
+  EXPECT_THAT(text->text,
+              Eq("oh no' (line=1001)\nE: this-is-not-an-element (line=88)\n    T: 'blah"));
+}
+
 class TestVisitor : public PackageAwareVisitor {
  public:
   using PackageAwareVisitor::Visit;