Merge "Fix IME crash on SoftInputWindow.show by TOKEN_PENDING"
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/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index a23f6e1..c460312 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -29,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;
@@ -2932,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 d35c03d..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
@@ -555,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);
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
index 725092c..6232dfb 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java
@@ -20,9 +20,13 @@
 
 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;
@@ -36,6 +40,7 @@
 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;
 
@@ -57,6 +62,7 @@
      */
     @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
@@ -69,6 +75,8 @@
     public PrefetchController(JobSchedulerService service) {
         super(service);
         mPcConstants = new PcConstants();
+        mThresholdAlarmListener = new ThresholdAlarmListener(
+                mContext, JobSchedulerBackgroundThread.get().getLooper());
     }
 
     @Override
@@ -82,9 +90,13 @@
                 jobs = new ArraySet<>();
                 mTrackedJobs.add(userId, pkgName, jobs);
             }
-            jobs.add(jobStatus);
-            updateConstraintLocked(jobStatus,
-                    sSystemClock.millis(), sElapsedRealtimeClock.millis());
+            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);
         }
     }
 
@@ -92,10 +104,11 @@
     @GuardedBy("mLock")
     public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
             boolean forUpdate) {
-        final ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUserId(),
-                jobStatus.getSourcePackageName());
-        if (jobs != null) {
-            jobs.remove(jobStatus);
+        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));
         }
     }
 
@@ -109,6 +122,7 @@
         final int userId = UserHandle.getUserId(uid);
         mTrackedJobs.delete(userId, packageName);
         mEstimatedLaunchTimes.delete(userId, packageName);
+        mThresholdAlarmListener.removeAlarmForKey(new Package(userId, packageName));
     }
 
     @Override
@@ -116,6 +130,7 @@
     public void onUserRemovedLocked(int userId) {
         mTrackedJobs.delete(userId);
         mEstimatedLaunchTimes.delete(userId);
+        mThresholdAlarmListener.removeAlarmsForUserId(userId);
     }
 
     /** Return the app's next estimated launch time. */
@@ -124,8 +139,14 @@
     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);
-        final long now = sSystemClock.millis();
         if (nextEstimatedLaunchTime == null || nextEstimatedLaunchTime < now) {
             // TODO(194532703): get estimated time from UsageStats
             nextEstimatedLaunchTime = now + 2 * HOUR_IN_MILLIS;
@@ -135,8 +156,8 @@
     }
 
     @GuardedBy("mLock")
-    private boolean maybeUpdateConstraintForPkgLocked(long now, long nowElapsed, int userId,
-            String pkgName) {
+    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;
@@ -150,10 +171,43 @@
     }
 
     @GuardedBy("mLock")
-    private boolean updateConstraintLocked(@NonNull JobStatus jobStatus, long now,
-            long nowElapsed) {
+    private boolean updateConstraintLocked(@NonNull JobStatus jobStatus,
+            @CurrentTimeMillisLong long now, @ElapsedRealtimeLong long nowElapsed) {
         return jobStatus.setPrefetchConstraintSatisfied(nowElapsed,
-                getNextEstimatedLaunchTimeLocked(jobStatus) <= now + mLaunchTimeThresholdMs);
+                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
@@ -186,6 +240,9 @@
                                     now, nowElapsed, userId, packageName)) {
                                 changedJobs.addAll(mTrackedJobs.valueAt(u, p));
                             }
+                            if (!willBeLaunchedSoonLocked(userId, packageName, now)) {
+                                updateThresholdAlarmLocked(userId, packageName, now, nowElapsed);
+                            }
                         }
                     }
                 }
@@ -196,6 +253,42 @@
         }
     }
 
+    /** 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;
@@ -225,6 +318,9 @@
                     if (mLaunchTimeThresholdMs != newLaunchTimeThresholdMs) {
                         mLaunchTimeThresholdMs = newLaunchTimeThresholdMs;
                         mShouldReevaluateConstraints = true;
+                        // Give a leeway of 10% of the launch time threshold between alarms.
+                        mThresholdAlarmListener.setMinTimeBetweenAlarmsMs(
+                                mLaunchTimeThresholdMs / 10);
                     }
                     break;
             }
@@ -294,6 +390,9 @@
                 pw.println();
             }
         });
+
+        pw.println();
+        mThresholdAlarmListener.dump(pw);
     }
 
     @Override
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 1016294..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;
@@ -79,7 +80,6 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -123,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));
     }
@@ -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,7 +1708,8 @@
         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.");
@@ -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;
             }
         }
@@ -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:
@@ -4119,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];
@@ -4143,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);
             }
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/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..48a373b 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.hardware.biometrics.face.V1_0.UserHandle;
+import android.os.Environment;
+import android.util.AtomicFile;
 import android.util.IndentingPrintWriter;
 import android.util.Log;
+import android.util.Pair;
+import android.util.Slog;
 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 com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
+
+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.Arrays;
+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,107 @@
     }
 
     /** 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;
+        }
+
+        UserManagerInternal userManagerInternal =
+                LocalServices.getService(UserManagerInternal.class);
+        final int[] userIds = userManagerInternal.getUserIds();
+        Arrays.sort(userIds);
+
+        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, userIds, 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 +272,206 @@
         }
     }
 
-    @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,
+            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);
+
+        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 || !"transaction".equals(tagName)) {
+                // Expecting only "transaction" tags.
+                Slog.e(TAG, "Unexpected event: (" + eventType + ") " + tagName);
+                return null;
+            }
+            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));
+        }
+
+        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, int[] validUserIds,
+            long endTimeCutoff) throws XmlPullParserException, IOException {
+        int curUser = parser.getAttributeInt(null, XML_ATTR_USER_ID);
+        if (Arrays.binarySearch(validUserIds, curUser) < 0) {
+            Slog.w(TAG, "Invalid user " + curUser + " is saved to disk");
+            curUser = UserHandle.NONE;
+            // 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.NONE) {
+                    continue;
+                }
+                final Pair<String, Ledger> ledgerData = readLedgerFromXml(parser, endTimeCutoff);
+                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/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/core/api/current.txt b/core/api/current.txt
index 4dd311c..2f0969b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -6257,6 +6257,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);
@@ -13300,6 +13301,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);
@@ -18011,7 +18013,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.DeviceStateOrientationMap> INFO_DEVICE_STATE_ORIENTATION_MAP;
+    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;
@@ -18706,7 +18708,7 @@
     method public android.util.Rational getElement(int, int);
   }
 
-  public final class DeviceStateOrientationMap {
+  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
@@ -25659,6 +25661,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";
@@ -25689,6 +25692,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";
@@ -25851,6 +25855,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";
@@ -25882,6 +25887,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";
@@ -31566,6 +31572,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);
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 5ef3d04..29d581a 100755
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2133,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;
@@ -5448,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);
@@ -5461,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
@@ -5477,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 {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index b225f5b..372ad9d 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -305,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);
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/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/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/BluetoothUuid.java b/core/java/android/bluetooth/BluetoothUuid.java
index 325a771..858819e 100644
--- a/core/java/android/bluetooth/BluetoothUuid.java
+++ b/core/java/android/bluetooth/BluetoothUuid.java
@@ -188,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/res/Configuration.java b/core/java/android/content/res/Configuration.java
index b66f048..7deff7c 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -2620,10 +2620,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 ddac22c..9f77a7e 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -22,7 +22,7 @@
 import android.hardware.camera2.impl.CameraMetadataNative;
 import android.hardware.camera2.impl.PublicKey;
 import android.hardware.camera2.impl.SyntheticKey;
-import android.hardware.camera2.params.DeviceStateOrientationMap;
+import android.hardware.camera2.params.DeviceStateSensorOrientationMap;
 import android.hardware.camera2.params.RecommendedStreamConfigurationMap;
 import android.hardware.camera2.params.SessionConfiguration;
 import android.hardware.camera2.utils.TypeReference;
@@ -258,11 +258,12 @@
     private <T> T overrideProperty(Key<T> key) {
         if (CameraCharacteristics.SENSOR_ORIENTATION.equals(key) && (mFoldStateListener != null) &&
                 (mProperties.get(CameraCharacteristics.INFO_DEVICE_STATE_ORIENTATIONS) != null)) {
-            DeviceStateOrientationMap deviceStateOrientationMap =
-                    mProperties.get(CameraCharacteristics.INFO_DEVICE_STATE_ORIENTATION_MAP);
+            DeviceStateSensorOrientationMap deviceStateSensorOrientationMap =
+                    mProperties.get(CameraCharacteristics.INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP);
             synchronized (mLock) {
-                Integer ret = deviceStateOrientationMap.getSensorOrientation(mFoldedDeviceState ?
-                        DeviceStateOrientationMap.FOLDED : DeviceStateOrientationMap.NORMAL);
+                Integer ret = deviceStateSensorOrientationMap.getSensorOrientation(
+                        mFoldedDeviceState ? DeviceStateSensorOrientationMap.FOLDED :
+                                DeviceStateSensorOrientationMap.NORMAL);
                 if (ret >= 0) {
                     return (T) ret;
                 } else {
@@ -4056,7 +4057,7 @@
      * 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.DeviceStateOrientationMap }.
+     * {@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
@@ -4384,7 +4385,7 @@
      * 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.DeviceStateOrientationMap }.
+     * {@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>
@@ -4398,8 +4399,8 @@
     @PublicKey
     @NonNull
     @SyntheticKey
-    public static final Key<android.hardware.camera2.params.DeviceStateOrientationMap> INFO_DEVICE_STATE_ORIENTATION_MAP =
-            new Key<android.hardware.camera2.params.DeviceStateOrientationMap>("android.info.deviceStateOrientationMap", android.hardware.camera2.params.DeviceStateOrientationMap.class);
+    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
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 2e86a8b..93f1d61 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -31,7 +31,6 @@
 import android.hardware.camera2.impl.CameraDeviceImpl;
 import android.hardware.camera2.impl.CameraInjectionSessionImpl;
 import android.hardware.camera2.impl.CameraMetadataNative;
-import android.hardware.camera2.params.DeviceStateOrientationMap;
 import android.hardware.camera2.params.ExtensionSessionConfiguration;
 import android.hardware.camera2.params.SessionConfiguration;
 import android.hardware.camera2.params.StreamConfiguration;
@@ -118,8 +117,16 @@
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
         mFoldStateListener = new FoldStateListener(context);
-        context.getSystemService(DeviceStateManager.class)
-                .registerCallback(new HandlerExecutor(mHandler), mFoldStateListener);
+        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;
@@ -185,7 +192,9 @@
         synchronized (mLock) {
             DeviceStateListener listener = chars.getDeviceStateListener();
             listener.onDeviceStateChanged(mFoldedDeviceState);
-            mDeviceStateListeners.add(new WeakReference<>(listener));
+            if (mFoldStateListener != null) {
+                mDeviceStateListeners.add(new WeakReference<>(listener));
+            }
         }
     }
 
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index 3745022..e393a66 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -50,11 +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.DeviceStateOrientationMap;
+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;
@@ -763,7 +762,7 @@
                     }
                 });
         sGetCommandMap.put(
-                CameraCharacteristics.INFO_DEVICE_STATE_ORIENTATION_MAP.getNativeKey(),
+                CameraCharacteristics.INFO_DEVICE_STATE_SENSOR_ORIENTATION_MAP.getNativeKey(),
                         new GetCommand() {
                     @Override
                     @SuppressWarnings("unchecked")
@@ -1004,7 +1003,7 @@
         return map;
     }
 
-    private DeviceStateOrientationMap getDeviceStateOrientationMap() {
+    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.
@@ -1012,7 +1011,7 @@
             return null;
         }
 
-        DeviceStateOrientationMap map = new DeviceStateOrientationMap(mapArray);
+        DeviceStateSensorOrientationMap map = new DeviceStateSensorOrientationMap(mapArray);
         return map;
     }
 
diff --git a/core/java/android/hardware/camera2/params/DeviceStateOrientationMap.java b/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java
similarity index 90%
rename from core/java/android/hardware/camera2/params/DeviceStateOrientationMap.java
rename to core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java
index 3907f04..200409e 100644
--- a/core/java/android/hardware/camera2/params/DeviceStateOrientationMap.java
+++ b/core/java/android/hardware/camera2/params/DeviceStateSensorOrientationMap.java
@@ -40,7 +40,7 @@
  *
  * @see CameraCharacteristics#SENSOR_ORIENTATION
  */
-public final class DeviceStateOrientationMap {
+public final class DeviceStateSensorOrientationMap {
     /**
      *  Needs to be kept in sync with the HIDL/AIDL DeviceState
      */
@@ -85,10 +85,10 @@
      *
      * @hide
      */
-    public DeviceStateOrientationMap(final long[] elements) {
+    public DeviceStateSensorOrientationMap(final long[] elements) {
         mElements = Objects.requireNonNull(elements, "elements must not be null");
         if ((elements.length % 2) != 0) {
-            throw new IllegalArgumentException("Device state orientation map length " +
+            throw new IllegalArgumentException("Device state sensor orientation map length " +
                     elements.length + " is not even!");
         }
 
@@ -121,7 +121,8 @@
     }
 
     /**
-     * Check if this DeviceStateOrientationMap is equal to another DeviceStateOrientationMap.
+     * 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>
@@ -136,8 +137,8 @@
         if (this == obj) {
             return true;
         }
-        if (obj instanceof DeviceStateOrientationMap) {
-            final DeviceStateOrientationMap other = (DeviceStateOrientationMap) obj;
+        if (obj instanceof DeviceStateSensorOrientationMap) {
+            final DeviceStateSensorOrientationMap other = (DeviceStateSensorOrientationMap) obj;
             return Arrays.equals(mElements, other.mElements);
         }
         return false;
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index dd387da..a85293a 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -2309,6 +2309,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.
      *
@@ -5044,6 +5076,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);
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 44d51db..fa578be 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);
 
@@ -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.
      *
@@ -3536,15 +3553,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;
@@ -3592,7 +3620,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);
             }
@@ -3601,7 +3629,7 @@
         public boolean hasFileDescriptors() {
             Parcel source = mSource;
             return (source != null)
-                    ? getValueParcel(source).hasFileDescriptors()
+                    ? source.hasFileDescriptors(mPosition, mLength)
                     : Parcel.hasFileDescriptors(mObject);
         }
 
@@ -3662,10 +3690,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;
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 8553c24..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 {
         /**
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/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/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/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java
index 63cce0a..7648c42 100644
--- a/core/java/com/android/internal/os/BatteryStatsImpl.java
+++ b/core/java/com/android/internal/os/BatteryStatsImpl.java
@@ -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);
@@ -844,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;
 
@@ -4455,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());
         }
     }
 
@@ -5054,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;
@@ -5130,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);
@@ -5152,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;
         }
     }
 
@@ -6842,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);
     }
@@ -10875,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);
@@ -10987,6 +11297,8 @@
             // Initialize the estimated battery capacity to a known preset one.
             mEstimatedBatteryCapacityMah = (int) mPowerProfile.getBatteryCapacity();
         }
+
+        setDisplayCountLocked(mPowerProfile.getNumDisplays());
     }
 
     PowerProfile getPowerProfile() {
@@ -11019,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();
@@ -11502,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 {
diff --git a/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl b/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl
index a8003a1..d69a240 100644
--- a/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl
+++ b/core/java/com/android/internal/policy/IKeyguardStateCallback.aidl
@@ -20,5 +20,4 @@
     void onSimSecureStateChanged(boolean simSecure);
     void onInputRestrictedStateChanged(boolean inputRestricted);
     void onTrustedChanged(boolean trusted);
-    void onHasLockscreenWallpaperChanged(boolean hasLockscreenWallpaper);
 }
\ No newline at end of file
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/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp
index aadd320..8fee610 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>
@@ -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},
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 612dfd0..bf2c08f 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -709,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                           -->
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/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/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java b/core/tests/coretests/src/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java
index d76037e..e95f6c2 100644
--- a/core/tests/coretests/src/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/AmbientDisplayPowerCalculatorTest.java
@@ -47,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,
@@ -78,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/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/MockBatteryStatsImpl.java b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
index b31587b..c24dc67 100644
--- a/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
+++ b/core/tests/coretests/src/com/android/internal/os/MockBatteryStatsImpl.java
@@ -39,6 +39,7 @@
 public class MockBatteryStatsImpl extends BatteryStatsImpl {
     public boolean mForceOnBattery;
     private NetworkStats mNetworkStats;
+    private DummyExternalStatsSync mExternalStatsSync = new DummyExternalStatsSync();
 
     MockBatteryStatsImpl() {
         this(new MockClock());
@@ -52,7 +53,7 @@
         super(clock, historyDirectory);
         initTimersAndCounters();
 
-        setExternalStatsSyncLocked(new DummyExternalStatsSync());
+        setExternalStatsSyncLocked(mExternalStatsSync);
         informThatAllExternalStatsAreFlushed();
 
         // A no-op handler.
@@ -185,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;
@@ -219,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/ScreenPowerCalculatorTest.java b/core/tests/coretests/src/com/android/internal/os/ScreenPowerCalculatorTest.java
index 50e0a15..73f4eb2 100644
--- a/core/tests/coretests/src/com/android/internal/os/ScreenPowerCalculatorTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/ScreenPowerCalculatorTest.java
@@ -53,7 +53,7 @@
         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);
@@ -70,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);
@@ -133,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/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index d965351..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",
@@ -2419,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",
@@ -2647,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",
@@ -2983,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",
@@ -3661,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",
@@ -3861,6 +3927,9 @@
     "WM_DEBUG_TASKS": {
       "tag": "WindowManager"
     },
+    "WM_DEBUG_WALLPAPER": {
+      "tag": "WindowManager"
+    },
     "WM_DEBUG_WINDOW_INSETS": {
       "tag": "WindowManager"
     },
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 b191cab..8df7cbb 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, IconProvider iconProvider, TransactionPool pool) {
+    SplashscreenContentDrawer(Context context, TransactionPool pool) {
         mContext = context;
-        mIconProvider = iconProvider;
+        mIconProvider = new IconProvider(context);
         mTransactionPool = pool;
 
         // Initialize Splashscreen worker thread
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 bd48696..979bf00 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
@@ -61,7 +61,6 @@
 
 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;
@@ -124,11 +123,11 @@
      * @param splashScreenExecutor The thread used to control add and remove starting window.
      */
     public StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor,
-            IconProvider iconProvider, TransactionPool pool) {
+            TransactionPool pool) {
         mContext = context;
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         mSplashScreenExecutor = splashScreenExecutor;
-        mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool);
+        mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, pool);
         mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance());
         mWindowManagerGlobal = WindowManagerGlobal.getInstance();
         mDisplayManager.getDisplay(DEFAULT_DISPLAY);
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 a86e07a..99644f9 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
@@ -43,7 +43,6 @@
 
 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;
@@ -86,11 +85,9 @@
     private final SparseIntArray mTaskBackgroundColors = new SparseIntArray();
 
     public StartingWindowController(Context context, ShellExecutor splashScreenExecutor,
-            StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider,
-            TransactionPool pool) {
+            StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, TransactionPool pool) {
         mContext = context;
-        mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor,
-                iconProvider, pool);
+        mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor, pool);
         mStartingWindowTypeAlgorithm = startingWindowTypeAlgorithm;
         mSplashScreenExecutor = splashScreenExecutor;
     }
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 b866bf9..e5a8aa0 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
@@ -69,7 +69,6 @@
 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;
@@ -94,8 +93,6 @@
     @Mock
     private WindowManager mMockWindowManager;
     @Mock
-    private IconProvider mIconProvider;
-    @Mock
     private TransactionPool mTransactionPool;
 
     private final Handler mTestHandler = new Handler(Looper.getMainLooper());
@@ -108,8 +105,8 @@
         int mAddWindowForTask = 0;
 
         TestStartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor,
-                IconProvider iconProvider, TransactionPool pool) {
-            super(context, splashScreenExecutor, iconProvider, pool);
+                TransactionPool pool) {
+            super(context, splashScreenExecutor, pool);
         }
 
         @Override
@@ -159,8 +156,7 @@
         doNothing().when(mMockWindowManager).addView(any(), any());
         mTestExecutor = new HandlerExecutor(mTestHandler);
         mStartingSurfaceDrawer = spy(
-                new TestStartingSurfaceDrawer(mTestContext, mTestExecutor, mIconProvider,
-                        mTransactionPool));
+                new TestStartingSurfaceDrawer(mTestContext, mTestExecutor, mTransactionPool));
     }
 
     @Test
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/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/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/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/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-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/flags.xml b/packages/SystemUI/res/values/flags.xml
index 08f72cb..14bf436 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -53,6 +53,8 @@
 
     <bool name="flag_ongoing_call_in_immersive">false</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/styles.xml b/packages/SystemUI/res/values/styles.xml
index 761e3db..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" />
 
@@ -943,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/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/KeyguardSliceViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
index 8038ce4..4a56773 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java
@@ -84,7 +84,7 @@
             mView.onDensityOrFontScaleChanged();
         }
         @Override
-        public void onOverlayChanged() {
+        public void onThemeChanged() {
             mView.onOverlayChanged();
         }
     };
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 5969e92..24f3673 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -277,7 +277,6 @@
     private boolean mBouncer; // true if bouncerIsOrWillBeShowing
     private boolean mAuthInterruptActive;
     private boolean mNeedsSlowUnlockTransition;
-    private boolean mHasLockscreenWallpaper;
     private boolean mAssistantVisible;
     private boolean mKeyguardOccluded;
     private boolean mOccludingAppRequestingFp;
@@ -2579,31 +2578,6 @@
     }
 
     /**
-     * Update the state whether Keyguard currently has a lockscreen wallpaper.
-     *
-     * @param hasLockscreenWallpaper Whether Keyguard has a lockscreen wallpaper.
-     */
-    public void setHasLockscreenWallpaper(boolean hasLockscreenWallpaper) {
-        Assert.isMainThread();
-        if (hasLockscreenWallpaper != mHasLockscreenWallpaper) {
-            mHasLockscreenWallpaper = hasLockscreenWallpaper;
-            for (int i = 0; i < mCallbacks.size(); i++) {
-                KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
-                if (cb != null) {
-                    cb.onHasLockscreenWallpaperChanged(hasLockscreenWallpaper);
-                }
-            }
-        }
-    }
-
-    /**
-     * @return Whether Keyguard has a lockscreen wallpaper.
-     */
-    public boolean hasLockscreenWallpaper() {
-        return mHasLockscreenWallpaper;
-    }
-
-    /**
      * Handle {@link #MSG_DPM_STATE_CHANGED}
      */
     private void handleDevicePolicyManagerStateChanged(int userId) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index 6aa7aaa..e970a86 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -292,11 +292,6 @@
     public void onStrongAuthStateChanged(int userId) { }
 
     /**
-     * Called when the state whether we have a lockscreen wallpaper has changed.
-     */
-    public void onHasLockscreenWallpaperChanged(boolean hasLockscreenWallpaper) { }
-
-    /**
      * Called when the dream's window state is changed.
      * @param dreaming true if the dream's window has been created and is visible
      */
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 9aa03a9..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,7 +260,7 @@
         boolean wasShowingLockIcon = mShowLockIcon;
         boolean wasShowingUnlockIcon = mShowUnlockIcon;
         mShowLockIcon = !mCanDismissLockScreen && !mUserUnlockedWithBiometric && isLockScreen()
-            && (!mUdfpsEnrolled || !mRunningFPS);
+                && (!mUdfpsEnrolled || !mRunningFPS);
         mShowUnlockIcon = (mCanDismissLockScreen || mUserUnlockedWithBiometric) && isLockScreen();
         mShowAODFpIcon = mIsDozing && mUdfpsEnrolled && !mRunningFPS;
 
@@ -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();
         }
@@ -551,11 +553,6 @@
         }
 
         @Override
-        public void onOverlayChanged() {
-            updateColors();
-        }
-
-        @Override
         public void onConfigChanged(Configuration newConfig) {
             updateConfiguration();
             updateColors();
@@ -656,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/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/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index 0932a8c..8b04bf5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -272,9 +272,6 @@
             override fun onThemeChanged() {
                 updateRippleColor()
             }
-            override fun onOverlayChanged() {
-                updateRippleColor()
-            }
     }
 
     private val udfpsControllerCallback =
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/flags/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.java
index 77bd777..5b9ccd6 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.java
@@ -128,6 +128,11 @@
                 && 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/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 5265718..7813840 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -670,13 +670,6 @@
                 }
             }
         }
-
-        @Override
-        public void onHasLockscreenWallpaperChanged(boolean hasLockscreenWallpaper) {
-            synchronized (KeyguardViewMediator.this) {
-                notifyHasLockscreenWallpaperChanged(hasLockscreenWallpaper);
-            }
-        }
     };
 
     ViewMediatorCallback mViewMediatorCallback = new ViewMediatorCallback() {
@@ -2873,21 +2866,6 @@
         }
     }
 
-    private void notifyHasLockscreenWallpaperChanged(boolean hasLockscreenWallpaper) {
-        int size = mKeyguardStateCallbacks.size();
-        for (int i = size - 1; i >= 0; i--) {
-            try {
-                mKeyguardStateCallbacks.get(i).onHasLockscreenWallpaperChanged(
-                        hasLockscreenWallpaper);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to call onHasLockscreenWallpaperChanged", e);
-                if (e instanceof DeadObjectException) {
-                    mKeyguardStateCallbacks.remove(i);
-                }
-            }
-        }
-    }
-
     public void addStateMonitorCallback(IKeyguardStateCallback callback) {
         synchronized (this) {
             mKeyguardStateCallbacks.add(callback);
@@ -2897,7 +2875,6 @@
                 callback.onInputRestrictedStateChanged(mInputRestricted);
                 callback.onTrustedChanged(mUpdateMonitor.getUserHasTrust(
                         KeyguardUpdateMonitor.getCurrentUser()));
-                callback.onHasLockscreenWallpaperChanged(mUpdateMonitor.hasLockscreenWallpaper());
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to call to IKeyguardStateCallback", e);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index 0e70945..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()
         }
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/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/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 452e737..5758ba4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -257,7 +257,8 @@
             OngoingCallLogger logger,
             DumpManager dumpManager,
             StatusBarWindowController statusBarWindowController,
-            SwipeStatusBarAwayGestureHandler swipeStatusBarAwayGestureHandler) {
+            SwipeStatusBarAwayGestureHandler swipeStatusBarAwayGestureHandler,
+            StatusBarStateController statusBarStateController) {
         Optional<StatusBarWindowController> windowController =
                 featureFlags.isOngoingCallInImmersiveEnabled()
                         ? Optional.of(statusBarWindowController)
@@ -277,8 +278,8 @@
                         logger,
                         dumpManager,
                         windowController,
-                        gestureHandler
-                );
+                        gestureHandler,
+                        statusBarStateController);
         ongoingCallController.init();
         return ongoingCallController;
     }
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/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/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/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/LockscreenWallpaper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
index 78fcd82..2a13e6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java
@@ -119,7 +119,6 @@
         LoaderResult result = loadBitmap(mCurrentUserId, mSelectedUser);
         if (result.success) {
             mCached = true;
-            mUpdateMonitor.setHasLockscreenWallpaper(result.bitmap != null);
             mCache = result.bitmap;
         }
         return mCache;
@@ -235,7 +234,6 @@
                 if (result.success) {
                     mCached = true;
                     mCache = result.bitmap;
-                    mUpdateMonitor.setHasLockscreenWallpaper(result.bitmap != null);
                     mMediaManager.updateMediaMetaData(
                             true /* metaDataChanged */, true /* allowEnterAnimation */);
                 }
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 d09a89e..83312cd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -4411,12 +4411,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();
         }
 
@@ -4430,12 +4425,6 @@
         }
 
         @Override
-        public void onOverlayChanged() {
-            if (DEBUG) Log.d(TAG, "onOverlayChanged");
-            reInflateViews();
-        }
-
-        @Override
         public void onDensityOrFontScaleChanged() {
             if (DEBUG) Log.d(TAG, "onDensityOrFontScaleChanged");
             reInflateViews();
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 371ec7a..a5cea06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -265,11 +265,6 @@
             }
 
             @Override
-            public void onOverlayChanged() {
-                ScrimController.this.onThemeChanged();
-            }
-
-            @Override
             public void onUiModeChanged() {
                 ScrimController.this.onThemeChanged();
             }
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 1f0785e..48cb8e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -3287,16 +3287,13 @@
      * 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();
-        }
+        mContext.setTheme(themeResId);
+        mConfigurationController.notifyThemeChanged();
     }
 
     private void updateDozingState() {
@@ -4409,6 +4406,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();
             }
@@ -4419,17 +4423,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();
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/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/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/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index c3c935e..7d476bf 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,6 +34,7 @@
 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.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
@@ -61,8 +62,10 @@
     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. */
@@ -124,6 +127,7 @@
         dumpManager.registerDumpable(this)
         if (featureFlags.isOngoingCallStatusBarChipEnabled) {
             notifCollection.addCollectionListener(notifListener)
+            statusBarStateController.addCallback(statusBarStateListener)
         }
     }
 
@@ -177,10 +181,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 -
@@ -191,19 +193,8 @@
                 timeView.setShouldHideText(true)
                 timeView.stop()
             }
+            updateChipClickListener()
 
-            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)
-                    )
-                }
-            }
             setUpUidObserver(currentCallNotificationInfo)
             if (!currentCallNotificationInfo.statusBarSwipedAway) {
                 statusBarWindowController.ifPresent {
@@ -227,6 +218,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.
      */
@@ -304,14 +319,21 @@
     * 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 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()
         }
     }
 
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/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 ff1929c..514a903 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java
@@ -26,7 +26,6 @@
 
 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;
@@ -167,12 +166,6 @@
         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
@@ -493,10 +486,9 @@
     @Provides
     static StartingWindowController provideStartingWindowController(Context context,
             @ShellSplashscreenThread ShellExecutor splashScreenExecutor,
-            StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider,
-            TransactionPool pool) {
+            StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, TransactionPool pool) {
         return new StartingWindowController(context, splashScreenExecutor,
-                startingWindowTypeAlgorithm, iconProvider, pool);
+                startingWindowTypeAlgorithm, pool);
     }
 
     //
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 ddf1d70..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,9 +18,10 @@
 
 import static junit.framework.Assert.assertEquals;
 
-import static org.mockito.ArgumentMatchers.anyObject;
+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;
@@ -37,6 +38,7 @@
 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;
 
@@ -96,6 +98,7 @@
     private @Mock Vibrator mVibrator;
     private @Mock AuthRippleController mAuthRippleController;
     private @Mock LottieAnimationView mAodFp;
+    private @Mock LayoutInflater mLayoutInflater;
 
     private LockIconViewController mLockIconViewController;
 
@@ -120,11 +123,11 @@
 
         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(), anyObject())).thenReturn(mIconDrawable);
+        when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable);
 
         when(mKeyguardStateController.isShowing()).thenReturn(true);
         when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
@@ -144,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();
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/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index be27876..ca6e1ee 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,6 +36,7 @@
 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.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
@@ -82,11 +83,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
 
@@ -98,13 +101,12 @@
         }
 
         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,
@@ -113,7 +115,8 @@
                 DumpManager(),
                 Optional.of(mockStatusBarWindowController),
                 Optional.of(mockSwipeStatusBarAwayGestureHandler),
-        )
+                mockStatusBarStateController,
+            )
         controller.init()
         controller.addCallback(mockOngoingCallListener)
         controller.setChipView(chipView)
@@ -455,6 +458,56 @@
     // 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()
+    }
+
     private fun createOngoingCallNotifEntry() = createCallNotifEntry(ongoingCallStyle)
 
     private fun createScreeningCallNotifEntry() = createCallNotifEntry(screeningCallStyle)
@@ -479,6 +532,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/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/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java b/services/core/java/com/android/server/am/BatteryExternalStatsWorker.java
index baf0f39..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;
@@ -675,7 +683,8 @@
             if (measuredEnergyDeltas != null) {
                 final long[] displayChargeUC = measuredEnergyDeltas.displayChargeUC;
                 if (displayChargeUC != null && displayChargeUC.length > 0) {
-                    // TODO (b/194107383): pass all display ordinals to mStats.
+                    // 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(primaryDisplayChargeUC,
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/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3aa7ab9..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;
@@ -8509,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 98452e5..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) {
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index b7744c7e..75d7893 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -5022,7 +5022,6 @@
 
         @Override
         public boolean matchesCallFilter(Bundle extras) {
-            enforceSystemOrSystemUI("INotificationManager.matchesCallFilter");
             return mZenModeHelper.matchesCallFilter(
                     Binder.getCallingUserHandle(),
                     extras,
@@ -5032,6 +5031,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);
diff --git a/services/core/java/com/android/server/notification/ZenModeFiltering.java b/services/core/java/com/android/server/notification/ZenModeFiltering.java
index 4d19855..0f526d4 100644
--- a/services/core/java/com/android/server/notification/ZenModeFiltering.java
+++ b/services/core/java/com/android/server/notification/ZenModeFiltering.java
@@ -311,6 +311,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 +350,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/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 37879d7..7dd02cb 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -458,10 +458,8 @@
         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) {
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 7596cdf..bcf2f92 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -220,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);
@@ -249,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);
                 }
             }
         }
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/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 12e6086d..5d34939 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -47,7 +47,6 @@
 import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW;
-import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
 import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
@@ -3291,18 +3290,7 @@
         final boolean showing = mKeyguardDelegate.isShowing();
         final boolean animate = showing && !isOccluded;
         mKeyguardDelegate.setOccluded(isOccluded, animate);
-
-        if (!showing) {
-            return false;
-        }
-        if (mKeyguardCandidate != null) {
-            if (isOccluded) {
-                mKeyguardCandidate.getAttrs().flags &= ~FLAG_SHOW_WALLPAPER;
-            } else if (!mKeyguardDelegate.hasLockscreenWallpaper()) {
-                mKeyguardCandidate.getAttrs().flags |= FLAG_SHOW_WALLPAPER;
-            }
-        }
-        return true;
+        return showing;
     }
 
     /** {@inheritDoc} */
diff --git a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
index 86ff33e..cdd36f7 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
@@ -235,13 +235,6 @@
         return false;
     }
 
-    public boolean hasLockscreenWallpaper() {
-        if (mKeyguardService != null) {
-            return mKeyguardService.hasLockscreenWallpaper();
-        }
-        return false;
-    }
-
     public boolean hasKeyguard() {
         return mKeyguardState.deviceHasKeyguard;
     }
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 c356fec..2029f86 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
@@ -267,10 +267,6 @@
         return mKeyguardStateMonitor.isTrusted();
     }
 
-    public boolean hasLockscreenWallpaper() {
-        return mKeyguardStateMonitor.hasLockscreenWallpaper();
-    }
-
     public boolean isSecure(int userId) {
         return mKeyguardStateMonitor.isSecure(userId);
     }
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 f0f62ed..c0aa8ae 100644
--- a/services/core/java/com/android/server/policy/keyguard/KeyguardStateMonitor.java
+++ b/services/core/java/com/android/server/policy/keyguard/KeyguardStateMonitor.java
@@ -44,7 +44,6 @@
     private volatile boolean mSimSecure = true;
     private volatile boolean mInputRestricted = true;
     private volatile boolean mTrusted = false;
-    private volatile boolean mHasLockscreenWallpaper = false;
 
     private int mCurrentUserId;
 
@@ -79,10 +78,6 @@
         return mTrusted;
     }
 
-    public boolean hasLockscreenWallpaper() {
-        return mHasLockscreenWallpaper;
-    }
-
     public int getCurrentUser() {
         return mCurrentUserId;
     }
@@ -116,11 +111,6 @@
         mCallback.onTrustedChanged();
     }
 
-    @Override // Binder interface
-    public void onHasLockscreenWallpaperChanged(boolean hasLockscreenWallpaper) {
-        mHasLockscreenWallpaper = hasLockscreenWallpaper;
-    }
-
     public interface StateCallback {
         void onTrustedChanged();
         void onShowingChanged();
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/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/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/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/wm/AnrController.java b/services/core/java/com/android/server/wm/AnrController.java
index 91f650f..38e1c99 100644
--- a/services/core/java/com/android/server/wm/AnrController.java
+++ b/services/core/java/com/android/server/wm/AnrController.java
@@ -87,16 +87,12 @@
                 return;
             }
 
-            WindowState windowState = target.asWindowState();
+            WindowState windowState = target.getWindowState();
             pid = target.getPid();
-            if (windowState != null) {
-                activity = windowState.mActivityRecord;
-            } else {
-                // Don't blame the host process, instead blame the embedded pid.
-                activity = null;
-                // Use host WindowState for logging and z-order test.
-                windowState = target.asEmbeddedWindow().mHostWindowState;
-            }
+            // 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);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 5e6f234..b13f622 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -90,6 +90,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 +128,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;
@@ -986,8 +986,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) {
@@ -5126,9 +5126,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
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 881bd35..c9a8d94 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -913,15 +913,6 @@
                 // letterboxed. Hence always let them extend under the cutout.
                 attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
                 break;
-            case TYPE_NOTIFICATION_SHADE:
-                // If the Keyguard is in a hidden state (occluded by another window), we force to
-                // remove the wallpaper and keyguard flag so that any change in-flight after setting
-                // the keyguard as occluded wouldn't set these flags again.
-                // See {@link #processKeyguardSetHiddenResultLw}.
-                if (mService.mPolicy.isKeyguardOccluded()) {
-                    attrs.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
-                }
-                break;
 
             case TYPE_TOAST:
                 // While apps should use the dedicated toast APIs to add such windows
diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
index dcd1148..fc317a1 100644
--- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java
+++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java
@@ -198,8 +198,8 @@
         }
 
         @Override
-        public EmbeddedWindow asEmbeddedWindow() {
-            return this;
+        public WindowState getWindowState() {
+            return mHostWindowState;
         }
 
         @Override
diff --git a/services/core/java/com/android/server/wm/InputTarget.java b/services/core/java/com/android/server/wm/InputTarget.java
index fec7cc9..c7d328a 100644
--- a/services/core/java/com/android/server/wm/InputTarget.java
+++ b/services/core/java/com/android/server/wm/InputTarget.java
@@ -25,13 +25,8 @@
  * of both targets.
  */
 interface InputTarget {
-    default WindowState asWindowState() {
-        return null;
-    }
-
-    default EmbeddedWindowController.EmbeddedWindow asEmbeddedWindow() {
-        return null;
-    }
+    /* Get the WindowState associated with the target. */
+    WindowState getWindowState();
 
     /* Display id of the target. */
     int getDisplayId();
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index d9f0091..0f7c649 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;
@@ -79,7 +80,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;
@@ -894,7 +894,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",
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 51ecce0..ac9f924 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -940,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
      */
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 cf10e70..f11fa16 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -2583,13 +2583,17 @@
             // an exit.
             win.mAnimatingExit = true;
         } else if (win.mDisplayContent.okToAnimate()
-                && win.mDisplayContent.mWallpaperController.isWallpaperTarget(win)) {
-            // If the wallpaper is currently behind this
-            // window, we need to change both of them inside
-            // of a transaction to avoid artifacts.
+                && win.mDisplayContent.mWallpaperController.isWallpaperTarget(win)
+                && win.mAttrs.type == TYPE_NOTIFICATION_SHADE) {
+            // If the wallpaper is currently behind this app window, we need to change both of them
+            // inside of a transaction to avoid artifacts.
+            // For NotificationShade, sysui is in charge of running window animation and it updates
+            // the client view visibility only after both NotificationShade and the wallpaper are
+            // hidden. So we don't need to care about exit animation, but can destroy its surface
+            // immediately.
             win.mAnimatingExit = true;
         } else {
-            boolean stopped = win.mActivityRecord != null ? win.mActivityRecord.mAppStopped : true;
+            boolean stopped = win.mActivityRecord == null || win.mActivityRecord.mAppStopped;
             // We set mDestroying=true so ActivityRecord#notifyAppStopped in-to destroy surfaces
             // will later actually destroy the surface if we do not do so here. Normally we leave
             // this to the exit animation.
@@ -5014,16 +5018,17 @@
             ProtoLog.i(WM_DEBUG_FOCUS_LIGHT, "Focus changing: %s -> %s", lastTarget, newTarget);
         }
 
-        if (newTarget != null && newTarget.asWindowState() != null) {
-            WindowState newFocus = newTarget.asWindowState();
-            mAnrController.onFocusChanged(newFocus);
-            newFocus.reportFocusChangedSerialized(true);
+        // 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 (lastTarget != null && lastTarget.asWindowState() != null) {
-            WindowState lastFocus = lastTarget.asWindowState();
-            lastFocus.reportFocusChangedSerialized(false);
+        WindowState lastFocusedWindow = lastTarget != null ? lastTarget.getWindowState() : null;
+        if (lastFocusedWindow != null && lastFocusedWindow.mInputChannelToken == oldToken) {
+            lastFocusedWindow.reportFocusChangedSerialized(false);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index a3d1378..9a43f6b 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1727,7 +1727,7 @@
     }
 
     @Override
-    public WindowState asWindowState() {
+    public WindowState getWindowState() {
         return this;
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 0447409..34576a6 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -10701,7 +10701,7 @@
     }
 
     @Override
-    public void resetNewUserDisclaimer() {
+    public void acknowledgeNewUserDisclaimer() {
         CallerIdentity callerIdentity = getCallerIdentity();
         canManageUsers(callerIdentity);
 
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/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..e2a37ee
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java
@@ -0,0 +1,222 @@
+/*
+ * 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.doReturn;
+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.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 com.android.server.pm.UserManagerInternal;
+
+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.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;
+
+    @Mock
+    private InternalResourceService mIrs;
+    @Mock
+    private UserManagerInternal mUserManagerInternal;
+
+    private Context getContext() {
+        return InstrumentationRegistry.getContext();
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        mMockingSession = mockitoSession()
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .mockStatic(LocalServices.class)
+                .startMocking();
+        doReturn(mUserManagerInternal)
+                .when(() -> LocalServices.getService(UserManagerInternal.class));
+        when(mIrs.getLock()).thenReturn(new Object());
+        when(mIrs.isEnabled()).thenReturn(true);
+        when(mUserManagerInternal.getUserIds()).thenReturn(new int[]{TEST_USER_ID});
+        mTestFileDir = new File(getContext().getFilesDir(), "scribe_test");
+        //noinspection ResultOfMethodCallIgnored
+        mTestFileDir.mkdirs();
+        Log.d(TAG, "Saving data to '" + mTestFileDir + "'");
+        mScribeUnderTest = new Scribe(mIrs, mTestFileDir);
+    }
+
+    @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;
+        final int[] userIds = new int[numUsers];
+        when(mUserManagerInternal.getUserIds()).thenReturn(userIds);
+        for (int u = 0; u < numUsers; ++u) {
+            final int userId = TEST_USER_ID + u;
+            userIds[u] = userId;
+            for (int l = 0; l < numLedgers; ++l) {
+                final String pkgName = TEST_PACKAGE + l;
+                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));
+    }
+
+    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);
+    }
+}
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/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/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index bbeb980..db7def8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -43,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;
@@ -442,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());
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/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/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 7257dd8..edfb7bf 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -1319,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";
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);
-    }
-}