Merge "Address failing tests for CtsMultiUserHostTest."
diff --git a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
index 4242cf8..5d67c96 100644
--- a/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
+++ b/apex/jobscheduler/framework/java/android/app/JobSchedulerImpl.java
@@ -17,6 +17,7 @@
 package android.app;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.app.job.IJobScheduler;
 import android.app.job.IUserVisibleJobObserver;
@@ -25,10 +26,13 @@
 import android.app.job.JobSnapshot;
 import android.app.job.JobWorkItem;
 import android.content.Context;
+import android.content.pm.ParceledListSlice;
 import android.os.RemoteException;
+import android.util.ArrayMap;
 
 import java.util.List;
-
+import java.util.Map;
+import java.util.Set;
 
 /**
  * Concrete implementation of the JobScheduler interface
@@ -41,16 +45,42 @@
 public class JobSchedulerImpl extends JobScheduler {
     IJobScheduler mBinder;
     private final Context mContext;
+    private final String mNamespace;
 
     public JobSchedulerImpl(@NonNull Context context, IJobScheduler binder) {
+        this(context, binder, null);
+    }
+
+    private JobSchedulerImpl(@NonNull Context context, IJobScheduler binder,
+            @Nullable String namespace) {
         mContext = context;
         mBinder = binder;
+        mNamespace = namespace;
+    }
+
+    private JobSchedulerImpl(JobSchedulerImpl jsi, @Nullable String namespace) {
+        this(jsi.mContext, jsi.mBinder, namespace);
+    }
+
+    @NonNull
+    @Override
+    public JobScheduler forNamespace(@NonNull String namespace) {
+        if (namespace == null) {
+            throw new IllegalArgumentException("namespace cannot be null");
+        }
+        return new JobSchedulerImpl(this, namespace);
+    }
+
+    @Nullable
+    @Override
+    public String getNamespace() {
+        return mNamespace;
     }
 
     @Override
     public int schedule(JobInfo job) {
         try {
-            return mBinder.schedule(job);
+            return mBinder.schedule(mNamespace, job);
         } catch (RemoteException e) {
             return JobScheduler.RESULT_FAILURE;
         }
@@ -59,7 +89,7 @@
     @Override
     public int enqueue(JobInfo job, JobWorkItem work) {
         try {
-            return mBinder.enqueue(job, work);
+            return mBinder.enqueue(mNamespace, job, work);
         } catch (RemoteException e) {
             return JobScheduler.RESULT_FAILURE;
         }
@@ -68,7 +98,7 @@
     @Override
     public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag) {
         try {
-            return mBinder.scheduleAsPackage(job, packageName, userId, tag);
+            return mBinder.scheduleAsPackage(mNamespace, job, packageName, userId, tag);
         } catch (RemoteException e) {
             return JobScheduler.RESULT_FAILURE;
         }
@@ -77,23 +107,44 @@
     @Override
     public void cancel(int jobId) {
         try {
-            mBinder.cancel(jobId);
+            mBinder.cancel(mNamespace, jobId);
         } catch (RemoteException e) {}
-
     }
 
     @Override
     public void cancelAll() {
         try {
+            mBinder.cancelAllInNamespace(mNamespace);
+        } catch (RemoteException e) {}
+    }
+
+    @Override
+    public void cancelInAllNamespaces() {
+        try {
             mBinder.cancelAll();
         } catch (RemoteException e) {}
-
     }
 
     @Override
     public List<JobInfo> getAllPendingJobs() {
         try {
-            return mBinder.getAllPendingJobs().getList();
+            return mBinder.getAllPendingJobsInNamespace(mNamespace).getList();
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    @Override
+    public Map<String, List<JobInfo>> getPendingJobsInAllNamespaces() {
+        try {
+            final Map<String, ParceledListSlice<JobInfo>> parceledList =
+                    mBinder.getAllPendingJobs();
+            final ArrayMap<String, List<JobInfo>> jobMap = new ArrayMap<>();
+            final Set<String> keys = parceledList.keySet();
+            for (String key : keys) {
+                jobMap.put(key, parceledList.get(key).getList());
+            }
+            return jobMap;
         } catch (RemoteException e) {
             return null;
         }
@@ -102,7 +153,7 @@
     @Override
     public JobInfo getPendingJob(int jobId) {
         try {
-            return mBinder.getPendingJob(jobId);
+            return mBinder.getPendingJob(mNamespace, jobId);
         } catch (RemoteException e) {
             return null;
         }
@@ -111,7 +162,7 @@
     @Override
     public int getPendingJobReason(int jobId) {
         try {
-            return mBinder.getPendingJobReason(jobId);
+            return mBinder.getPendingJobReason(mNamespace, jobId);
         } catch (RemoteException e) {
             return PENDING_JOB_REASON_UNDEFINED;
         }
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
index c87a2af..a1f1954 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobScheduler.aidl
@@ -21,20 +21,24 @@
 import android.app.job.JobSnapshot;
 import android.app.job.JobWorkItem;
 import android.content.pm.ParceledListSlice;
+import java.util.Map;
 
  /**
   * IPC interface that supports the app-facing {@link #JobScheduler} api.
   * {@hide}
   */
 interface IJobScheduler {
-    int schedule(in JobInfo job);
-    int enqueue(in JobInfo job, in JobWorkItem work);
-    int scheduleAsPackage(in JobInfo job, String packageName, int userId, String tag);
-    void cancel(int jobId);
+    int schedule(String namespace, in JobInfo job);
+    int enqueue(String namespace, in JobInfo job, in JobWorkItem work);
+    int scheduleAsPackage(String namespace, in JobInfo job, String packageName, int userId, String tag);
+    void cancel(String namespace, int jobId);
     void cancelAll();
-    ParceledListSlice getAllPendingJobs();
-    JobInfo getPendingJob(int jobId);
-    int getPendingJobReason(int jobId);
+    void cancelAllInNamespace(String namespace);
+    // Returns Map<String, ParceledListSlice>, where the keys are the namespaces.
+    Map<String, ParceledListSlice<JobInfo>> getAllPendingJobs();
+    ParceledListSlice<JobInfo> getAllPendingJobsInNamespace(String namespace);
+    JobInfo getPendingJob(String namespace, int jobId);
+    int getPendingJobReason(String namespace, int jobId);
     boolean canRunLongJobs(String packageName);
     boolean hasRunLongJobsPermission(String packageName, int userId);
     List<JobInfo> getStartedJobs();
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index deb97a5..3bbbb15 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -1251,6 +1251,9 @@
          * in them all being treated the same. The priorities each have slightly different
          * behaviors, as noted in their relevant javadoc.
          *
+         * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
+         * the priority will only affect sorting order within the job's namespace.
+         *
          * <b>NOTE:</b> Setting all of your jobs to high priority will not be
          * beneficial to your app and in fact may hurt its performance in the
          * long run.
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index a5a7f93..18ddffb 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -277,6 +277,8 @@
 
     @UnsupportedAppUsage
     private final int jobId;
+    @Nullable
+    private final String mJobNamespace;
     private final PersistableBundle extras;
     private final Bundle transientExtras;
     private final ClipData clipData;
@@ -295,7 +297,7 @@
     private String debugStopReason; // Human readable stop reason for debugging.
 
     /** @hide */
-    public JobParameters(IBinder callback, int jobId, PersistableBundle extras,
+    public JobParameters(IBinder callback, String namespace, int jobId, PersistableBundle extras,
             Bundle transientExtras, ClipData clipData, int clipGrantFlags,
             boolean overrideDeadlineExpired, boolean isExpedited,
             boolean isUserInitiated, Uri[] triggeredContentUris,
@@ -312,6 +314,7 @@
         this.mTriggeredContentUris = triggeredContentUris;
         this.mTriggeredContentAuthorities = triggeredContentAuthorities;
         this.network = network;
+        this.mJobNamespace = namespace;
     }
 
     /**
@@ -322,6 +325,19 @@
     }
 
     /**
+     * Get the namespace this job was placed in.
+     *
+     * @see JobScheduler#forNamespace(String)
+     * @return The namespace this job was scheduled in. Will be {@code null} if there was no
+     * explicit namespace set and this job is therefore in the default namespace.
+     * @hide
+     */
+    @Nullable
+    public String getJobNamespace() {
+        return mJobNamespace;
+    }
+
+    /**
      * @return The reason {@link JobService#onStopJob(JobParameters)} was called on this job. Will
      * be {@link #STOP_REASON_UNDEFINED} if {@link JobService#onStopJob(JobParameters)} has not
      * yet been called.
@@ -540,6 +556,7 @@
 
     private JobParameters(Parcel in) {
         jobId = in.readInt();
+        mJobNamespace = in.readString();
         extras = in.readPersistableBundle();
         transientExtras = in.readBundle();
         if (in.readInt() != 0) {
@@ -581,6 +598,7 @@
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeInt(jobId);
+        dest.writeString(mJobNamespace);
         dest.writePersistableBundle(extras);
         dest.writeBundle(transientExtras);
         if (clipData != null) {
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index c9981da..3764249 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -38,6 +38,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
+import java.util.Map;
 
 /**
  * This is an API for scheduling various types of jobs against the framework that will be executed
@@ -264,6 +265,31 @@
     }
 
     /**
+     * Get a JobScheduler instance that is dedicated to a specific namespace. Any API calls using
+     * this instance will interact with jobs in that namespace, unless the API documentation says
+     * otherwise. Attempting to update a job scheduled in another namespace will not be possible
+     * but will instead create or update the job inside the current namespace. A JobScheduler
+     * instance dedicated to a namespace must be used to schedule or update jobs in that namespace.
+     * @see #getNamespace()
+     * @hide
+     */
+    @NonNull
+    public JobScheduler forNamespace(@NonNull String namespace) {
+        throw new RuntimeException("Not implemented. Must override in a subclass.");
+    }
+
+    /**
+     * Get the namespace this JobScheduler instance is operating in. A {@code null} value means
+     * that the app has not specified a namespace for this instance, and it is therefore using the
+     * default namespace.
+     * @hide
+     */
+    @Nullable
+    public String getNamespace() {
+        throw new RuntimeException("Not implemented. Must override in a subclass.");
+    }
+
+    /**
      * Schedule a job to be executed.  Will replace any currently scheduled job with the same
      * ID with the new information in the {@link JobInfo}.  If a job with the given ID is currently
      * running, it will be stopped.
@@ -364,6 +390,15 @@
     public abstract void cancelAll();
 
     /**
+     * Cancel <em>all</em> jobs that have been scheduled by the calling application, regardless of
+     * namespace.
+     * @hide
+     */
+    public void cancelInAllNamespaces() {
+        throw new RuntimeException("Not implemented. Must override in a subclass.");
+    }
+
+    /**
      * Retrieve all jobs that have been scheduled by the calling application.
      *
      * @return a list of all of the app's scheduled jobs.  This includes jobs that are
@@ -372,6 +407,21 @@
     public abstract @NonNull List<JobInfo> getAllPendingJobs();
 
     /**
+     * Retrieve all jobs that have been scheduled by the calling application within the current
+     * namespace.
+     *
+     * @return a list of all of the app's scheduled jobs scheduled with the current namespace.
+     * If a namespace hasn't been explicitly set with {@link #forNamespace(String)},
+     * then this will return jobs in the default namespace.
+     * This includes jobs that are currently started as well as those that are still waiting to run.
+     * @hide
+     */
+    @NonNull
+    public Map<String, List<JobInfo>> getPendingJobsInAllNamespaces() {
+        throw new RuntimeException("Not implemented. Must override in a subclass.");
+    }
+
+    /**
      * Look up the description of a scheduled job.
      *
      * @return The {@link JobInfo} description of the given scheduled job, or {@code null}
diff --git a/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java b/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java
index afcbe7d..311a9b2 100644
--- a/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java
+++ b/apex/jobscheduler/framework/java/android/app/job/UserVisibleJobSummary.java
@@ -17,9 +17,12 @@
 package android.app.job;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.util.Objects;
+
 /**
  * Summary of a scheduled job that the user is meant to be aware of.
  *
@@ -30,13 +33,16 @@
     private final int mSourceUserId;
     @NonNull
     private final String mSourcePackageName;
+    @Nullable
+    private final String mNamespace;
     private final int mJobId;
 
     public UserVisibleJobSummary(int callingUid, int sourceUserId,
-            @NonNull String sourcePackageName, int jobId) {
+            @NonNull String sourcePackageName, String namespace, int jobId) {
         mCallingUid = callingUid;
         mSourceUserId = sourceUserId;
         mSourcePackageName = sourcePackageName;
+        mNamespace = namespace;
         mJobId = jobId;
     }
 
@@ -44,6 +50,7 @@
         mCallingUid = in.readInt();
         mSourceUserId = in.readInt();
         mSourcePackageName = in.readString();
+        mNamespace = in.readString();
         mJobId = in.readInt();
     }
 
@@ -55,6 +62,10 @@
         return mJobId;
     }
 
+    public String getNamespace() {
+        return mNamespace;
+    }
+
     public int getSourceUserId() {
         return mSourceUserId;
     }
@@ -71,6 +82,7 @@
         return mCallingUid == that.mCallingUid
                 && mSourceUserId == that.mSourceUserId
                 && mSourcePackageName.equals(that.mSourcePackageName)
+                && Objects.equals(mNamespace, that.mNamespace)
                 && mJobId == that.mJobId;
     }
 
@@ -80,6 +92,9 @@
         result = 31 * result + mCallingUid;
         result = 31 * result + mSourceUserId;
         result = 31 * result + mSourcePackageName.hashCode();
+        if (mNamespace != null) {
+            result = 31 * result + mNamespace.hashCode();
+        }
         result = 31 * result + mJobId;
         return result;
     }
@@ -90,6 +105,7 @@
                 + "callingUid=" + mCallingUid
                 + ", sourceUserId=" + mSourceUserId
                 + ", sourcePackageName='" + mSourcePackageName + "'"
+                + ", namespace=" + mNamespace
                 + ", jobId=" + mJobId
                 + "}";
     }
@@ -104,6 +120,7 @@
         dest.writeInt(mCallingUid);
         dest.writeInt(mSourceUserId);
         dest.writeString(mSourcePackageName);
+        dest.writeString(mNamespace);
         dest.writeInt(mJobId);
     }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index 397d2c4..b806ef8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -615,7 +615,7 @@
     private boolean isSimilarJobRunningLocked(JobStatus job) {
         for (int i = mRunningJobs.size() - 1; i >= 0; --i) {
             JobStatus js = mRunningJobs.valueAt(i);
-            if (job.getUid() == js.getUid() && job.getJobId() == js.getJobId()) {
+            if (job.matches(js.getUid(), js.getNamespace(), js.getJobId())) {
                 return true;
             }
         }
@@ -1687,12 +1687,12 @@
 
     @GuardedBy("mLock")
     boolean executeTimeoutCommandLocked(PrintWriter pw, String pkgName, int userId,
-            boolean hasJobId, int jobId) {
+            @Nullable String namespace, boolean hasJobId, int jobId) {
         boolean foundSome = false;
         for (int i = 0; i < mActiveServices.size(); i++) {
             final JobServiceContext jc = mActiveServices.get(i);
             final JobStatus js = jc.getRunningJobLocked();
-            if (jc.timeoutIfExecutingLocked(pkgName, userId, hasJobId, jobId, "shell")) {
+            if (jc.timeoutIfExecutingLocked(pkgName, userId, namespace, hasJobId, jobId, "shell")) {
                 foundSome = true;
                 pw.print("Timing out: ");
                 js.printUniqueId(pw);
@@ -1709,11 +1709,13 @@
      */
     @Nullable
     @GuardedBy("mLock")
-    Pair<Long, Long> getEstimatedNetworkBytesLocked(String pkgName, int uid, int jobId) {
+    Pair<Long, Long> getEstimatedNetworkBytesLocked(String pkgName, int uid,
+            String namespace, int jobId) {
         for (int i = 0; i < mActiveServices.size(); i++) {
             final JobServiceContext jc = mActiveServices.get(i);
             final JobStatus js = jc.getRunningJobLocked();
-            if (js != null && js.matches(uid, jobId) && js.getSourcePackageName().equals(pkgName)) {
+            if (js != null && js.matches(uid, namespace, jobId)
+                    && js.getSourcePackageName().equals(pkgName)) {
                 return jc.getEstimatedNetworkBytes();
             }
         }
@@ -1726,11 +1728,13 @@
      */
     @Nullable
     @GuardedBy("mLock")
-    Pair<Long, Long> getTransferredNetworkBytesLocked(String pkgName, int uid, int jobId) {
+    Pair<Long, Long> getTransferredNetworkBytesLocked(String pkgName, int uid,
+            String namespace, int jobId) {
         for (int i = 0; i < mActiveServices.size(); i++) {
             final JobServiceContext jc = mActiveServices.get(i);
             final JobStatus js = jc.getRunningJobLocked();
-            if (js != null && js.matches(uid, jobId) && js.getSourcePackageName().equals(pkgName)) {
+            if (js != null && js.matches(uid, namespace, jobId)
+                    && js.getSourcePackageName().equals(pkgName)) {
                 return jc.getTransferredNetworkBytes();
             }
         }
@@ -1753,6 +1757,9 @@
         pendingJobQueue.resetIterator();
         while ((js = pendingJobQueue.next()) != null) {
             s.append("(")
+                    .append("{")
+                    .append(js.getNamespace())
+                    .append("} ")
                     .append(js.getJob().getId())
                     .append(", ")
                     .append(js.getUid())
@@ -1777,6 +1784,9 @@
                 if (job == null) {
                     s.append("nothing");
                 } else {
+                    if (job.getNamespace() != null) {
+                        s.append(job.getNamespace()).append(":");
+                    }
                     s.append(job.getJobId()).append("/").append(job.getUid());
                 }
                 s.append(")");
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 c032513..c7a2997 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -89,6 +89,7 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseArrayMap;
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 import android.util.SparseSetArray;
@@ -149,6 +150,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
@@ -377,8 +379,12 @@
     @GuardedBy("mLock")
     private final ArraySet<JobStatus> mChangedJobList = new ArraySet<>();
 
+    /**
+     * Cached pending job reasons. Mapping from UID -> namespace -> job ID -> reason.
+     */
     @GuardedBy("mPendingJobReasonCache") // Use its own lock to avoid blocking JS processing
-    private final SparseArray<SparseIntArray> mPendingJobReasonCache = new SparseArray<>();
+    private final SparseArrayMap<String, SparseIntArray> mPendingJobReasonCache =
+            new SparseArrayMap<>();
 
     /**
      * Named indices into standby bucket arrays, for clarity in referring to
@@ -1333,7 +1339,7 @@
     private final Predicate<Integer> mIsUidActivePredicate = this::isUidActive;
 
     public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName,
-            int userId, String tag) {
+            int userId, @Nullable String namespace, String tag) {
         // Rate limit excessive schedule() calls.
         final String servicePkg = job.getService().getPackageName();
         if (job.isPersisted() && (packageName == null || packageName.equals(servicePkg))) {
@@ -1392,7 +1398,7 @@
         }
 
         synchronized (mLock) {
-            final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());
+            final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, namespace, job.getId());
 
             if (work != null && toCancel != null) {
                 // Fast path: we are adding work to an existing job, and the JobInfo is not
@@ -1409,7 +1415,8 @@
                 }
             }
 
-            JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag);
+            JobStatus jobStatus =
+                    JobStatus.createFromJobInfo(job, uId, packageName, userId, namespace, tag);
 
             // Return failure early if expedited job quota used up.
             if (jobStatus.isRequestedExpeditedJob()) {
@@ -1504,26 +1511,47 @@
         return JobScheduler.RESULT_SUCCESS;
     }
 
-    public List<JobInfo> getPendingJobs(int uid) {
+    private ArrayMap<String, List<JobInfo>> getPendingJobs(int uid) {
+        final ArrayMap<String, List<JobInfo>> outMap = new ArrayMap<>();
+        synchronized (mLock) {
+            ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
+            // Write out for loop to avoid addAll() creating an Iterator.
+            for (int i = jobs.size() - 1; i >= 0; i--) {
+                final JobStatus job = jobs.valueAt(i);
+                List<JobInfo> outList = outMap.get(job.getNamespace());
+                if (outList == null) {
+                    outList = new ArrayList<JobInfo>(jobs.size());
+                    outMap.put(job.getNamespace(), outList);
+                }
+
+                outList.add(job.getJob());
+            }
+            return outMap;
+        }
+    }
+
+    private List<JobInfo> getPendingJobsInNamespace(int uid, @Nullable String namespace) {
         synchronized (mLock) {
             ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
             ArrayList<JobInfo> outList = new ArrayList<JobInfo>(jobs.size());
             // Write out for loop to avoid addAll() creating an Iterator.
             for (int i = jobs.size() - 1; i >= 0; i--) {
                 final JobStatus job = jobs.valueAt(i);
-                outList.add(job.getJob());
+                if (Objects.equals(namespace, job.getNamespace())) {
+                    outList.add(job.getJob());
+                }
             }
             return outList;
         }
     }
 
     @JobScheduler.PendingJobReason
-    private int getPendingJobReason(int uid, int jobId) {
+    private int getPendingJobReason(int uid, String namespace, int jobId) {
         int reason;
         // Some apps may attempt to query this frequently, so cache the reason under a separate lock
         // so that the rest of JS processing isn't negatively impacted.
         synchronized (mPendingJobReasonCache) {
-            SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid);
+            SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid, namespace);
             if (jobIdToReason != null) {
                 reason = jobIdToReason.get(jobId, JobScheduler.PENDING_JOB_REASON_UNDEFINED);
                 if (reason != JobScheduler.PENDING_JOB_REASON_UNDEFINED) {
@@ -1532,16 +1560,17 @@
             }
         }
         synchronized (mLock) {
-            reason = getPendingJobReasonLocked(uid, jobId);
+            reason = getPendingJobReasonLocked(uid, namespace, jobId);
             if (DEBUG) {
-                Slog.v(TAG, "getPendingJobReason(" + uid + "," + jobId + ")=" + reason);
+                Slog.v(TAG, "getPendingJobReason("
+                        + uid + "," + namespace + "," + jobId + ")=" + reason);
             }
         }
         synchronized (mPendingJobReasonCache) {
-            SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid);
+            SparseIntArray jobIdToReason = mPendingJobReasonCache.get(uid, namespace);
             if (jobIdToReason == null) {
                 jobIdToReason = new SparseIntArray();
-                mPendingJobReasonCache.put(uid, jobIdToReason);
+                mPendingJobReasonCache.add(uid, namespace, jobIdToReason);
             }
             jobIdToReason.put(jobId, reason);
         }
@@ -1550,10 +1579,10 @@
 
     @JobScheduler.PendingJobReason
     @GuardedBy("mLock")
-    private int getPendingJobReasonLocked(int uid, int jobId) {
+    private int getPendingJobReasonLocked(int uid, String namespace, int jobId) {
         // Very similar code to isReadyToBeExecutedLocked.
 
-        JobStatus job = mJobs.getJobByUidAndJobId(uid, jobId);
+        JobStatus job = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
         if (job == null) {
             // Job doesn't exist.
             return JobScheduler.PENDING_JOB_REASON_INVALID_JOB_ID;
@@ -1645,12 +1674,12 @@
         return JobScheduler.PENDING_JOB_REASON_UNDEFINED;
     }
 
-    public JobInfo getPendingJob(int uid, int jobId) {
+    private JobInfo getPendingJob(int uid, @Nullable String namespace, int jobId) {
         synchronized (mLock) {
             ArraySet<JobStatus> jobs = mJobs.getJobsByUid(uid);
             for (int i = jobs.size() - 1; i >= 0; i--) {
                 JobStatus job = jobs.valueAt(i);
-                if (job.getJobId() == jobId) {
+                if (job.getJobId() == jobId && Objects.equals(namespace, job.getNamespace())) {
                     return job.getJob();
                 }
             }
@@ -1726,12 +1755,20 @@
      * This will remove the job from the master list, and cancel the job if it was staged for
      * execution or being executed.
      *
-     * @param uid Uid to check against for removal of a job.
+     * @param uid              Uid to check against for removal of a job.
      * @param includeSourceApp Whether to include jobs scheduled for this UID by another UID.
      *                         If false, only jobs scheduled by this UID will be cancelled.
      */
     public boolean cancelJobsForUid(int uid, boolean includeSourceApp,
             @JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
+        return cancelJobsForUid(uid, includeSourceApp,
+                /* namespaceOnly */ false, /* namespace */ null,
+                reason, internalReasonCode, debugReason);
+    }
+
+    private boolean cancelJobsForUid(int uid, boolean includeSourceApp,
+            boolean namespaceOnly, @Nullable String namespace,
+            @JobParameters.StopReason int reason, int internalReasonCode, String debugReason) {
         if (uid == Process.SYSTEM_UID) {
             Slog.wtfStack(TAG, "Can't cancel all jobs for system uid");
             return false;
@@ -1748,8 +1785,10 @@
             }
             for (int i = 0; i < jobsForUid.size(); i++) {
                 JobStatus toRemove = jobsForUid.valueAt(i);
-                cancelJobImplLocked(toRemove, null, reason, internalReasonCode, debugReason);
-                jobsCanceled = true;
+                if (!namespaceOnly || Objects.equals(namespace, toRemove.getNamespace())) {
+                    cancelJobImplLocked(toRemove, null, reason, internalReasonCode, debugReason);
+                    jobsCanceled = true;
+                }
             }
         }
         return jobsCanceled;
@@ -1763,11 +1802,11 @@
      * @param uid   Uid of the calling client.
      * @param jobId Id of the job, provided at schedule-time.
      */
-    private boolean cancelJob(int uid, int jobId, int callingUid,
+    private boolean cancelJob(int uid, String namespace, int jobId, int callingUid,
             @JobParameters.StopReason int reason) {
         JobStatus toCancel;
         synchronized (mLock) {
-            toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
+            toCancel = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
             if (toCancel != null) {
                 cancelJobImplLocked(toCancel, null, reason,
                         JobParameters.INTERNAL_STOP_REASON_CANCELED,
@@ -2197,7 +2236,8 @@
         jobStatus.stopTrackingJobLocked(incomingJob);
 
         synchronized (mPendingJobReasonCache) {
-            SparseIntArray reasonCache = mPendingJobReasonCache.get(jobStatus.getUid());
+            SparseIntArray reasonCache =
+                    mPendingJobReasonCache.get(jobStatus.getUid(), jobStatus.getNamespace());
             if (reasonCache != null) {
                 reasonCache.delete(jobStatus.getJobId());
             }
@@ -2228,7 +2268,8 @@
     /** Remove the pending job reason for this job from the cache. */
     void resetPendingJobReasonCache(@NonNull JobStatus jobStatus) {
         synchronized (mPendingJobReasonCache) {
-            final SparseIntArray reasons = mPendingJobReasonCache.get(jobStatus.getUid());
+            final SparseIntArray reasons =
+                    mPendingJobReasonCache.get(jobStatus.getUid(), jobStatus.getNamespace());
             if (reasons != null) {
                 reasons.delete(jobStatus.getJobId());
             }
@@ -2490,7 +2531,8 @@
             if (DEBUG) {
                 Slog.d(TAG, "Could not find job to remove. Was job removed while executing?");
             }
-            JobStatus newJs = mJobs.getJobByUidAndJobId(jobStatus.getUid(), jobStatus.getJobId());
+            JobStatus newJs = mJobs.getJobByUidAndJobId(
+                    jobStatus.getUid(), jobStatus.getNamespace(), jobStatus.getJobId());
             if (newJs != null) {
                 // This job was stopped because the app scheduled a new job with the same job ID.
                 // Check if the new job is ready to run.
@@ -2947,10 +2989,12 @@
                     synchronized (mPendingJobReasonCache) {
                         for (int i = 0; i < numRunnableJobs; ++i) {
                             final JobStatus job = runnableJobs.get(i);
-                            SparseIntArray reasons = mPendingJobReasonCache.get(job.getUid());
+                            SparseIntArray reasons =
+                                    mPendingJobReasonCache.get(job.getUid(), job.getNamespace());
                             if (reasons == null) {
                                 reasons = new SparseIntArray();
-                                mPendingJobReasonCache.put(job.getUid(), reasons);
+                                mPendingJobReasonCache
+                                        .add(job.getUid(), job.getNamespace(), reasons);
                             }
                             // We're force batching these jobs, so consider it an optimization
                             // policy reason.
@@ -3749,7 +3793,7 @@
 
         // IJobScheduler implementation
         @Override
-        public int schedule(JobInfo job) throws RemoteException {
+        public int schedule(String namespace, JobInfo job) throws RemoteException {
             if (DEBUG) {
                 Slog.d(TAG, "Scheduling job: " + job.toString());
             }
@@ -3770,10 +3814,14 @@
                 return result;
             }
 
+            if (namespace != null) {
+                namespace = namespace.intern();
+            }
+
             final long ident = Binder.clearCallingIdentity();
             try {
                 return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, userId,
-                        null);
+                        namespace, null);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -3781,7 +3829,7 @@
 
         // IJobScheduler implementation
         @Override
-        public int enqueue(JobInfo job, JobWorkItem work) throws RemoteException {
+        public int enqueue(String namespace, JobInfo job, JobWorkItem work) throws RemoteException {
             if (DEBUG) {
                 Slog.d(TAG, "Enqueueing job: " + job.toString() + " work: " + work);
             }
@@ -3801,18 +3849,22 @@
                 return result;
             }
 
+            if (namespace != null) {
+                namespace = namespace.intern();
+            }
+
             final long ident = Binder.clearCallingIdentity();
             try {
                 return JobSchedulerService.this.scheduleAsPackage(job, work, uid, null, userId,
-                        null);
+                        namespace, null);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
         }
 
         @Override
-        public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag)
-                throws RemoteException {
+        public int scheduleAsPackage(String namespace, JobInfo job, String packageName, int userId,
+                String tag) throws RemoteException {
             final int callerUid = Binder.getCallingUid();
             if (DEBUG) {
                 Slog.d(TAG, "Caller uid " + callerUid + " scheduling job: " + job.toString()
@@ -3835,46 +3887,70 @@
                 return result;
             }
 
+            if (namespace != null) {
+                namespace = namespace.intern();
+            }
+
             final long ident = Binder.clearCallingIdentity();
             try {
                 return JobSchedulerService.this.scheduleAsPackage(job, null, callerUid,
-                        packageName, userId, tag);
+                        packageName, userId, namespace, tag);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
         }
 
         @Override
-        public ParceledListSlice<JobInfo> getAllPendingJobs() throws RemoteException {
+        public Map<String, ParceledListSlice<JobInfo>> getAllPendingJobs() throws RemoteException {
             final int uid = Binder.getCallingUid();
 
             final long ident = Binder.clearCallingIdentity();
             try {
-                return new ParceledListSlice<>(JobSchedulerService.this.getPendingJobs(uid));
+                final ArrayMap<String, List<JobInfo>> jobs =
+                        JobSchedulerService.this.getPendingJobs(uid);
+                final ArrayMap<String, ParceledListSlice<JobInfo>> outMap = new ArrayMap<>();
+                for (int i = 0; i < jobs.size(); ++i) {
+                    outMap.put(jobs.keyAt(i), new ParceledListSlice<>(jobs.valueAt(i)));
+                }
+                return outMap;
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
         }
 
         @Override
-        public int getPendingJobReason(int jobId) throws RemoteException {
+        public ParceledListSlice<JobInfo> getAllPendingJobsInNamespace(String namespace)
+                throws RemoteException {
             final int uid = Binder.getCallingUid();
 
             final long ident = Binder.clearCallingIdentity();
             try {
-                return JobSchedulerService.this.getPendingJobReason(uid, jobId);
+                return new ParceledListSlice<>(
+                        JobSchedulerService.this.getPendingJobsInNamespace(uid, namespace));
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
         }
 
         @Override
-        public JobInfo getPendingJob(int jobId) throws RemoteException {
+        public JobInfo getPendingJob(String namespace, int jobId) throws RemoteException {
             final int uid = Binder.getCallingUid();
 
             final long ident = Binder.clearCallingIdentity();
             try {
-                return JobSchedulerService.this.getPendingJob(uid, jobId);
+                return JobSchedulerService.this.getPendingJob(uid, namespace, jobId);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
+        @Override
+        public int getPendingJobReason(String namespace, int jobId) throws RemoteException {
+            final int uid = Binder.getCallingUid();
+
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                return JobSchedulerService.this.getPendingJobReason(uid, namespace, jobId);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -3897,12 +3973,29 @@
         }
 
         @Override
-        public void cancel(int jobId) throws RemoteException {
+        public void cancelAllInNamespace(String namespace) throws RemoteException {
+            final int uid = Binder.getCallingUid();
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                JobSchedulerService.this.cancelJobsForUid(uid,
+                        // Documentation says only jobs scheduled BY the app will be cancelled
+                        /* includeSourceApp */ false,
+                        /* namespaceOnly */ true, namespace,
+                        JobParameters.STOP_REASON_CANCELLED_BY_APP,
+                        JobParameters.INTERNAL_STOP_REASON_CANCELED,
+                        "cancelAllInNamespace() called by app, callingUid=" + uid);
+            } finally {
+                Binder.restoreCallingIdentity(ident);
+            }
+        }
+
+        @Override
+        public void cancel(String namespace, int jobId) throws RemoteException {
             final int uid = Binder.getCallingUid();
 
             final long ident = Binder.clearCallingIdentity();
             try {
-                JobSchedulerService.this.cancelJob(uid, jobId, uid,
+                JobSchedulerService.this.cancelJob(uid, namespace, jobId, uid,
                         JobParameters.STOP_REASON_CANCELLED_BY_APP);
             } finally {
                 Binder.restoreCallingIdentity(ident);
@@ -4080,8 +4173,9 @@
     }
 
     // Shell command infrastructure: run the given job immediately
-    int executeRunCommand(String pkgName, int userId, int jobId, boolean satisfied, boolean force) {
-        Slog.d(TAG, "executeRunCommand(): " + pkgName + "/" + userId
+    int executeRunCommand(String pkgName, int userId, @Nullable String namespace,
+            int jobId, boolean satisfied, boolean force) {
+        Slog.d(TAG, "executeRunCommand(): " + pkgName + "/" + namespace + "/" + userId
                 + " " + jobId + " s=" + satisfied + " f=" + force);
 
         try {
@@ -4092,7 +4186,7 @@
             }
 
             synchronized (mLock) {
-                final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
+                final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
                 if (js == null) {
                     return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
                 }
@@ -4122,14 +4216,14 @@
 
     // Shell command infrastructure: immediately timeout currently executing jobs
     int executeTimeoutCommand(PrintWriter pw, String pkgName, int userId,
-            boolean hasJobId, int jobId) {
+            @Nullable String namespace, boolean hasJobId, int jobId) {
         if (DEBUG) {
             Slog.v(TAG, "executeTimeoutCommand(): " + pkgName + "/" + userId + " " + jobId);
         }
 
         synchronized (mLock) {
             final boolean foundSome = mConcurrencyManager.executeTimeoutCommandLocked(pw,
-                    pkgName, userId, hasJobId, jobId);
+                    pkgName, userId, namespace, hasJobId, jobId);
             if (!foundSome) {
                 pw.println("No matching executing jobs found.");
             }
@@ -4138,7 +4232,7 @@
     }
 
     // Shell command infrastructure: cancel a scheduled job
-    int executeCancelCommand(PrintWriter pw, String pkgName, int userId,
+    int executeCancelCommand(PrintWriter pw, String pkgName, int userId, @Nullable String namespace,
             boolean hasJobId, int jobId) {
         if (DEBUG) {
             Slog.v(TAG, "executeCancelCommand(): " + pkgName + "/" + userId + " " + jobId);
@@ -4166,7 +4260,8 @@
             }
         } else {
             pw.println("Canceling job " + pkgName + "/#" + jobId + " in user " + userId);
-            if (!cancelJob(pkgUid, jobId, Process.SHELL_UID, JobParameters.STOP_REASON_USER)) {
+            if (!cancelJob(pkgUid, namespace, jobId,
+                    Process.SHELL_UID, JobParameters.STOP_REASON_USER)) {
                 pw.println("No matching job found.");
             }
         }
@@ -4213,8 +4308,8 @@
         }
     }
 
-    int getEstimatedNetworkBytes(PrintWriter pw, String pkgName, int userId, int jobId,
-            int byteOption) {
+    int getEstimatedNetworkBytes(PrintWriter pw, String pkgName, int userId, String namespace,
+            int jobId, int byteOption) {
         try {
             final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
                     userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
@@ -4226,9 +4321,10 @@
             }
 
             synchronized (mLock) {
-                final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
+                final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
                 if (DEBUG) {
-                    Slog.d(TAG, "get-estimated-network-bytes " + uid + "/" + jobId + ": " + js);
+                    Slog.d(TAG, "get-estimated-network-bytes " + uid + "/"
+                            + namespace + "/" + jobId + ": " + js);
                 }
                 if (js == null) {
                     pw.print("unknown("); UserHandle.formatUid(pw, uid);
@@ -4238,8 +4334,8 @@
 
                 final long downloadBytes;
                 final long uploadBytes;
-                final Pair<Long, Long> bytes =
-                        mConcurrencyManager.getEstimatedNetworkBytesLocked(pkgName, uid, jobId);
+                final Pair<Long, Long> bytes = mConcurrencyManager.getEstimatedNetworkBytesLocked(
+                        pkgName, uid, namespace, jobId);
                 if (bytes == null) {
                     downloadBytes = js.getEstimatedNetworkDownloadBytes();
                     uploadBytes = js.getEstimatedNetworkUploadBytes();
@@ -4260,8 +4356,8 @@
         return 0;
     }
 
-    int getTransferredNetworkBytes(PrintWriter pw, String pkgName, int userId, int jobId,
-            int byteOption) {
+    int getTransferredNetworkBytes(PrintWriter pw, String pkgName, int userId, String namespace,
+            int jobId, int byteOption) {
         try {
             final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
                     userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
@@ -4273,9 +4369,10 @@
             }
 
             synchronized (mLock) {
-                final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
+                final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
                 if (DEBUG) {
-                    Slog.d(TAG, "get-transferred-network-bytes " + uid + "/" + jobId + ": " + js);
+                    Slog.d(TAG, "get-transferred-network-bytes " + uid
+                            + namespace + "/" + "/" + jobId + ": " + js);
                 }
                 if (js == null) {
                     pw.print("unknown("); UserHandle.formatUid(pw, uid);
@@ -4285,8 +4382,8 @@
 
                 final long downloadBytes;
                 final long uploadBytes;
-                final Pair<Long, Long> bytes =
-                        mConcurrencyManager.getTransferredNetworkBytesLocked(pkgName, uid, jobId);
+                final Pair<Long, Long> bytes = mConcurrencyManager.getTransferredNetworkBytesLocked(
+                        pkgName, uid, namespace, jobId);
                 if (bytes == null) {
                     downloadBytes = 0;
                     uploadBytes = 0;
@@ -4335,7 +4432,8 @@
     }
 
     // Shell command infrastructure
-    int getJobState(PrintWriter pw, String pkgName, int userId, int jobId) {
+    int getJobState(PrintWriter pw, String pkgName, int userId, @Nullable String namespace,
+            int jobId) {
         try {
             final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
                     userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
@@ -4347,11 +4445,17 @@
             }
 
             synchronized (mLock) {
-                final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
-                if (DEBUG) Slog.d(TAG, "get-job-state " + uid + "/" + jobId + ": " + js);
+                final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
+                if (DEBUG) {
+                    Slog.d(TAG,
+                            "get-job-state " + namespace + "/" + uid + "/" + jobId + ": " + js);
+                }
                 if (js == null) {
-                    pw.print("unknown("); UserHandle.formatUid(pw, uid);
-                    pw.print("/jid"); pw.print(jobId); pw.println(")");
+                    pw.print("unknown(");
+                    UserHandle.formatUid(pw, uid);
+                    pw.print("/jid");
+                    pw.print(jobId);
+                    pw.println(")");
                     return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
                 }
 
@@ -4527,7 +4631,9 @@
                     }
                     jobPrinted = true;
 
-                    pw.print("JOB #"); job.printUniqueId(pw); pw.print(": ");
+                    pw.print("JOB ");
+                    job.printUniqueId(pw);
+                    pw.print(": ");
                     pw.println(job.toShortStringExceptUniqueId());
 
                     pw.increaseIndent();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
index 36ba8dd..2eeb25e 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -16,6 +16,7 @@
 
 package com.android.server.job;
 
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.AppGlobals;
 import android.content.pm.IPackageManager;
@@ -107,7 +108,8 @@
         }
     }
 
-    private boolean printError(int errCode, String pkgName, int userId, int jobId) {
+    private boolean printError(int errCode, String pkgName, int userId, @Nullable String namespace,
+            int jobId) {
         PrintWriter pw;
         switch (errCode) {
             case CMD_ERR_NO_PACKAGE:
@@ -124,6 +126,10 @@
                 pw.print(jobId);
                 pw.print(" in package ");
                 pw.print(pkgName);
+                if (namespace != null) {
+                    pw.print(" / namespace ");
+                    pw.print(namespace);
+                }
                 pw.print(" / user ");
                 pw.println(userId);
                 return true;
@@ -134,6 +140,10 @@
                 pw.print(jobId);
                 pw.print(" in package ");
                 pw.print(pkgName);
+                if (namespace != null) {
+                    pw.print(" / namespace ");
+                    pw.print(namespace);
+                }
                 pw.print(" / user ");
                 pw.print(userId);
                 pw.println(" has functional constraints but --force not specified");
@@ -150,6 +160,7 @@
         boolean force = false;
         boolean satisfied = false;
         int userId = UserHandle.USER_SYSTEM;
+        String namespace = null;
 
         String opt;
         while ((opt = getNextOption()) != null) {
@@ -169,6 +180,11 @@
                     userId = Integer.parseInt(getNextArgRequired());
                     break;
 
+                case "-n":
+                case "--namespace":
+                    namespace = getNextArgRequired();
+                    break;
+
                 default:
                     pw.println("Error: unknown option '" + opt + "'");
                     return -1;
@@ -185,8 +201,9 @@
 
         final long ident = Binder.clearCallingIdentity();
         try {
-            int ret = mInternal.executeRunCommand(pkgName, userId, jobId, satisfied, force);
-            if (printError(ret, pkgName, userId, jobId)) {
+            int ret = mInternal.executeRunCommand(pkgName, userId, namespace,
+                    jobId, satisfied, force);
+            if (printError(ret, pkgName, userId, namespace, jobId)) {
                 return ret;
             }
 
@@ -207,6 +224,7 @@
         checkPermission("force timeout jobs");
 
         int userId = UserHandle.USER_ALL;
+        String namespace = null;
 
         String opt;
         while ((opt = getNextOption()) != null) {
@@ -216,6 +234,11 @@
                     userId = UserHandle.parseUserArg(getNextArgRequired());
                     break;
 
+                case "-n":
+                case "--namespace":
+                    namespace = getNextArgRequired();
+                    break;
+
                 default:
                     pw.println("Error: unknown option '" + opt + "'");
                     return -1;
@@ -232,7 +255,8 @@
 
         final long ident = Binder.clearCallingIdentity();
         try {
-            return mInternal.executeTimeoutCommand(pw, pkgName, userId, jobIdStr != null, jobId);
+            return mInternal.executeTimeoutCommand(pw, pkgName, userId, namespace,
+                    jobIdStr != null, jobId);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -242,6 +266,7 @@
         checkPermission("cancel jobs");
 
         int userId = UserHandle.USER_SYSTEM;
+        String namespace = null;
 
         String opt;
         while ((opt = getNextOption()) != null) {
@@ -251,6 +276,11 @@
                     userId = UserHandle.parseUserArg(getNextArgRequired());
                     break;
 
+                case "-n":
+                case "--namespace":
+                    namespace = getNextArgRequired();
+                    break;
+
                 default:
                     pw.println("Error: unknown option '" + opt + "'");
                     return -1;
@@ -268,7 +298,8 @@
 
         final long ident = Binder.clearCallingIdentity();
         try {
-            return mInternal.executeCancelCommand(pw, pkgName, userId, jobIdStr != null, jobId);
+            return mInternal.executeCancelCommand(pw, pkgName, userId, namespace,
+                    jobIdStr != null, jobId);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -319,6 +350,7 @@
         checkPermission("get estimated bytes");
 
         int userId = UserHandle.USER_SYSTEM;
+        String namespace = null;
 
         String opt;
         while ((opt = getNextOption()) != null) {
@@ -328,6 +360,11 @@
                     userId = UserHandle.parseUserArg(getNextArgRequired());
                     break;
 
+                case "-n":
+                case "--namespace":
+                    namespace = getNextArgRequired();
+                    break;
+
                 default:
                     pw.println("Error: unknown option '" + opt + "'");
                     return -1;
@@ -344,8 +381,9 @@
 
         final long ident = Binder.clearCallingIdentity();
         try {
-            int ret = mInternal.getEstimatedNetworkBytes(pw, pkgName, userId, jobId, byteOption);
-            printError(ret, pkgName, userId, jobId);
+            int ret = mInternal.getEstimatedNetworkBytes(pw, pkgName, userId, namespace,
+                    jobId, byteOption);
+            printError(ret, pkgName, userId, namespace, jobId);
             return ret;
         } finally {
             Binder.restoreCallingIdentity(ident);
@@ -368,6 +406,7 @@
         checkPermission("get transferred bytes");
 
         int userId = UserHandle.USER_SYSTEM;
+        String namespace = null;
 
         String opt;
         while ((opt = getNextOption()) != null) {
@@ -377,6 +416,11 @@
                     userId = UserHandle.parseUserArg(getNextArgRequired());
                     break;
 
+                case "-n":
+                case "--namespace":
+                    namespace = getNextArgRequired();
+                    break;
+
                 default:
                     pw.println("Error: unknown option '" + opt + "'");
                     return -1;
@@ -393,8 +437,9 @@
 
         final long ident = Binder.clearCallingIdentity();
         try {
-            int ret = mInternal.getTransferredNetworkBytes(pw, pkgName, userId, jobId, byteOption);
-            printError(ret, pkgName, userId, jobId);
+            int ret = mInternal.getTransferredNetworkBytes(pw, pkgName, userId, namespace,
+                    jobId, byteOption);
+            printError(ret, pkgName, userId, namespace, jobId);
             return ret;
         } finally {
             Binder.restoreCallingIdentity(ident);
@@ -405,6 +450,7 @@
         checkPermission("get job state");
 
         int userId = UserHandle.USER_SYSTEM;
+        String namespace = null;
 
         String opt;
         while ((opt = getNextOption()) != null) {
@@ -414,6 +460,11 @@
                     userId = UserHandle.parseUserArg(getNextArgRequired());
                     break;
 
+                case "-n":
+                case "--namespace":
+                    namespace = getNextArgRequired();
+                    break;
+
                 default:
                     pw.println("Error: unknown option '" + opt + "'");
                     return -1;
@@ -430,8 +481,8 @@
 
         final long ident = Binder.clearCallingIdentity();
         try {
-            int ret = mInternal.getJobState(pw, pkgName, userId, jobId);
-            printError(ret, pkgName, userId, jobId);
+            int ret = mInternal.getJobState(pw, pkgName, userId, namespace, jobId);
+            printError(ret, pkgName, userId, namespace, jobId);
             return ret;
         } finally {
             Binder.restoreCallingIdentity(ident);
@@ -521,7 +572,8 @@
         pw.println("Job scheduler (jobscheduler) commands:");
         pw.println("  help");
         pw.println("    Print this help text.");
-        pw.println("  run [-f | --force] [-s | --satisfied] [-u | --user USER_ID] PACKAGE JOB_ID");
+        pw.println("  run [-f | --force] [-s | --satisfied] [-u | --user USER_ID]"
+                + " [-n | --namespace NAMESPACE] PACKAGE JOB_ID");
         pw.println("    Trigger immediate execution of a specific scheduled job. For historical");
         pw.println("    reasons, some constraints, such as battery, are ignored when this");
         pw.println("    command is called. If you don't want any constraints to be ignored,");
@@ -530,23 +582,30 @@
         pw.println("      -f or --force: run the job even if technical constraints such as");
         pw.println("         connectivity are not currently met. This is incompatible with -f ");
         pw.println("         and so an error will be reported if both are given.");
+        pw.println("      -n or --namespace: specify the namespace this job sits in; the default");
+        pw.println("         is null (no namespace).");
         pw.println("      -s or --satisfied: run the job only if all constraints are met.");
         pw.println("         This is incompatible with -f and so an error will be reported");
         pw.println("         if both are given.");
         pw.println("      -u or --user: specify which user's job is to be run; the default is");
         pw.println("         the primary or system user");
-        pw.println("  timeout [-u | --user USER_ID] [PACKAGE] [JOB_ID]");
+        pw.println("  timeout [-u | --user USER_ID] [-n | --namespace NAMESPACE]"
+                + " [PACKAGE] [JOB_ID]");
         pw.println("    Trigger immediate timeout of currently executing jobs, as if their.");
         pw.println("    execution timeout had expired.");
         pw.println("    Options:");
         pw.println("      -u or --user: specify which user's job is to be run; the default is");
         pw.println("         all users");
-        pw.println("  cancel [-u | --user USER_ID] PACKAGE [JOB_ID]");
+        pw.println("      -n or --namespace: specify the namespace this job sits in; the default");
+        pw.println("         is null (no namespace).");
+        pw.println("  cancel [-u | --user USER_ID] [-n | --namespace NAMESPACE] PACKAGE [JOB_ID]");
         pw.println("    Cancel a scheduled job.  If a job ID is not supplied, all jobs scheduled");
         pw.println("    by that package will be canceled.  USE WITH CAUTION.");
         pw.println("    Options:");
         pw.println("      -u or --user: specify which user's job is to be run; the default is");
         pw.println("         the primary or system user");
+        pw.println("      -n or --namespace: specify the namespace this job sits in; the default");
+        pw.println("         is null (no namespace).");
         pw.println("  heartbeat [num]");
         pw.println("    No longer used.");
         pw.println("  monitor-battery [on|off]");
@@ -558,12 +617,14 @@
         pw.println("    Return whether the battery is currently considered to be charging.");
         pw.println("  get-battery-not-low");
         pw.println("    Return whether the battery is currently considered to not be low.");
-        pw.println("  get-estimated-download-bytes [-u | --user USER_ID] PACKAGE JOB_ID");
+        pw.println("  get-estimated-download-bytes [-u | --user USER_ID]"
+                + " [-n | --namespace NAMESPACE] PACKAGE JOB_ID");
         pw.println("    Return the most recent estimated download bytes for the job.");
         pw.println("    Options:");
         pw.println("      -u or --user: specify which user's job is to be run; the default is");
         pw.println("         the primary or system user");
-        pw.println("  get-estimated-upload-bytes [-u | --user USER_ID] PACKAGE JOB_ID");
+        pw.println("  get-estimated-upload-bytes [-u | --user USER_ID]"
+                + " [-n | --namespace NAMESPACE] PACKAGE JOB_ID");
         pw.println("    Return the most recent estimated upload bytes for the job.");
         pw.println("    Options:");
         pw.println("      -u or --user: specify which user's job is to be run; the default is");
@@ -572,17 +633,20 @@
         pw.println("    Return the last storage update sequence number that was received.");
         pw.println("  get-storage-not-low");
         pw.println("    Return whether storage is currently considered to not be low.");
-        pw.println("  get-transferred-download-bytes [-u | --user USER_ID] PACKAGE JOB_ID");
+        pw.println("  get-transferred-download-bytes [-u | --user USER_ID]"
+                + " [-n | --namespace NAMESPACE] PACKAGE JOB_ID");
         pw.println("    Return the most recent transferred download bytes for the job.");
         pw.println("    Options:");
         pw.println("      -u or --user: specify which user's job is to be run; the default is");
         pw.println("         the primary or system user");
-        pw.println("  get-transferred-upload-bytes [-u | --user USER_ID] PACKAGE JOB_ID");
+        pw.println("  get-transferred-upload-bytes [-u | --user USER_ID]"
+                + " [-n | --namespace NAMESPACE] PACKAGE JOB_ID");
         pw.println("    Return the most recent transferred upload bytes for the job.");
         pw.println("    Options:");
         pw.println("      -u or --user: specify which user's job is to be run; the default is");
         pw.println("         the primary or system user");
-        pw.println("  get-job-state [-u | --user USER_ID] PACKAGE JOB_ID");
+        pw.println("  get-job-state [-u | --user USER_ID] [-n | --namespace NAMESPACE]"
+                + " PACKAGE JOB_ID");
         pw.println("    Return the current state of a job, may be any combination of:");
         pw.println("      pending: currently on the pending list, waiting to be active");
         pw.println("      active: job is actively running");
@@ -594,6 +658,8 @@
         pw.println("    Options:");
         pw.println("      -u or --user: specify which user's job is to be run; the default is");
         pw.println("         the primary or system user");
+        pw.println("      -n or --namespace: specify the namespace this job sits in; the default");
+        pw.println("         is null (no namespace).");
         pw.println("  trigger-dock-state [idle|active]");
         pw.println("    Trigger wireless charging dock state.  Active by default.");
         pw.println();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index 15fc3c9..285b982 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -64,6 +64,8 @@
 import com.android.server.tare.EconomyManagerInternal;
 import com.android.server.tare.JobSchedulerEconomicPolicy;
 
+import java.util.Objects;
+
 /**
  * Handles client binding and lifecycle of a job. Jobs execute one at a time on an instance of this
  * class.
@@ -304,7 +306,8 @@
                 job.changedAuthorities.toArray(triggeredAuthorities);
             }
             final JobInfo ji = job.getJob();
-            mParams = new JobParameters(mRunningCallback, job.getJobId(), ji.getExtras(),
+            mParams = new JobParameters(mRunningCallback, job.getNamespace(), job.getJobId(),
+                    ji.getExtras(),
                     ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(),
                     isDeadlineExpired, job.shouldTreatAsExpeditedJob(),
                     job.shouldTreatAsUserInitiatedJob(), triggeredUris, triggeredAuthorities,
@@ -518,11 +521,12 @@
     }
 
     @GuardedBy("mLock")
-    boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId,
-            String reason) {
+    boolean timeoutIfExecutingLocked(String pkgName, int userId, @Nullable String namespace,
+            boolean matchJobId, int jobId, String reason) {
         final JobStatus executing = getRunningJobLocked();
         if (executing != null && (userId == UserHandle.USER_ALL || userId == executing.getUserId())
                 && (pkgName == null || pkgName.equals(executing.getSourcePackageName()))
+                && Objects.equals(namespace, executing.getNamespace())
                 && (!matchJobId || jobId == executing.getJobId())) {
             if (mVerb == VERB_EXECUTING) {
                 mParams.setStopReason(JobParameters.STOP_REASON_TIMEOUT,
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 a1153e3..5f5f447 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -66,6 +66,7 @@
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.StringJoiner;
 import java.util.function.Consumer;
@@ -386,8 +387,8 @@
      * @return the JobStatus that matches the provided uId and jobId, or null if none found.
      */
     @Nullable
-    public JobStatus getJobByUidAndJobId(int uid, int jobId) {
-        return mJobSet.get(uid, jobId);
+    public JobStatus getJobByUidAndJobId(int uid, @Nullable String namespace, int jobId) {
+        return mJobSet.get(uid, namespace, jobId);
     }
 
     /**
@@ -764,6 +765,9 @@
             if (jobStatus.getSourcePackageName() != null) {
                 out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName());
             }
+            if (jobStatus.getNamespace() != null) {
+                out.attribute(null, "namespace", jobStatus.getNamespace());
+            }
             if (jobStatus.getSourceTag() != null) {
                 out.attribute(null, "sourceTag", jobStatus.getSourceTag());
             }
@@ -1135,6 +1139,7 @@
             }
 
             String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
+            final String namespace = parser.getAttributeValue(null, "namespace");
             final String sourceTag = parser.getAttributeValue(null, "sourceTag");
 
             int eventType;
@@ -1292,7 +1297,7 @@
                     sourceUserId, nowElapsed);
             JobStatus js = new JobStatus(
                     builtJob, uid, sourcePackageName, sourceUserId,
-                    appBucket, sourceTag,
+                    appBucket, namespace, sourceTag,
                     elapsedRuntimes.first, elapsedRuntimes.second,
                     lastSuccessfulRunTime, lastFailedRunTime,
                     (rtcIsGood) ? null : rtcRuntimes, internalFlags, /* dynamicConstraints */ 0);
@@ -1592,12 +1597,12 @@
             return jobs != null && jobs.contains(job);
         }
 
-        public JobStatus get(int uid, int jobId) {
+        public JobStatus get(int uid, @Nullable String namespace, int jobId) {
             ArraySet<JobStatus> jobs = mJobs.get(uid);
             if (jobs != null) {
                 for (int i = jobs.size() - 1; i >= 0; i--) {
                     JobStatus job = jobs.valueAt(i);
-                    if (job.getJobId() == jobId) {
+                    if (job.getJobId() == jobId && Objects.equals(namespace, job.getNamespace())) {
                         return job;
                     }
                 }
diff --git a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
index 0eacfd6..36a26f0 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/PendingJobQueue.java
@@ -28,6 +28,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Objects;
 import java.util.PriorityQueue;
 
 /**
@@ -280,12 +281,14 @@
                 return job1EJ ? -1 : 1;
             }
 
-            final int job1Priority = job1.getEffectivePriority();
-            final int job2Priority = job2.getEffectivePriority();
-            if (job1Priority != job2Priority) {
-                // Use the priority set by an app for intra-app job ordering. Higher
-                // priority should be before lower priority.
-                return Integer.compare(job2Priority, job1Priority);
+            if (Objects.equals(job1.getNamespace(), job2.getNamespace())) {
+                final int job1Priority = job1.getEffectivePriority();
+                final int job2Priority = job2.getEffectivePriority();
+                if (job1Priority != job2Priority) {
+                    // Use the priority set by an app for intra-app job ordering. Higher
+                    // priority should be before lower priority.
+                    return Integer.compare(job2Priority, job1Priority);
+                }
             }
 
             if (job1.lastEvaluatedBias != job2.lastEvaluatedBias) {
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 9b6186e..5712599 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
@@ -30,6 +30,7 @@
 
 import android.annotation.ElapsedRealtimeLong;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.AppGlobals;
 import android.app.job.JobInfo;
 import android.app.job.JobParameters;
@@ -70,6 +71,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -225,6 +227,8 @@
     final int sourceUserId;
     final int sourceUid;
     final String sourceTag;
+    @Nullable
+    private final String mNamespace;
 
     final String tag;
 
@@ -515,6 +519,7 @@
      * @param standbyBucket The standby bucket that the source package is currently assigned to,
      *     cached here for speed of handling during runnability evaluations (and updated when bucket
      *     assignments are changed)
+     * @param namespace The custom namespace the app put this job in.
      * @param tag A string associated with the job for debugging/logging purposes.
      * @param numFailures Count of how many times this job has requested a reschedule because
      *     its work was not yet finished.
@@ -529,13 +534,15 @@
      * @param internalFlags Non-API property flags about this job
      */
     private JobStatus(JobInfo job, int callingUid, String sourcePackageName,
-            int sourceUserId, int standbyBucket, String tag, int numFailures, int numSystemStops,
+            int sourceUserId, int standbyBucket, @Nullable String namespace, String tag,
+            int numFailures, int numSystemStops,
             long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
             long lastSuccessfulRunTime, long lastFailedRunTime, int internalFlags,
             int dynamicConstraints) {
         this.job = job;
         this.callingUid = callingUid;
         this.standbyBucket = standbyBucket;
+        mNamespace = namespace;
 
         int tempSourceUid = -1;
         if (sourceUserId != -1 && sourcePackageName != null) {
@@ -658,7 +665,7 @@
     public JobStatus(JobStatus jobStatus) {
         this(jobStatus.getJob(), jobStatus.getUid(),
                 jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(),
-                jobStatus.getStandbyBucket(),
+                jobStatus.getStandbyBucket(), jobStatus.getNamespace(),
                 jobStatus.getSourceTag(), jobStatus.getNumFailures(), jobStatus.getNumSystemStops(),
                 jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(),
                 jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime(),
@@ -680,13 +687,13 @@
      * standby bucket is whatever the OS thinks it should be at this moment.
      */
     public JobStatus(JobInfo job, int callingUid, String sourcePkgName, int sourceUserId,
-            int standbyBucket, String sourceTag,
+            int standbyBucket, @Nullable String namespace, String sourceTag,
             long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
             long lastSuccessfulRunTime, long lastFailedRunTime,
             Pair<Long, Long> persistedExecutionTimesUTC,
             int innerFlags, int dynamicConstraints) {
         this(job, callingUid, sourcePkgName, sourceUserId,
-                standbyBucket,
+                standbyBucket, namespace,
                 sourceTag, /* numFailures */ 0, /* numSystemStops */ 0,
                 earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
                 lastSuccessfulRunTime, lastFailedRunTime, innerFlags, dynamicConstraints);
@@ -710,7 +717,7 @@
             long lastSuccessfulRunTime, long lastFailedRunTime) {
         this(rescheduling.job, rescheduling.getUid(),
                 rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(),
-                rescheduling.getStandbyBucket(),
+                rescheduling.getStandbyBucket(), rescheduling.getNamespace(),
                 rescheduling.getSourceTag(), numFailures, numSystemStops,
                 newEarliestRuntimeElapsedMillis,
                 newLatestRuntimeElapsedMillis,
@@ -727,7 +734,7 @@
      *     caller.
      */
     public static JobStatus createFromJobInfo(JobInfo job, int callingUid, String sourcePkg,
-            int sourceUserId, String tag) {
+            int sourceUserId, @Nullable String namespace, String tag) {
         final long elapsedNow = sElapsedRealtimeClock.millis();
         final long earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis;
         if (job.isPeriodic()) {
@@ -749,7 +756,7 @@
         int standbyBucket = JobSchedulerService.standbyBucketForPackage(jobPackage,
                 sourceUserId, elapsedNow);
         return new JobStatus(job, callingUid, sourcePkg, sourceUserId,
-                standbyBucket, tag, /* numFailures */ 0, /* numSystemStops */ 0,
+                standbyBucket, namespace, tag, /* numFailures */ 0, /* numSystemStops */ 0,
                 earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
                 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */,
                 /*innerFlags=*/ 0, /* dynamicConstraints */ 0);
@@ -897,6 +904,12 @@
     }
 
     public void printUniqueId(PrintWriter pw) {
+        if (mNamespace != null) {
+            pw.print(mNamespace);
+            pw.print(":");
+        } else {
+            pw.print("#");
+        }
         UserHandle.formatUid(pw, callingUid);
         pw.print("/");
         pw.print(job.getId());
@@ -1036,6 +1049,10 @@
         return true;
     }
 
+    public String getNamespace() {
+        return mNamespace;
+    }
+
     public String getSourceTag() {
         return sourceTag;
     }
@@ -1362,7 +1379,8 @@
     public UserVisibleJobSummary getUserVisibleJobSummary() {
         if (mUserVisibleJobSummary == null) {
             mUserVisibleJobSummary = new UserVisibleJobSummary(
-                    callingUid, getSourceUserId(), getSourcePackageName(), getJobId());
+                    callingUid, getSourceUserId(), getSourcePackageName(),
+                    getNamespace(), getJobId());
         }
         return mUserVisibleJobSummary;
     }
@@ -1989,8 +2007,12 @@
         return (sat & mRequiredConstraintsOfInterest) == mRequiredConstraintsOfInterest;
     }
 
-    public boolean matches(int uid, int jobId) {
-        return this.job.getId() == jobId && this.callingUid == uid;
+    /**
+     * Returns true if the given parameters match this job's unique identifier.
+     */
+    public boolean matches(int uid, @Nullable String namespace, int jobId) {
+        return this.job.getId() == jobId && this.callingUid == uid
+                && Objects.equals(mNamespace, namespace);
     }
 
     @Override
@@ -1998,7 +2020,13 @@
         StringBuilder sb = new StringBuilder(128);
         sb.append("JobStatus{");
         sb.append(Integer.toHexString(System.identityHashCode(this)));
-        sb.append(" #");
+        if (mNamespace != null) {
+            sb.append(" ");
+            sb.append(mNamespace);
+            sb.append(":");
+        } else {
+            sb.append(" #");
+        }
         UserHandle.formatUid(sb, callingUid);
         sb.append("/");
         sb.append(job.getId());
@@ -2087,6 +2115,9 @@
     public String toShortString() {
         StringBuilder sb = new StringBuilder();
         sb.append(Integer.toHexString(System.identityHashCode(this)));
+        if (mNamespace != null) {
+            sb.append(" {").append(mNamespace).append("}");
+        }
         sb.append(" #");
         UserHandle.formatUid(sb, callingUid);
         sb.append("/");
diff --git a/core/api/current.txt b/core/api/current.txt
index 5bb6652..a77040c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -18263,6 +18263,7 @@
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_SYSTEM_CAMERA = 14; // 0xe
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_ULTRA_HIGH_RESOLUTION_SENSOR = 16; // 0x10
     field public static final int REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING = 7; // 0x7
+    field public static final int SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW = 6; // 0x6
     field public static final int SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT = 0; // 0x0
     field public static final int SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW = 1; // 0x1
     field public static final int SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL = 4; // 0x4
@@ -18522,6 +18523,7 @@
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Float> REPROCESS_EFFECTIVE_EXPOSURE_FACTOR;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Byte> REQUEST_PIPELINE_DEPTH;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.graphics.Rect> SCALER_CROP_REGION;
+    field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.graphics.Rect> SCALER_RAW_CROP_REGION;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> SCALER_ROTATE_AND_CROP;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<float[]> SENSOR_DYNAMIC_BLACK_LEVEL;
     field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> SENSOR_DYNAMIC_WHITE_LEVEL;
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ac5c54d..97b9a51 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -4244,6 +4244,7 @@
     method @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public void setBrightnessConfiguration(android.hardware.display.BrightnessConfiguration);
     method @RequiresPermission(android.Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS) public void setBrightnessConfigurationForDisplay(@NonNull android.hardware.display.BrightnessConfiguration, @NonNull String);
     method @Deprecated @RequiresPermission(android.Manifest.permission.CONTROL_DISPLAY_SATURATION) public void setSaturationLevel(float);
+    field public static final int VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED = 65536; // 0x10000
     field public static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1024; // 0x400
   }
 
@@ -6682,6 +6683,7 @@
   }
 
   public final class AudioPlaybackConfiguration implements android.os.Parcelable {
+    method public int getChannelMask();
     method public int getClientPid();
     method public int getClientUid();
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMutedBy();
@@ -6689,9 +6691,11 @@
     method public android.media.PlayerProxy getPlayerProxy();
     method public int getPlayerState();
     method public int getPlayerType();
+    method @IntRange(from=0) public int getSampleRate();
     method @IntRange(from=0) public int getSessionId();
     method public boolean isActive();
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean isMuted();
+    method public boolean isSpatialized();
     field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_APP_OPS = 8; // 0x8
     field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_CLIENT_VOLUME = 16; // 0x10
     field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_MASTER = 1; // 0x1
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 320af06..11b80cc 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -3756,6 +3756,7 @@
      *   <li>{@link #SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD VIDEO_RECORD}</li>
      *   <li>{@link #SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL PREVIEW_VIDEO_STILL}</li>
      *   <li>{@link #SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL VIDEO_CALL}</li>
+     *   <li>{@link #SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW CROPPED_RAW}</li>
      * </ul>
      *
      * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
@@ -3765,6 +3766,7 @@
      * @see #SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD
      * @see #SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL
      * @see #SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL
+     * @see #SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW
      */
     @PublicKey
     @NonNull
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index bf2e563..577c8a3 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -900,13 +900,27 @@
      * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code VIDEO_RECORD}</td> <td colspan="3" id="rb"></td> <td>Preview with video recording or in-app video processing</td> </tr>
      * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td colspan="3" id="rb"></td> <td>Preview with in-application image processing</td> </tr>
      * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code s1440p}</td><td id="rb">{@code VIDEO_CALL}</td> <td colspan="3" id="rb"></td> <td>Preview with video call</td> </tr>
-     * <tr> <td>{@code YUV / PRIV}</td><td id="rb">{@code s1440p}</td><td id="rb">{@code PREVIEW_VIDEO_STILL}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td colspan="3" id="rb"></td> <td>Multi-purpose stream with JPEG or YUV still capture</td> </tr>
+     * <tr> <td>{@code YUV / PRIV}</td><td id="rb">{@code s1440p}</td><td id="rb">{@code PREVIEW_VIDEO_STILL}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td colspan="3" id="rb"></td> <td>MultI-purpose stream with JPEG or YUV still capture</td> </tr>
      * <tr> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>{@code JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td colspan="3" id="rb"></td> <td>YUV and JPEG concurrent still image capture (for testing)</td> </tr>
      * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / PRIV}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code VIDEO_RECORD}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code RECORD}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, video record and JPEG or YUV video snapshot</td> </tr>
      * <tr> <td>{@code PRIV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>Preview, in-application image processing, and JPEG or YUV still image capture</td> </tr>
      * </table><br>
      * </p>
      *
+     * <p>Devices that include the {@link CameraMetadata#SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW}
+     * stream use-case in {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES},
+     * support the additional stream combinations below:
+     *
+     * <table>
+     * <tr><th colspan="10">STREAM_USE_CASE_CROPPED_RAW capability additional guaranteed configurations</th></tr>
+     * <tr><th colspan="3" id="rb">Target 1</th><th colspan="3" id="rb">Target 2</th><th colspan="3" id="rb">Target 3</th> <th rowspan="2">Sample use case(s)</th> </tr>
+     * <tr><th>Type</th><th id="rb">Max size</th><th>Usecase</th><th>Type</th><th id="rb">Max size</th><th>Usecase</th><th>Type</th><th id="rb">Max size</th><th>Usecase</th> </tr>
+     * <tr> <td>{@code RAW}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code CROPPED_RAW}</td> <td colspan="3" id="rb"></td> <td colspan="3" id="rb"></td> <td>Cropped RAW still capture without preview</td> </tr>
+     * <tr> <td>{@code PRIV / YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code RAW}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code CROPPED_RAW}</td> <td colspan="3" id="rb"></td> <td>Preview with cropped RAW still capture</td> </tr>
+     * <tr> <td>{@code PRIV / YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV / JPEG}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code STILL_CAPTURE}</td> <td>{@code RAW}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code CROPPED_RAW}</td> <td>Preview with YUV / JPEG and cropped RAW still capture</td> </tr>
+     * <tr> <td>{@code PRIV / YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code PREVIEW}</td> <td>{@code PRIV / YUV}</td><td id="rb">{@code PREVIEW}</td><td id="rb">{@code VIDEO_RECORD / PREVIEW}</td> <td>{@code RAW}</td><td id="rb">{@code MAXIMUM}</td><td id="rb">{@code CROPPED_RAW}</td> <td>Video recording with preview and cropped RAW still capture</td> </tr>
+     *
+     *
      *<p> For devices where {@link CameraCharacteristics#CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES}
      * includes {@link CameraMetadata#CONTROL_VIDEO_STABILIZATION_MODE_PREVIEW_STABILIZATION},
      * the following stream combinations are guaranteed,
diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java
index 67dcd93..788302b 100644
--- a/core/java/android/hardware/camera2/CameraMetadata.java
+++ b/core/java/android/hardware/camera2/CameraMetadata.java
@@ -1489,6 +1489,31 @@
     public static final int SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL = 0x5;
 
     /**
+     * <p>Cropped RAW stream when the client chooses to crop the field of view.</p>
+     * <p>Certain types of image sensors can run in binned modes in order to improve signal to
+     * noise ratio while capturing frames. However, at certain zoom levels and / or when
+     * other scene conditions are deemed fit, the camera sub-system may choose to un-bin and
+     * remosaic the sensor's output. This results in a RAW frame which is cropped in field
+     * of view and yet has the same number of pixels as full field of view RAW, thereby
+     * improving image detail.</p>
+     * <p>The resultant field of view of the RAW stream will be greater than or equal to
+     * croppable non-RAW streams. The effective crop region for this RAW stream will be
+     * reflected in the CaptureResult key {@link CaptureResult#SCALER_RAW_CROP_REGION android.scaler.rawCropRegion}.</p>
+     * <p>If this stream use case is set on a non-RAW stream, i.e. not one of :</p>
+     * <ul>
+     * <li>{@link android.graphics.ImageFormat#RAW_SENSOR RAW_SENSOR}</li>
+     * <li>{@link android.graphics.ImageFormat#RAW10 RAW10}</li>
+     * <li>{@link android.graphics.ImageFormat#RAW12 RAW12}</li>
+     * </ul>
+     * <p>session configuration is not guaranteed to succeed.</p>
+     * <p>This stream use case may not be supported on some devices.</p>
+     *
+     * @see CaptureResult#SCALER_RAW_CROP_REGION
+     * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES
+     */
+    public static final int SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW = 0x6;
+
+    /**
      * <p>Vendor defined use cases. These depend on the vendor implementation.</p>
      * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES
      * @hide
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 43bfdcc..3d83009 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -3086,9 +3086,9 @@
      * <p>Output streams use this rectangle to produce their output, cropping to a smaller region
      * if necessary to maintain the stream's aspect ratio, then scaling the sensor input to
      * match the output's configured resolution.</p>
-     * <p>The crop region is applied after the RAW to other color space (e.g. YUV)
-     * conversion. Since raw streams (e.g. RAW16) don't have the conversion stage, they are not
-     * croppable. The crop region will be ignored by raw streams.</p>
+     * <p>The crop region is usually applied after the RAW to other color space (e.g. YUV)
+     * conversion. As a result RAW streams are not croppable unless supported by the
+     * camera device. See {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES android.scaler.availableStreamUseCases}#CROPPED_RAW for details.</p>
      * <p>For non-raw streams, any additional per-stream cropping will be done to maximize the
      * final pixel area of the stream.</p>
      * <p>For example, if the crop region is set to a 4:3 aspect ratio, then 4:3 streams will use
@@ -3183,6 +3183,7 @@
      * @see CaptureRequest#CONTROL_ZOOM_RATIO
      * @see CaptureRequest#DISTORTION_CORRECTION_MODE
      * @see CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM
+     * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES
      * @see CameraCharacteristics#SCALER_CROPPING_TYPE
      * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE
      * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index fb52cc6..dad7d3e 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -3748,9 +3748,9 @@
      * <p>Output streams use this rectangle to produce their output, cropping to a smaller region
      * if necessary to maintain the stream's aspect ratio, then scaling the sensor input to
      * match the output's configured resolution.</p>
-     * <p>The crop region is applied after the RAW to other color space (e.g. YUV)
-     * conversion. Since raw streams (e.g. RAW16) don't have the conversion stage, they are not
-     * croppable. The crop region will be ignored by raw streams.</p>
+     * <p>The crop region is usually applied after the RAW to other color space (e.g. YUV)
+     * conversion. As a result RAW streams are not croppable unless supported by the
+     * camera device. See {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES android.scaler.availableStreamUseCases}#CROPPED_RAW for details.</p>
      * <p>For non-raw streams, any additional per-stream cropping will be done to maximize the
      * final pixel area of the stream.</p>
      * <p>For example, if the crop region is set to a 4:3 aspect ratio, then 4:3 streams will use
@@ -3845,6 +3845,7 @@
      * @see CaptureRequest#CONTROL_ZOOM_RATIO
      * @see CaptureRequest#DISTORTION_CORRECTION_MODE
      * @see CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM
+     * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES
      * @see CameraCharacteristics#SCALER_CROPPING_TYPE
      * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE
      * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE_MAXIMUM_RESOLUTION
@@ -3952,6 +3953,60 @@
             new Key<Integer>("android.scaler.rotateAndCrop", int.class);
 
     /**
+     * <p>The region of the sensor that corresponds to the RAW read out for this
+     * capture when the stream use case of a RAW stream is set to CROPPED_RAW.</p>
+     * <p>The coordinate system follows that of {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE android.sensor.info.preCorrectionActiveArraySize}.</p>
+     * <p>This CaptureResult key will be set when the corresponding CaptureRequest has a RAW target
+     * with stream use case set to
+     * {@link android.hardware.camera2.CameraMetadata#SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW },
+     * otherwise it will be {@code null}.
+     * The value of this key specifies the region of the sensor used for the RAW capture and can
+     * be used to calculate the corresponding field of view of RAW streams.
+     * This field of view will always be &gt;= field of view for (processed) non-RAW streams for the
+     * capture. Note: The region specified may not necessarily be centered.</p>
+     * <p>For example: Assume a camera device has a pre correction active array size of
+     * {@code {0, 0, 1500, 2000}}. If the RAW_CROP_REGION is {@code {500, 375, 1500, 1125}}, that
+     * corresponds to a centered crop of 1/4th of the full field of view RAW stream.</p>
+     * <p>The metadata keys which describe properties of RAW frames:</p>
+     * <ul>
+     * <li>{@link CaptureResult#STATISTICS_HOT_PIXEL_MAP android.statistics.hotPixelMap}</li>
+     * <li>{@link CaptureResult#STATISTICS_LENS_SHADING_CORRECTION_MAP android.statistics.lensShadingCorrectionMap}</li>
+     * <li>{@link CameraCharacteristics#LENS_DISTORTION android.lens.distortion}</li>
+     * <li>{@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation}</li>
+     * <li>{@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation}</li>
+     * <li>{@link CameraCharacteristics#LENS_DISTORTION android.lens.distortion}</li>
+     * <li>{@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration}</li>
+     * </ul>
+     * <p>should be interpreted in the effective after raw crop field-of-view coordinate system.
+     * In this coordinate system,
+     * {preCorrectionActiveArraySize.left, preCorrectionActiveArraySize.top} corresponds to the
+     * the top left corner of the cropped RAW frame and
+     * {preCorrectionActiveArraySize.right, preCorrectionActiveArraySize.bottom} corresponds to
+     * the bottom right corner. Client applications must use the values of the keys
+     * in the CaptureResult metadata if present.</p>
+     * <p>Crop regions (android.scaler.CropRegion), AE/AWB/AF regions and face coordinates still
+     * use the {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize} coordinate system as usual.</p>
+     * <p><b>Units</b>: Pixel coordinates relative to
+     * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize} or
+     * {@link CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE android.sensor.info.preCorrectionActiveArraySize} depending on distortion correction
+     * capability and mode</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *
+     * @see CameraCharacteristics#LENS_DISTORTION
+     * @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+     * @see CameraCharacteristics#LENS_POSE_ROTATION
+     * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+     * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE
+     * @see CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE
+     * @see CaptureResult#STATISTICS_HOT_PIXEL_MAP
+     * @see CaptureResult#STATISTICS_LENS_SHADING_CORRECTION_MAP
+     */
+    @PublicKey
+    @NonNull
+    public static final Key<android.graphics.Rect> SCALER_RAW_CROP_REGION =
+            new Key<android.graphics.Rect>("android.scaler.rawCropRegion", android.graphics.Rect.class);
+
+    /**
      * <p>Duration each pixel is exposed to
      * light.</p>
      * <p>If the sensor can't expose this exact duration, it will shorten the
@@ -5643,4 +5698,5 @@
 
 
 
+
 }
diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
index 9a16474..8d742b5 100644
--- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
+++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
@@ -1461,6 +1461,23 @@
         return ret;
     }
 
+    private boolean isCroppedRawSupported() {
+        boolean ret = false;
+
+        long[] streamUseCases =
+                getBase(CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES);
+        if (streamUseCases == null) {
+            return false;
+        }
+        for (long useCase : streamUseCases) {
+            if (useCase == CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW) {
+                return true;
+            }
+        }
+
+        return ret;
+    }
+
     private MandatoryStreamCombination[] getMandatoryStreamCombinationsHelper(
             int mandatoryStreamsType) {
         int[] capabilities = getBase(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
@@ -1472,7 +1489,8 @@
         int hwLevel = getBase(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
         MandatoryStreamCombination.Builder build = new MandatoryStreamCombination.Builder(
                 mCameraId, hwLevel, mDisplaySize, caps, getStreamConfigurationMap(),
-                getStreamConfigurationMapMaximumResolution(), isPreviewStabilizationSupported());
+                getStreamConfigurationMapMaximumResolution(), isPreviewStabilizationSupported(),
+                isCroppedRawSupported());
 
         List<MandatoryStreamCombination> combs = null;
         switch (mandatoryStreamsType) {
diff --git a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
index 0905e1b..6f77d12 100644
--- a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
+++ b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java
@@ -375,6 +375,8 @@
             CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL;
     private static final long STREAM_USE_CASE_VIDEO_CALL =
             CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL;
+    private static final long STREAM_USE_CASE_CROPPED_RAW =
+            CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW;
 
     /**
      * Create a new {@link MandatoryStreamCombination}.
@@ -1262,6 +1264,86 @@
                 "Preview, in-application image processing, and YUV still image capture"),
     };
 
+    private static StreamCombinationTemplate sCroppedRawStreamUseCaseCombinations[] = {
+        // Single stream combination
+        new StreamCombinationTemplate(new StreamTemplate [] {
+                new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_CROPPED_RAW)},
+                "Cropped RAW still image capture without preview"),
+
+        // 2 Stream combinations
+        new StreamCombinationTemplate(new StreamTemplate [] {
+                new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_PREVIEW),
+                new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_CROPPED_RAW)},
+                "Cropped RAW still image capture with preview"),
+        new StreamCombinationTemplate(new StreamTemplate [] {
+                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_PREVIEW),
+                new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_CROPPED_RAW)},
+                "In-app image processing with cropped RAW still image capture"),
+
+        // 3 stream combinations
+        new StreamCombinationTemplate(new StreamTemplate [] {
+                new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_PREVIEW),
+                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_STILL_CAPTURE),
+                new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_CROPPED_RAW)},
+                "Preview with YUV and RAW still image capture"),
+        new StreamCombinationTemplate(new StreamTemplate [] {
+                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_PREVIEW),
+                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_STILL_CAPTURE),
+                new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_CROPPED_RAW)},
+                "In-app image processing with YUV and RAW still image capture"),
+        new StreamCombinationTemplate(new StreamTemplate [] {
+                new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_PREVIEW),
+                new StreamTemplate(ImageFormat.JPEG, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_STILL_CAPTURE),
+                new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_CROPPED_RAW)},
+                "Preview with JPEG and RAW still image capture"),
+        new StreamCombinationTemplate(new StreamTemplate [] {
+                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_PREVIEW),
+                new StreamTemplate(ImageFormat.JPEG, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_STILL_CAPTURE),
+                new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_CROPPED_RAW)},
+                "In-app image processing with JPEG and RAW still image capture"),
+        new StreamCombinationTemplate(new StreamTemplate [] {
+                new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_PREVIEW),
+                new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_RECORD),
+                new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_CROPPED_RAW)},
+                "Preview with video recording and RAW snapshot"),
+        new StreamCombinationTemplate(new StreamTemplate [] {
+                new StreamTemplate(ImageFormat.PRIVATE, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_PREVIEW),
+                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_PREVIEW),
+                new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_CROPPED_RAW)},
+                "Preview with in-app image processing and RAW still image capture"),
+        new StreamCombinationTemplate(new StreamTemplate [] {
+                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_PREVIEW),
+                new StreamTemplate(ImageFormat.YUV_420_888, SizeThreshold.PREVIEW,
+                        STREAM_USE_CASE_PREVIEW),
+                new StreamTemplate(ImageFormat.RAW_SENSOR, SizeThreshold.MAXIMUM,
+                        STREAM_USE_CASE_CROPPED_RAW)},
+                "Two input in-app processing and RAW still image capture"),
+    };
+
     private static StreamCombinationTemplate sPreviewStabilizedStreamCombinations[] = {
         // 1 stream combinations
         new StreamCombinationTemplate(new StreamTemplate [] {
@@ -1317,7 +1399,8 @@
         private StreamConfigurationMap mStreamConfigMap;
         private StreamConfigurationMap mStreamConfigMapMaximumResolution;
         private boolean mIsHiddenPhysicalCamera;
-        private boolean mIsPreviewStabilizationSupported;
+        private boolean mIsPreviewStabilizationSupported = false;
+        private boolean mIsCroppedRawSupported = false;
 
         private final Size kPreviewSizeBound = new Size(1920, 1088);
 
@@ -1331,10 +1414,13 @@
          * @param sm The camera device stream configuration map.
          * @param smMaxResolution The camera device stream configuration map when it runs in max
          *                        resolution mode.
+         * @param previewStabilization The camera device supports preview stabilization.
+         * @param croppedRaw The camera device supports the cropped raw stream use case.
          */
         public Builder(int cameraId, int hwLevel, @NonNull Size displaySize,
                 @NonNull List<Integer> capabilities, @NonNull StreamConfigurationMap sm,
-                StreamConfigurationMap smMaxResolution, boolean previewStabilization) {
+                StreamConfigurationMap smMaxResolution, boolean previewStabilization,
+                boolean isCroppedRawSupported) {
             mCameraId = cameraId;
             mDisplaySize = displaySize;
             mCapabilities = capabilities;
@@ -1344,6 +1430,7 @@
             mIsHiddenPhysicalCamera =
                     CameraManager.isHiddenPhysicalCamera(Integer.toString(mCameraId));
             mIsPreviewStabilizationSupported = previewStabilization;
+            mIsCroppedRawSupported = isCroppedRawSupported;
         }
 
         private @Nullable List<MandatoryStreamCombination>
@@ -1483,10 +1570,23 @@
                 Log.e(TAG, "Available size enumeration failed!");
                 return null;
             }
+            ArrayList<StreamCombinationTemplate> availableTemplates =
+                    new ArrayList<StreamCombinationTemplate> ();
+            availableTemplates.addAll(Arrays.asList(sStreamUseCaseCombinations));
 
             ArrayList<MandatoryStreamCombination> availableStreamCombinations = new ArrayList<>();
-            availableStreamCombinations.ensureCapacity(sStreamUseCaseCombinations.length);
-            for (StreamCombinationTemplate combTemplate : sStreamUseCaseCombinations) {
+            int capacity = sStreamUseCaseCombinations.length;
+            if (mIsCroppedRawSupported) {
+                capacity += sCroppedRawStreamUseCaseCombinations.length;
+                availableStreamCombinations.ensureCapacity(capacity);
+                availableTemplates.addAll(Arrays.asList(sCroppedRawStreamUseCaseCombinations));
+            }
+             else {
+                availableStreamCombinations.ensureCapacity(capacity);
+             }
+
+
+            for (StreamCombinationTemplate combTemplate : availableTemplates) {
                 ArrayList<MandatoryStreamInformation> streamsInfo =
                         new ArrayList<MandatoryStreamInformation>();
                 streamsInfo.ensureCapacity(combTemplate.mStreamTemplates.length);
@@ -2012,6 +2112,7 @@
         private @Nullable HashMap<Pair<SizeThreshold, Integer>, List<Size>>
             enumerateAvailableSizes() {
             final int[] formats = {
+                ImageFormat.RAW_SENSOR,
                 ImageFormat.PRIVATE,
                 ImageFormat.YUV_420_888,
                 ImageFormat.JPEG,
diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java
index 65df5d1..8b7c5ec 100644
--- a/core/java/android/hardware/camera2/params/OutputConfiguration.java
+++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java
@@ -289,7 +289,8 @@
          CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_STILL_CAPTURE,
          CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_RECORD,
          CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL,
-         CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL})
+         CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL,
+         CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW})
     public @interface StreamUseCase {};
 
     /**
@@ -998,7 +999,7 @@
      */
     public void setStreamUseCase(@StreamUseCase long streamUseCase) {
         // Verify that the value is in range
-        long maxUseCaseValue = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL;
+        long maxUseCaseValue = CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_CROPPED_RAW;
         if (streamUseCase > maxUseCaseValue &&
                 streamUseCase < CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VENDOR_START) {
             throw new IllegalArgumentException("Not a valid stream use case value " +
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 23d108f..42803a1 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -139,6 +139,7 @@
             VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED,
             VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED,
             VIRTUAL_DISPLAY_FLAG_OWN_FOCUS,
+            VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface VirtualDisplayFlag {}
@@ -429,6 +430,18 @@
     public static final int VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP = 1 << 15;
 
 
+    /**
+     * Virtual display flags: Indicates that the display should not become the top focused display
+     * by stealing the top focus from another display.
+     *
+     * @see Display#FLAG_STEAL_TOP_FOCUS_DISABLED
+     * @see #createVirtualDisplay
+     * @see #VIRTUAL_DISPLAY_FLAG_OWN_FOCUS
+     * @hide
+     */
+    @SystemApi
+    public static final int VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED = 1 << 16;
+
     /** @hide */
     @IntDef(prefix = {"MATCH_CONTENT_FRAMERATE_"}, value = {
             MATCH_CONTENT_FRAMERATE_UNKNOWN,
diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java
index 891ba36..abd647f 100644
--- a/core/java/android/hardware/display/VirtualDisplayConfig.java
+++ b/core/java/android/hardware/display/VirtualDisplayConfig.java
@@ -23,7 +23,6 @@
 import android.annotation.Nullable;
 import android.hardware.display.DisplayManager.VirtualDisplayFlag;
 import android.media.projection.MediaProjection;
-import android.os.Handler;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.view.Surface;
@@ -35,7 +34,8 @@
 
 /**
  * Holds configuration used to create {@link VirtualDisplay} instances. See
- * {@link MediaProjection#createVirtualDisplay(VirtualDisplayConfig, VirtualDisplay.Callback, Handler)}.
+ * {@link MediaProjection#createVirtualDisplay} and
+ * {@link android.companion.virtual.VirtualDeviceManager.VirtualDevice#createVirtualDisplay}.
  *
  * @hide
  */
diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java
index a31c2e6..ada5c55 100644
--- a/core/java/android/os/Debug.java
+++ b/core/java/android/os/Debug.java
@@ -982,6 +982,43 @@
 
 
     /**
+     * Wait until a debugger attaches. As soon as a debugger attaches,
+     * suspend all Java threads and send VM_START (a.k.a VM_INIT)
+     * packet.
+     *
+     * @hide
+     */
+    public static void suspendAllAndSendVmStart() {
+        if (!VMDebug.isDebuggingEnabled()) {
+            return;
+        }
+
+        // if DDMS is listening, inform them of our plight
+        System.out.println("Sending WAIT chunk");
+        byte[] data = new byte[] { 0 };     // 0 == "waiting for debugger"
+        Chunk waitChunk = new Chunk(ChunkHandler.type("WAIT"), data, 0, 1);
+        DdmServer.sendChunk(waitChunk);
+
+        // We must wait until a debugger is connected (debug socket is
+        // open and at least one non-DDM JDWP packedt has been received.
+        // This guarantees that oj-libjdwp has been attached and that
+        // ART's default implementation of suspendAllAndSendVmStart has
+        // been replaced with an implementation that will suspendAll and
+        // send VM_START.
+        System.out.println("Waiting for debugger first packet");
+        while (!isDebuggerConnected()) {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException ie) {
+            }
+        }
+
+        System.out.println("Debug.suspendAllAndSentVmStart");
+        VMDebug.suspendAllAndSendVmStart();
+        System.out.println("Debug.suspendAllAndSendVmStart, resumed");
+    }
+
+    /**
      * Wait until a debugger attaches.  As soon as the debugger attaches,
      * this returns, so you will need to place a breakpoint after the
      * waitForDebugger() call if you want to start tracing immediately.
diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java
index cfb195b..4892312 100644
--- a/core/java/android/security/net/config/SystemCertificateSource.java
+++ b/core/java/android/security/net/config/SystemCertificateSource.java
@@ -18,6 +18,7 @@
 
 import android.os.Environment;
 import android.os.UserHandle;
+
 import java.io.File;
 
 /**
@@ -32,11 +33,20 @@
     private final File mUserRemovedCaDir;
 
     private SystemCertificateSource() {
-        super(new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts"));
+        super(getDirectory());
         File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
         mUserRemovedCaDir = new File(configDir, "cacerts-removed");
     }
 
+    private static File getDirectory() {
+        // TODO(miguelaranda): figure out correct code path.
+        File updatable_dir = new File("/apex/com.android.conscrypt/cacerts");
+        if (updatable_dir.exists()) {
+            return updatable_dir;
+        }
+        return new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts");
+    }
+
     public static SystemCertificateSource getInstance() {
         return NoPreloadHolder.INSTANCE;
     }
diff --git a/core/java/android/service/controls/ControlsProviderService.java b/core/java/android/service/controls/ControlsProviderService.java
index 65e2390..b85cf6d 100644
--- a/core/java/android/service/controls/ControlsProviderService.java
+++ b/core/java/android/service/controls/ControlsProviderService.java
@@ -61,7 +61,7 @@
      * the same package that will be launched embedded in the device controls space.
      *
      * The activity must be exported, enabled and protected by
-     * {@link Manifest.permission.BIND_CONTROLS}.
+     * {@link Manifest.permission#BIND_CONTROLS}.
      *
      * It is recommended that the activity is declared {@code android:resizeableActivity="true"}.
      */
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 71030bcc..689dce8 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -332,6 +332,34 @@
     public static final int FLAG_OWN_FOCUS = 1 << 11;
 
     /**
+     * Flag: Indicates that the display should not become the top focused display by stealing the
+     * top focus from another display.
+     *
+     * <p>The result is that only targeted input events (displayId of input event matches the
+     * displayId of the display) can reach this display. A display with this flag set can still
+     * become the top focused display, if the system consists of only one display or if all
+     * displays have this flag set. In both cases the default display becomes the top focused
+     * display.
+     *
+     * <p>Note:  A display only has a focused window if either
+     * - the display is the top focused display or
+     * - the display manages its own focus (via {@link #FLAG_OWN_FOCUS})
+     * - or all the displays manage their own focus (via {@code config_perDisplayFocusEnabled} flag)
+     * If a display has no focused window, no input event is dispatched to it. Therefore this
+     * flag is only useful together with {@link #FLAG_OWN_FOCUS} and will be
+     * ignored if it is not set.
+     *
+     * <p>Note: The framework only supports IME on the top focused display (b/262520411). Therefore,
+     * Enabling this flag on a display implicitly disables showing any IME. This is not intended
+     * behavior but cannot be fixed until b/262520411 is implemented. If you need IME on display do
+     * not set this flag.
+     *
+     * @hide
+     * @see #getFlags()
+     */
+    public static final int FLAG_STEAL_TOP_FOCUS_DISABLED = 1 << 12;
+
+    /**
      * Display flag: Indicates that the contents of the display should not be scaled
      * to fit the physical screen dimensions.  Used for development only to emulate
      * devices with smaller physicals screens while preserving density.
@@ -1705,6 +1733,16 @@
         return (mFlags & FLAG_TRUSTED) == FLAG_TRUSTED;
     }
 
+    /**
+     * @return {@code true} if the display can steal the top focus from another display.
+     *
+     * @see #FLAG_STEAL_TOP_FOCUS_DISABLED
+     * @hide
+     */
+    public boolean canStealTopFocus() {
+        return (mFlags & FLAG_STEAL_TOP_FOCUS_DISABLED) == 0;
+    }
+
     private void updateDisplayInfoLocked() {
         // Note: The display manager caches display info objects on our behalf.
         DisplayInfo newInfo = mGlobal.getDisplayInfo(mDisplayId);
diff --git a/core/jni/com_android_internal_os_KernelAllocationStats.cpp b/core/jni/com_android_internal_os_KernelAllocationStats.cpp
index 5b10497..35c9487 100644
--- a/core/jni/com_android_internal_os_KernelAllocationStats.cpp
+++ b/core/jni/com_android_internal_os_KernelAllocationStats.cpp
@@ -44,7 +44,7 @@
 static jobjectArray KernelAllocationStats_getDmabufAllocations(JNIEnv *env, jobject) {
     std::vector<DmaBuffer> buffers;
 
-    if (!dmabufinfo::ReadDmaBufs(&buffers)) {
+    if (!dmabufinfo::ReadProcfsDmaBufs(&buffers)) {
         return nullptr;
     }
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 6dd7073..3274e85 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7429,6 +7429,14 @@
                  android:permission="android.permission.BIND_JOB_SERVICE" >
         </service>
 
+        <service android:name="com.android.server.companion.datatransfer.contextsync.CallMetadataSyncInCallService"
+                 android:permission="android.permission.BIND_INCALL_SERVICE"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.telecom.InCallService"/>
+            </intent-filter>
+        </service>
+
         <provider
             android:name="com.android.server.textclassifier.IconsContentProvider"
             android:authorities="com.android.textclassifier.icons"
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 9b23a41..bddf452 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -2035,6 +2035,12 @@
       "group": "WM_DEBUG_ANIM",
       "at": "com\/android\/server\/wm\/SurfaceAnimator.java"
     },
+    "-206549078": {
+      "message": "Not moving display (displayId=%d) to top. Top focused displayId=%d. Reason: config_perDisplayFocusEnabled",
+      "level": "INFO",
+      "group": "WM_DEBUG_FOCUS_LIGHT",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "-203358733": {
       "message": "commitFinishDrawingLocked: mDrawState=READY_TO_SHOW %s",
       "level": "INFO",
@@ -2263,6 +2269,12 @@
       "group": "WM_DEBUG_RESIZE",
       "at": "com\/android\/server\/wm\/WindowState.java"
     },
+    "34682671": {
+      "message": "Not moving display (displayId=%d) to top. Top focused displayId=%d. Reason: FLAG_STEAL_TOP_FOCUS_DISABLED",
+      "level": "INFO",
+      "group": "WM_DEBUG_FOCUS_LIGHT",
+      "at": "com\/android\/server\/wm\/WindowManagerService.java"
+    },
     "35398067": {
       "message": "goodToGo(): onAnimationStart, transit=%s, apps=%d, wallpapers=%d, nonApps=%d",
       "level": "DEBUG",
diff --git a/graphics/java/android/graphics/Mesh.java b/graphics/java/android/graphics/Mesh.java
index f32e0ee..1f693166 100644
--- a/graphics/java/android/graphics/Mesh.java
+++ b/graphics/java/android/graphics/Mesh.java
@@ -29,7 +29,8 @@
  * {@link #makeIndexed(MeshSpecification, Mode, Buffer, int, ShortBuffer, Rect)} methods,
  * where a {@link MeshSpecification} is required along with various attributes for
  * detailing the mesh object, including a mode, vertex buffer, optional index buffer, and bounds
- * for the mesh.
+ * for the mesh. Once generated, a mesh object can be drawn through
+ * {@link Canvas#drawMesh(Mesh, BlendMode, Paint)}
  *
  * @hide
  */
@@ -53,8 +54,12 @@
      *
      * @param meshSpec     {@link MeshSpecification} used when generating the mesh.
      * @param mode         {@link Mode} enum
-     * @param vertexBuffer vertex buffer representing through {@link Buffer}.
-     * @param vertexCount  the number of vertices represented in the vertexBuffer.
+     * @param vertexBuffer vertex buffer representing through {@link Buffer}. This provides the data
+     *                     for all attributes provided within the meshSpec for every vertex. That
+     *                     is, a vertex buffer should be (attributes size * number of vertices) in
+     *                     length to be valid. Note that currently implementation will have a CPU
+     *                     backed buffer generated.
+     * @param vertexCount  the number of vertices represented in the vertexBuffer and mesh.
      * @param bounds       bounds of the mesh object.
      * @return a new Mesh object.
      */
@@ -70,13 +75,20 @@
     }
 
     /**
-     * Generates an indexed {@link Mesh} object.
+     * Generates a {@link Mesh} object.
      *
      * @param meshSpec     {@link MeshSpecification} used when generating the mesh.
      * @param mode         {@link Mode} enum
-     * @param vertexBuffer vertex buffer representing through {@link Buffer}.
-     * @param vertexCount  the number of vertices represented in the vertexBuffer.
-     * @param indexBuffer  index buffer representing through {@link ShortBuffer}.
+     * @param vertexBuffer vertex buffer representing through {@link Buffer}. This provides the data
+     *                     for all attributes provided within the meshSpec for every vertex. That
+     *                     is, a vertex buffer should be (attributes size * number of vertices) in
+     *                     length to be valid. Note that currently implementation will have a CPU
+     *                     backed buffer generated.
+     * @param vertexCount  the number of vertices represented in the vertexBuffer and mesh.
+     * @param indexBuffer  index buffer representing through {@link ShortBuffer}. Indices are
+     *                     required to be 16 bits, so ShortBuffer is necessary. Note that
+     *                     currently implementation will have a CPU
+     *                     backed buffer generated.
      * @param bounds       bounds of the mesh object.
      * @return a new Mesh object.
      */
@@ -93,7 +105,10 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader
+     * does not have a uniform with that name or if the uniform is declared with a type other than
+     * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is
+     * thrown.
      *
      * @param uniformName name matching the color uniform declared in the shader program.
      * @param color       the provided sRGB color will be converted into the shader program's output
@@ -104,7 +119,10 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader
+     * does not have a uniform with that name or if the uniform is declared with a type other than
+     * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is
+     * thrown.
      *
      * @param uniformName name matching the color uniform declared in the shader program.
      * @param color       the provided sRGB color will be converted into the shader program's output
@@ -116,7 +134,10 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform color value corresponding to the shader assigned to the mesh. If the shader
+     * does not have a uniform with that name or if the uniform is declared with a type other than
+     * vec3 or vec4 and corresponding layout(color) annotation then an IllegalArgumentExcepton is
+     * thrown.
      *
      * @param uniformName name matching the color uniform declared in the shader program.
      * @param color       the provided sRGB color will be converted into the shader program's output
@@ -132,7 +153,9 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+     * not have a uniform with that name or if the uniform is declared with a type other than a
+     * float or float[1] then an IllegalArgumentException is thrown.
      *
      * @param uniformName name matching the float uniform declared in the shader program.
      * @param value       float value corresponding to the float uniform with the given name.
@@ -142,7 +165,9 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+     * not have a uniform with that name or if the uniform is declared with a type other than a
+     * vec2 or float[2] then an IllegalArgumentException is thrown.
      *
      * @param uniformName name matching the float uniform declared in the shader program.
      * @param value1      first float value corresponding to the float uniform with the given name.
@@ -153,7 +178,9 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+     * not have a uniform with that name or if the uniform is declared with a type other than a
+     * vec3 or float[3] then an IllegalArgumentException is thrown.
      *
      * @param uniformName name matching the float uniform declared in the shader program.
      * @param value1      first float value corresponding to the float uniform with the given name.
@@ -166,7 +193,9 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+     * not have a uniform with that name or if the uniform is declared with a type other than a
+     * vec4 or float[4] then an IllegalArgumentException is thrown.
      *
      * @param uniformName name matching the float uniform declared in the shader program.
      * @param value1      first float value corresponding to the float uniform with the given name.
@@ -180,7 +209,10 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+     * not have a uniform with that name or if the uniform is declared with a type other than a
+     * float (for N=1), vecN, or float[N], where N is the length of the values param, then an
+     * IllegalArgumentException is thrown.
      *
      * @param uniformName name matching the float uniform declared in the shader program.
      * @param values      float value corresponding to the vec4 float uniform with the given name.
@@ -210,7 +242,9 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+     * not have a uniform with that name or if the uniform is declared with a type other than int
+     * or int[1] then an IllegalArgumentException is thrown.
      *
      * @param uniformName name matching the int uniform delcared in the shader program.
      * @param value       value corresponding to the int uniform with the given name.
@@ -220,7 +254,9 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+     * not have a uniform with that name or if the uniform is declared with a type other than ivec2
+     * or int[2] then an IllegalArgumentException is thrown.
      *
      * @param uniformName name matching the int uniform delcared in the shader program.
      * @param value1      first value corresponding to the int uniform with the given name.
@@ -231,7 +267,9 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+     * not have a uniform with that name or if the uniform is declared with a type other than ivec3
+     * or int[3] then an IllegalArgumentException is thrown.
      *
      * @param uniformName name matching the int uniform delcared in the shader program.
      * @param value1      first value corresponding to the int uniform with the given name.
@@ -243,7 +281,9 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+     * not have a uniform with that name or if the uniform is declared with a type other than ivec4
+     * or int[4] then an IllegalArgumentException is thrown.
      *
      * @param uniformName name matching the int uniform delcared in the shader program.
      * @param value1      first value corresponding to the int uniform with the given name.
@@ -256,7 +296,10 @@
     }
 
     /**
-     * Sets the uniform color value corresponding to the shader assigned to the mesh.
+     * Sets the uniform value corresponding to the shader assigned to the mesh. If the shader does
+     * not have a uniform with that name or if the uniform is declared with a type other than an
+     * int (for N=1), ivecN, or int[N], where N is the length of the values param, then an
+     * IllegalArgumentException is thrown.
      *
      * @param uniformName name matching the int uniform delcared in the shader program.
      * @param values      int values corresponding to the vec4 int uniform with the given name.
diff --git a/graphics/java/android/graphics/MeshSpecification.java b/graphics/java/android/graphics/MeshSpecification.java
index 45c13af..dd8fb7a 100644
--- a/graphics/java/android/graphics/MeshSpecification.java
+++ b/graphics/java/android/graphics/MeshSpecification.java
@@ -25,7 +25,7 @@
  * generates a {@link MeshSpecification} via the Make method, where multiple parameters to set up
  * the mesh are supplied, including attributes, vertex stride, varyings, and
  * vertex/fragment shaders. There are also additional methods to provide an optional
- * {@link ColorSpace} as well as an alpha type.
+ * {@link ColorSpace} as well as an {@link AlphaType}.
  *
  * Note that there are several limitations on various mesh specifications:
  * 1. The max amount of attributes allowed is 8.
@@ -43,15 +43,30 @@
 
     /**
      * Constants for {@link #make(Attribute[], int, Varying[], String, String, ColorSpace, int)}
-     * to determine alpha type
+     * to determine alpha type. Describes how to interpret the alpha component of a pixel.
      */
     @IntDef({UNKNOWN, OPAQUE, PREMUL, UNPREMULT})
     public @interface AlphaType {
     }
 
+    /**
+     * uninitialized.
+     */
     public static final int UNKNOWN = 0;
+
+    /**
+     * Pixel is opaque.
+     */
     public static final int OPAQUE = 1;
+
+    /**
+     * Pixel components are premultiplied by alpha.
+     */
     public static final int PREMUL = 2;
+
+    /**
+     * Pixel components are independent of alpha.
+     */
     public static final int UNPREMULT = 3;
 
     /**
@@ -61,15 +76,41 @@
     public @interface Type {
     }
 
+    /**
+     * Represents one float. Its equivalent shader type is float.
+     */
     public static final int FLOAT = 0;
+
+    /**
+     * Represents two floats. Its equivalent shader type is float2.
+     */
     public static final int FLOAT2 = 1;
+
+    /**
+     * Represents three floats. Its equivalent shader type is float3.
+     */
     public static final int FLOAT3 = 2;
+
+    /**
+     * Represents four floats. Its equivalent shader type is float4.
+     */
     public static final int FLOAT4 = 3;
+
+    /**
+     * Represents four bytes. Its equivalent shader type is half4.
+     */
     public static final int UBYTE4 = 4;
 
     /**
      * Data class to represent a single attribute in a shader. Note that type parameter must be
      * one of {@link #FLOAT}, {@link #FLOAT2}, {@link #FLOAT3}, {@link #FLOAT4}, or {@link #UBYTE4}.
+     *
+     * Note that offset is the offset in number of bytes. For example, if we had two attributes
+     *
+     * Float3 att1
+     * Float att2
+     *
+     * att1 would have an offset of 0, while att2 would have an offset of 12 bytes.
      */
     public static class Attribute {
         @Type
@@ -106,14 +147,19 @@
     }
 
     /**
-     * Creates a {@link MeshSpecification} object.
+     * Creates a {@link MeshSpecification} object for use within {@link Mesh}.
      *
      * @param attributes     list of attributes represented by {@link Attribute}. Can hold a max of
      *                       8.
-     * @param vertexStride   length of vertex stride. Max of 1024 is accepted.
+     * @param vertexStride   length of vertex stride in bytes. This should be the size of a single
+     *                       vertex' attributes. Max of 1024 is accepted.
      * @param varyings       List of varyings represented by {@link Varying}. Can hold a max of 6.
-     * @param vertexShader   vertex shader to be supplied to the mesh.
-     * @param fragmentShader fragment shader to be suppied to the mesh.
+     *                       Note that `position` is provided by default, does not need to be
+     *                       provided in the list, and does not count towards
+     *                       the 6 varyings allowed.
+     * @param vertexShader   vertex shader to be supplied to the mesh. Ensure that the position
+     *                       varying is set within the shader to get proper results.
+     * @param fragmentShader fragment shader to be supplied to the mesh.
      * @return {@link MeshSpecification} object for use when creating {@link Mesh}
      */
     public static MeshSpecification make(Attribute[] attributes, int vertexStride,
@@ -131,10 +177,14 @@
      *
      * @param attributes     list of attributes represented by {@link Attribute}. Can hold a max of
      *                       8.
-     * @param vertexStride   length of vertex stride. Max of 1024 is accepted.
-     * @param varyings       List of varyings represented by {@link Varying}. Can hold a max of
-     *                       6.
-     * @param vertexShader   vertex shader to be supplied to the mesh.
+     * @param vertexStride   length of vertex stride in bytes. This should be the size of a single
+     *                       vertex' attributes. Max of 1024 is accepted.
+     * @param varyings       List of varyings represented by {@link Varying}. Can hold a max of 6.
+     *                       Note that `position` is provided by default, does not need to be
+     *                       provided in the list, and does not count towards
+     *                       the 6 varyings allowed.
+     * @param vertexShader   vertex shader to be supplied to the mesh. Ensure that the position
+     *                       varying is set within the shader to get proper results.
      * @param fragmentShader fragment shader to be supplied to the mesh.
      * @param colorSpace     {@link ColorSpace} to tell what color space to work in.
      * @return {@link MeshSpecification} object for use when creating {@link Mesh}
@@ -154,10 +204,15 @@
      *
      * @param attributes     list of attributes represented by {@link Attribute}. Can hold a max of
      *                       8.
-     * @param vertexStride   length of vertex stride. Max of 1024 is accepted.
+     * @param vertexStride   length of vertex stride in bytes. This should be the size of a single
+     *                       vertex' attributes. Max of 1024 is accepted.
      * @param varyings       List of varyings represented by {@link Varying}. Can hold a max of 6.
-     * @param vertexShader   vertex shader code to be supplied to the mesh.
-     * @param fragmentShader fragment shader code to be suppied to the mesh.
+     *                       Note that `position` is provided by default, does not need to be
+     *                       provided in the list, and does not count towards
+     *                       the 6 varyings allowed.
+     * @param vertexShader   vertex shader to be supplied to the mesh. Ensure that the position
+     *                       varying is set within the shader to get proper results.
+     * @param fragmentShader fragment shader to be supplied to the mesh.
      * @param colorSpace     {@link ColorSpace} to tell what color space to work in.
      * @param alphaType      Describes how to interpret the alpha component for a pixel. Must be
      *                       one of {@link AlphaType} values.
diff --git a/media/java/android/media/AudioPlaybackConfiguration.java b/media/java/android/media/AudioPlaybackConfiguration.java
index 59a0f7b..f64e5cc 100644
--- a/media/java/android/media/AudioPlaybackConfiguration.java
+++ b/media/java/android/media/AudioPlaybackConfiguration.java
@@ -32,6 +32,8 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -178,6 +180,11 @@
      * Used to update the mute state of a player through its port id
      */
     public static final int PLAYER_UPDATE_MUTED = 7;
+    /**
+     * @hide
+     * Used to update the spatialization status and format of a player through its port id
+     */
+    public static final int PLAYER_UPDATE_FORMAT = 8;
 
     /** @hide */
     @IntDef({
@@ -190,6 +197,7 @@
         PLAYER_UPDATE_DEVICE_ID,
         PLAYER_UPDATE_PORT_ID,
         PLAYER_UPDATE_MUTED,
+        PLAYER_UPDATE_FORMAT,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface PlayerState {}
@@ -206,6 +214,7 @@
             case PLAYER_UPDATE_DEVICE_ID: return "PLAYER_UPDATE_DEVICE_ID";
             case PLAYER_UPDATE_PORT_ID: return "PLAYER_UPDATE_PORT_ID";
             case PLAYER_UPDATE_MUTED: return "PLAYER_UPDATE_MUTED";
+            case PLAYER_UPDATE_FORMAT: return "PLAYER_UPDATE_FORMAT";
             default:
                 return "invalid state " + state;
         }
@@ -213,6 +222,27 @@
 
     /**
      * @hide
+     * Used to update the spatialization status of a player through its port ID. Must be kept in
+     * sync with frameworks/native/include/audiomanager/AudioManager.h
+     */
+    public static final String EXTRA_PLAYER_EVENT_SPATIALIZED =
+            "android.media.extra.PLAYER_EVENT_SPATIALIZED";
+    /**
+     * @hide
+     * Used to update the sample rate of a player through its port ID. Must be kept in sync with
+     * frameworks/native/include/audiomanager/AudioManager.h
+     */
+    public static final String EXTRA_PLAYER_EVENT_SAMPLE_RATE =
+            "android.media.extra.PLAYER_EVENT_SAMPLE_RATE";
+    /**
+     * @hide
+     * Used to update the channel mask of a player through its port ID. Must be kept in sync with
+     * frameworks/native/include/audiomanager/AudioManager.h
+     */
+    public static final String EXTRA_PLAYER_EVENT_CHANNEL_MASK =
+            "android.media.extra.PLAYER_EVENT_CHANNEL_MASK";
+    /**
+     * @hide
      * Used to update the mute state of a player through its port ID. Must be kept in sync with
      * frameworks/native/include/audiomanager/AudioManager.h
      */
@@ -284,10 +314,16 @@
     private int mPlayerState;
     private AudioAttributes mPlayerAttr; // never null
 
+    // lock for updateable properties
+    private final Object mUpdateablePropLock = new Object();
+
+    @GuardedBy("mUpdateablePropLock")
     private int mDeviceId;
-
+    @GuardedBy("mUpdateablePropLock")
     private int mSessionId;
-
+    @GuardedBy("mUpdateablePropLock")
+    private @NonNull FormatInfo mFormatInfo;
+    @GuardedBy("mUpdateablePropLock")
     @PlayerMuteEvent private int mMutedState;
 
     /**
@@ -320,6 +356,7 @@
             mIPlayerShell = null;
         }
         mSessionId = pic.mSessionId;
+        mFormatInfo = FormatInfo.DEFAULT;
     }
 
     /**
@@ -333,13 +370,23 @@
         }
     }
 
+    // sets the fields that are updateable and require synchronization
+    private void setUpdateableFields(int deviceId, int sessionId, int mutedState, FormatInfo format)
+    {
+        synchronized (mUpdateablePropLock) {
+            mDeviceId = deviceId;
+            mSessionId = sessionId;
+            mMutedState = mutedState;
+            mFormatInfo = format;
+        }
+    }
     // Note that this method is called server side, so no "privileged" information is ever sent
     // to a client that is not supposed to have access to it.
     /**
      * @hide
      * Creates a copy of the playback configuration that is stripped of any data enabling
      * identification of which application it is associated with ("anonymized").
-     * @param toSanitize
+     * @param in the instance to copy from
      */
     public static AudioPlaybackConfiguration anonymizedCopy(AudioPlaybackConfiguration in) {
         final AudioPlaybackConfiguration anonymCopy = new AudioPlaybackConfiguration(in.mPlayerIId);
@@ -357,14 +404,16 @@
             builder.setUsage(in.mPlayerAttr.getUsage());
         }
         anonymCopy.mPlayerAttr = builder.build();
-        anonymCopy.mDeviceId = in.mDeviceId;
         // anonymized data
-        anonymCopy.mMutedState = 0;
         anonymCopy.mPlayerType = PLAYER_TYPE_UNKNOWN;
         anonymCopy.mClientUid = PLAYER_UPID_INVALID;
         anonymCopy.mClientPid = PLAYER_UPID_INVALID;
         anonymCopy.mIPlayerShell = null;
-        anonymCopy.mSessionId = AudioSystem.AUDIO_SESSION_ALLOCATE;
+        anonymCopy.setUpdateableFields(
+                /*deviceId*/ PLAYER_DEVICEID_INVALID,
+                /*sessionId*/ AudioSystem.AUDIO_SESSION_ALLOCATE,
+                /*mutedState*/ 0,
+                FormatInfo.DEFAULT);
         return anonymCopy;
     }
 
@@ -401,10 +450,14 @@
      * @return the audio playback device or null if the device is not available at the time of query
      */
     public @Nullable AudioDeviceInfo getAudioDeviceInfo() {
-        if (mDeviceId == PLAYER_DEVICEID_INVALID) {
+        final int deviceId;
+        synchronized (mUpdateablePropLock) {
+            deviceId = mDeviceId;
+        }
+        if (deviceId == PLAYER_DEVICEID_INVALID) {
             return null;
         }
-        return AudioManager.getDeviceForPortId(mDeviceId, AudioManager.GET_DEVICES_OUTPUTS);
+        return AudioManager.getDeviceForPortId(deviceId, AudioManager.GET_DEVICES_OUTPUTS);
     }
 
     /**
@@ -415,7 +468,9 @@
      */
     @SystemApi
     public @IntRange(from = 0) int getSessionId() {
-        return mSessionId;
+        synchronized (mUpdateablePropLock) {
+            return mSessionId;
+        }
     }
 
     /**
@@ -428,7 +483,9 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
     public boolean isMuted() {
-        return mMutedState != 0;
+        synchronized (mUpdateablePropLock) {
+            return mMutedState != 0;
+        }
     }
 
     /**
@@ -440,7 +497,9 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
     @PlayerMuteEvent public int getMutedBy() {
-        return mMutedState;
+        synchronized (mUpdateablePropLock) {
+            return mMutedState;
+        }
     }
 
     /**
@@ -500,6 +559,43 @@
 
     /**
      * @hide
+     * Return whether this player's output is spatialized
+     * @return true if spatialized, false if not or playback hasn't started
+     */
+    @SystemApi
+    public boolean isSpatialized() {
+        synchronized (mUpdateablePropLock) {
+            return mFormatInfo.mIsSpatialized;
+        }
+    }
+
+    /**
+     * @hide
+     * Return the sample rate in Hz of the content being played.
+     * @return the sample rate in Hertz, or 0 if playback hasn't started
+     */
+    @SystemApi
+    public @IntRange(from = 0) int getSampleRate() {
+        synchronized (mUpdateablePropLock) {
+            return mFormatInfo.mSampleRate;
+        }
+    }
+
+    /**
+     * @hide
+     * Return the player's channel mask
+     * @return the channel mask, or 0 if playback hasn't started. See {@link AudioFormat} and
+     *     the definitions for the <code>CHANNEL_OUT_*</code> values used for the mask's bitfield
+     */
+    @SystemApi
+    public int getChannelMask() {
+        synchronized (mUpdateablePropLock) {
+            return (AudioFormat.convertNativeChannelMaskToOutMask(mFormatInfo.mNativeChannelMask));
+        }
+    }
+
+    /**
+     * @hide
      * @return the IPlayer interface for the associated player
      */
     IPlayer getIPlayer() {
@@ -527,9 +623,11 @@
      * @param sessionId the audio session ID
      */
     public boolean handleSessionIdEvent(int sessionId) {
-        final boolean changed = sessionId != mSessionId;
-        mSessionId = sessionId;
-        return changed;
+        synchronized (mUpdateablePropLock) {
+            final boolean changed = sessionId != mSessionId;
+            mSessionId = sessionId;
+            return changed;
+        }
     }
 
     /**
@@ -539,9 +637,25 @@
      * @return true if the state changed, false otherwise
      */
     public boolean handleMutedEvent(@PlayerMuteEvent int mutedState) {
-        final boolean changed = mMutedState != mutedState;
-        mMutedState = mutedState;
-        return changed;
+        synchronized (mUpdateablePropLock) {
+            final boolean changed = mMutedState != mutedState;
+            mMutedState = mutedState;
+            return changed;
+        }
+    }
+
+    /**
+     * @hide
+     * Handle a change of playback format
+     * @param fi the latest format information
+     * @return true if the format changed, false otherwise
+     */
+    public boolean handleFormatEvent(@NonNull FormatInfo fi) {
+        synchronized (mUpdateablePropLock) {
+            final boolean changed = !mFormatInfo.equals(fi);
+            mFormatInfo = fi;
+            return changed;
+        }
     }
 
     /**
@@ -558,7 +672,7 @@
      */
     public boolean handleStateEvent(int event, int deviceId) {
         boolean changed = false;
-        synchronized (this) {
+        synchronized (mUpdateablePropLock) {
 
             // Do not update if it is only device id update
             if (event != PLAYER_UPDATE_DEVICE_ID) {
@@ -647,8 +761,10 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mPlayerIId, mDeviceId, mMutedState, mPlayerType, mClientUid, mClientPid,
-                mSessionId);
+        synchronized (mUpdateablePropLock) {
+            return Objects.hash(mPlayerIId, mDeviceId, mMutedState, mPlayerType, mClientUid,
+                    mClientPid, mSessionId);
+        }
     }
 
     @Override
@@ -658,20 +774,23 @@
 
     @Override
     public void writeToParcel(Parcel dest, int flags) {
-        dest.writeInt(mPlayerIId);
-        dest.writeInt(mDeviceId);
-        dest.writeInt(mMutedState);
-        dest.writeInt(mPlayerType);
-        dest.writeInt(mClientUid);
-        dest.writeInt(mClientPid);
-        dest.writeInt(mPlayerState);
-        mPlayerAttr.writeToParcel(dest, 0);
-        final IPlayerShell ips;
-        synchronized (this) {
-            ips = mIPlayerShell;
+        synchronized (mUpdateablePropLock) {
+            dest.writeInt(mPlayerIId);
+            dest.writeInt(mDeviceId);
+            dest.writeInt(mMutedState);
+            dest.writeInt(mPlayerType);
+            dest.writeInt(mClientUid);
+            dest.writeInt(mClientPid);
+            dest.writeInt(mPlayerState);
+            mPlayerAttr.writeToParcel(dest, 0);
+            final IPlayerShell ips;
+            synchronized (this) {
+                ips = mIPlayerShell;
+            }
+            dest.writeStrongInterface(ips == null ? null : ips.getIPlayer());
+            dest.writeInt(mSessionId);
+            mFormatInfo.writeToParcel(dest, 0);
         }
-        dest.writeStrongInterface(ips == null ? null : ips.getIPlayer());
-        dest.writeInt(mSessionId);
     }
 
     private AudioPlaybackConfiguration(Parcel in) {
@@ -686,6 +805,7 @@
         final IPlayer p = IPlayer.Stub.asInterface(in.readStrongBinder());
         mIPlayerShell = (p == null) ? null : new IPlayerShell(null, p);
         mSessionId = in.readInt();
+        mFormatInfo = FormatInfo.CREATOR.createFromParcel(in);
     }
 
     @Override
@@ -707,35 +827,39 @@
     @Override
     public String toString() {
         StringBuilder apcToString = new StringBuilder();
-        apcToString.append("AudioPlaybackConfiguration piid:").append(mPlayerIId).append(
-                " deviceId:").append(mDeviceId).append(" type:").append(
-                toLogFriendlyPlayerType(mPlayerType)).append(" u/pid:").append(mClientUid).append(
-                "/").append(mClientPid).append(" state:").append(
-                toLogFriendlyPlayerState(mPlayerState)).append(" attr:").append(mPlayerAttr).append(
-                " sessionId:").append(mSessionId).append(" mutedState:");
-        if (mMutedState == 0) {
-            apcToString.append("none ");
-        } else {
-            if ((mMutedState & MUTED_BY_MASTER) != 0) {
-                apcToString.append("master ");
+        synchronized (mUpdateablePropLock) {
+            apcToString.append("AudioPlaybackConfiguration piid:").append(mPlayerIId).append(
+                    " deviceId:").append(mDeviceId).append(" type:").append(
+                    toLogFriendlyPlayerType(mPlayerType)).append(" u/pid:").append(
+                    mClientUid).append(
+                    "/").append(mClientPid).append(" state:").append(
+                    toLogFriendlyPlayerState(mPlayerState)).append(" attr:").append(
+                    mPlayerAttr).append(
+                    " sessionId:").append(mSessionId).append(" mutedState:");
+            if (mMutedState == 0) {
+                apcToString.append("none ");
+            } else {
+                if ((mMutedState & MUTED_BY_MASTER) != 0) {
+                    apcToString.append("master ");
+                }
+                if ((mMutedState & MUTED_BY_STREAM_VOLUME) != 0) {
+                    apcToString.append("streamVolume ");
+                }
+                if ((mMutedState & MUTED_BY_STREAM_MUTED) != 0) {
+                    apcToString.append("streamMute ");
+                }
+                if ((mMutedState & MUTED_BY_APP_OPS) != 0) {
+                    apcToString.append("appOps ");
+                }
+                if ((mMutedState & MUTED_BY_CLIENT_VOLUME) != 0) {
+                    apcToString.append("clientVolume ");
+                }
+                if ((mMutedState & MUTED_BY_VOLUME_SHAPER) != 0) {
+                    apcToString.append("volumeShaper ");
+                }
             }
-            if ((mMutedState & MUTED_BY_STREAM_VOLUME) != 0) {
-                apcToString.append("streamVolume ");
-            }
-            if ((mMutedState & MUTED_BY_STREAM_MUTED) != 0) {
-                apcToString.append("streamMute ");
-            }
-            if ((mMutedState & MUTED_BY_APP_OPS) != 0) {
-                apcToString.append("appOps ");
-            }
-            if ((mMutedState & MUTED_BY_CLIENT_VOLUME) != 0) {
-                apcToString.append("clientVolume ");
-            }
-            if ((mMutedState & MUTED_BY_VOLUME_SHAPER) != 0) {
-                apcToString.append("volumeShaper ");
-            }
+            apcToString.append(" ").append(mFormatInfo);
         }
-
         return apcToString.toString();
     }
 
@@ -788,6 +912,79 @@
     }
 
     //=====================================================================
+
+    /**
+     * @hide
+     * Class to store sample rate, channel mask, and spatialization status
+     */
+    public static final class FormatInfo implements Parcelable {
+        static final FormatInfo DEFAULT = new FormatInfo(
+                /*spatialized*/ false, /*channel mask*/ 0, /*sample rate*/ 0);
+        final boolean mIsSpatialized;
+        final int mNativeChannelMask;
+        final int mSampleRate;
+
+        public FormatInfo(boolean isSpatialized, int nativeChannelMask, int sampleRate) {
+            mIsSpatialized = isSpatialized;
+            mNativeChannelMask = nativeChannelMask;
+            mSampleRate = sampleRate;
+        }
+
+        @Override
+        public String toString() {
+            return "FormatInfo{"
+                    + "isSpatialized=" + mIsSpatialized
+                    + ", channelMask=0x" + Integer.toHexString(mNativeChannelMask)
+                    + ", sampleRate=" + mSampleRate
+                    + '}';
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof FormatInfo)) return false;
+            FormatInfo that = (FormatInfo) o;
+            return mIsSpatialized == that.mIsSpatialized
+                    && mNativeChannelMask == that.mNativeChannelMask
+                    && mSampleRate == that.mSampleRate;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mIsSpatialized, mNativeChannelMask, mSampleRate);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) {
+            dest.writeBoolean(mIsSpatialized);
+            dest.writeInt(mNativeChannelMask);
+            dest.writeInt(mSampleRate);
+        }
+
+        private FormatInfo(Parcel in) {
+            this(
+                    in.readBoolean(), // isSpatialized
+                    in.readInt(),     // channelMask
+                    in.readInt()      // sampleRate
+            );
+        }
+
+        public static final @NonNull Parcelable.Creator<FormatInfo> CREATOR =
+                new Parcelable.Creator<FormatInfo>() {
+            public FormatInfo createFromParcel(Parcel p) {
+                return new FormatInfo(p);
+            }
+            public FormatInfo[] newArray(int size) {
+                return new FormatInfo[size];
+            }
+        };
+    }
+    //=====================================================================
     // Utilities
 
     /** @hide */
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
index f55fb97..9058510 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -169,11 +169,9 @@
             setFloatUniform("in_progress", value)
             val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
 
-            setFloatUniform(
-                "in_size",
-                /* width= */ maxSize.x * curvedProg,
-                /* height= */ maxSize.y * curvedProg
-            )
+            currentWidth = maxSize.x * curvedProg
+            currentHeight = maxSize.y * curvedProg
+            setFloatUniform("in_size", /* width= */ currentWidth, /* height= */ currentHeight)
             setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
             // radius should not exceed width and height values.
             setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg)
@@ -237,4 +235,10 @@
      * False for a ring effect.
      */
     var rippleFill: Boolean = false
+
+    var currentWidth: Float = 0f
+        private set
+
+    var currentHeight: Float = 0f
+        private set
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index ae28a8b..b37c734 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -36,7 +36,7 @@
  */
 open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
 
-    private lateinit var rippleShader: RippleShader
+    protected lateinit var rippleShader: RippleShader
     lateinit var rippleShape: RippleShape
         private set
 
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index efd683f..e4e0bd4 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -190,7 +190,6 @@
             app:layout_constraintBottom_toBottomOf="parent"
             android:contentDescription="@string/screenshot_dismiss_work_profile">
             <ImageView
-                android:id="@+id/screenshot_dismiss_image"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:layout_margin="@dimen/overlay_dismiss_button_margin"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 6a9149e..ea1095d 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1286,6 +1286,9 @@
          translate into their final position. -->
     <dimen name="lockscreen_shade_keyguard_transition_distance">@dimen/lockscreen_shade_media_transition_distance</dimen>
 
+    <!-- DREAMING -> LOCKSCREEN transition: Amount to shift lockscreen content on entering -->
+    <dimen name="dreaming_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen>
+
     <!-- The amount of vertical offset for the keyguard during the full shade transition. -->
     <dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen>
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index 8b9823b..b8e196f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -16,6 +16,8 @@
 
 package com.android.keyguard;
 
+import static java.util.Collections.emptySet;
+
 import android.content.Context;
 import android.os.Trace;
 import android.util.AttributeSet;
@@ -88,8 +90,9 @@
     }
 
     /** Sets a translationY value on every child view except for the media view. */
-    public void setChildrenTranslationYExcludingMediaView(float translationY) {
-        setChildrenTranslationYExcluding(translationY, Set.of(mMediaHostContainer));
+    public void setChildrenTranslationY(float translationY, boolean excludeMedia) {
+        setChildrenTranslationYExcluding(translationY,
+                excludeMedia ? Set.of(mMediaHostContainer) : emptySet());
     }
 
     /** Sets a translationY value on every view except for the views in the provided set. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 7849747..aec3063 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,6 +20,8 @@
 import android.util.Slog;
 
 import com.android.keyguard.KeyguardClockSwitch.ClockSize;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
@@ -59,6 +61,7 @@
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             ConfigurationController configurationController,
             DozeParameters dozeParameters,
+            FeatureFlags featureFlags,
             ScreenOffAnimationController screenOffAnimationController) {
         super(keyguardStatusView);
         mKeyguardSliceViewController = keyguardSliceViewController;
@@ -67,6 +70,8 @@
         mConfigurationController = configurationController;
         mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController,
                 dozeParameters, screenOffAnimationController, /* animateYPos= */ true);
+        mKeyguardVisibilityHelper.setOcclusionTransitionFlagEnabled(
+                featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION));
     }
 
     @Override
@@ -115,8 +120,8 @@
     /**
      * Sets a translationY on the views on the keyguard, except on the media view.
      */
-    public void setTranslationYExcludingMedia(float translationY) {
-        mView.setChildrenTranslationYExcludingMediaView(translationY);
+    public void setTranslationY(float translationY, boolean excludeMedia) {
+        mView.setChildrenTranslationY(translationY, excludeMedia);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index 498304b..bde0692 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -44,6 +44,7 @@
     private boolean mAnimateYPos;
     private boolean mKeyguardViewVisibilityAnimating;
     private boolean mLastOccludedState = false;
+    private boolean mIsUnoccludeTransitionFlagEnabled = false;
     private final AnimationProperties mAnimationProperties = new AnimationProperties();
 
     public KeyguardVisibilityHelper(View view,
@@ -62,6 +63,10 @@
         return mKeyguardViewVisibilityAnimating;
     }
 
+    public void setOcclusionTransitionFlagEnabled(boolean enabled) {
+        mIsUnoccludeTransitionFlagEnabled = enabled;
+    }
+
     /**
      * Set the visibility of a keyguard view based on some new state.
      */
@@ -129,7 +134,7 @@
                 // since it may need to be cancelled due to keyguard lifecycle events.
                 mScreenOffAnimationController.animateInKeyguard(
                         mView, mAnimateKeyguardStatusViewVisibleEndRunnable);
-            } else if (mLastOccludedState && !isOccluded) {
+            } else if (!mIsUnoccludeTransitionFlagEnabled && mLastOccludedState && !isOccluded) {
                 // An activity was displayed over the lock screen, and has now gone away
                 mView.setVisibility(View.VISIBLE);
                 mView.setAlpha(0f);
@@ -142,7 +147,9 @@
                         .start();
             } else {
                 mView.setVisibility(View.VISIBLE);
-                mView.setAlpha(1f);
+                if (!mIsUnoccludeTransitionFlagEnabled) {
+                    mView.setAlpha(1f);
+                }
             }
         } else {
             mView.setVisibility(View.GONE);
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 58dfe30..4268d3e 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -188,10 +188,6 @@
     // TODO(b/260619425): Tracking Bug
     @JvmField val MODERN_ALTERNATE_BOUNCER = unreleasedFlag(219, "modern_alternate_bouncer")
 
-    // TODO(b/262780002): Tracking Bug
-    @JvmField
-    val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
-
     /** Flag to control the migration of face auth to modern architecture. */
     // TODO(b/262838215): Tracking bug
     @JvmField val FACE_AUTH_REFACTOR = unreleasedFlag(220, "face_auth_refactor")
@@ -200,6 +196,15 @@
     // TODO(b/244313043): Tracking bug
     @JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag(221, "biometrics_animation_revamp")
 
+    // TODO(b/262780002): Tracking Bug
+    @JvmField
+    val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
+
+    /** A different path for unocclusion transitions back to keyguard */
+    // TODO(b/262859270): Tracking Bug
+    @JvmField
+    val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = false)
+
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
@@ -315,6 +320,10 @@
     // TODO(b/261734857): Tracking Bug
     @JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
 
+    // TODO(b/263272731): Tracking Bug
+    val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
+        unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
+
     // 1000 - dock
     val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index d597455..3beec36 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -33,7 +33,7 @@
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE;
 import static com.android.systemui.DejankUtils.whitelistIpcs;
-import static com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.TO_LOCKSCREEN_DURATION_MS;
+import static com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.LOCKSCREEN_ANIMATION_DURATION_MS;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -1233,7 +1233,7 @@
 
         mDreamOpenAnimationDuration = context.getResources().getInteger(
                 com.android.internal.R.integer.config_dreamOpenAnimationDuration);
-        mDreamCloseAnimationDuration = (int) TO_LOCKSCREEN_DURATION_MS;
+        mDreamCloseAnimationDuration = (int) LOCKSCREEN_ANIMATION_DURATION_MS;
     }
 
     public void userActivity() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
index 4d60579..188930c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
@@ -179,6 +179,5 @@
     companion object {
         private val DEFAULT_DURATION = 500.milliseconds
         val TO_LOCKSCREEN_DURATION = 1183.milliseconds
-        @JvmField val TO_LOCKSCREEN_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index 9b36e8c..402fac1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -46,12 +47,11 @@
     /** Dream overlay views alpha - fade out */
     val dreamOverlayAlpha: Flow<Float> = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it }
 
-    /** Dream background alpha - fade out */
-    val dreamBackgroundAlpha: Flow<Float> = flowForAnimation(DREAM_BACKGROUND_ALPHA).map { 1f - it }
-
     /** Lockscreen views y-translation */
     fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
-        return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { it * translatePx }
+        return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+            -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
+        }
     }
 
     /** Lockscreen views alpha */
@@ -68,10 +68,12 @@
     companion object {
         /* Length of time before ending the dream activity, in order to start unoccluding */
         val DREAM_ANIMATION_DURATION = 250.milliseconds
+        @JvmField
+        val LOCKSCREEN_ANIMATION_DURATION_MS =
+            (TO_LOCKSCREEN_DURATION - DREAM_ANIMATION_DURATION).inWholeMilliseconds
 
-        val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
+        val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 600.milliseconds)
         val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds)
-        val DREAM_BACKGROUND_ALPHA = AnimationParams(duration = 250.milliseconds)
         val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
         val LOCKSCREEN_ALPHA =
             AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
index 03bc935..8a565fa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
@@ -26,4 +26,8 @@
 class MediaTttFlags @Inject constructor(private val featureFlags: FeatureFlags) {
     /** */
     fun isMediaTttEnabled(): Boolean = featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER)
+
+    /** Check whether the flag for the receiver success state is enabled. */
+    fun isMediaTttReceiverSuccessRippleEnabled(): Boolean =
+        featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 358534c..889147b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -114,6 +114,9 @@
         }
     }
 
+    private var maxRippleWidth: Float = 0f
+    private var maxRippleHeight: Float = 0f
+
     private fun updateMediaTapToTransferReceiverDisplay(
         @StatusBarManager.MediaTransferReceiverState displayState: Int,
         routeInfo: MediaRoute2Info,
@@ -216,7 +219,7 @@
         expandRipple(view.requireViewById(R.id.ripple))
     }
 
-    override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+    override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
         val appIconView = view.getAppIconView()
         appIconView.animate()
                 .translationYBy(getTranslationAmount().toFloat())
@@ -226,7 +229,14 @@
                 .alpha(0f)
                 .setDuration(ICON_ALPHA_ANIM_DURATION)
                 .start()
-        (view.requireViewById(R.id.ripple) as ReceiverChipRippleView).collapseRipple(onAnimationEnd)
+
+        val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
+        if (removalReason == ChipStateReceiver.TRANSFER_TO_RECEIVER_SUCCEEDED.name &&
+                mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()) {
+            expandRippleToFull(rippleView, onAnimationEnd)
+        } else {
+            rippleView.collapseRipple(onAnimationEnd)
+        }
     }
 
     override fun getTouchableRegion(view: View, outRect: Rect) {
@@ -271,12 +281,19 @@
         })
     }
 
-    private fun layoutRipple(rippleView: ReceiverChipRippleView) {
+    private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
         val windowBounds = windowManager.currentWindowMetrics.bounds
         val height = windowBounds.height().toFloat()
         val width = windowBounds.width().toFloat()
 
-        rippleView.setMaxSize(width / 2f, height / 2f)
+        if (isFullScreen) {
+            maxRippleHeight = height * 2f
+            maxRippleWidth = width * 2f
+        } else {
+            maxRippleHeight = height / 2f
+            maxRippleWidth = width / 2f
+        }
+        rippleView.setMaxSize(maxRippleWidth, maxRippleHeight)
         // Center the ripple on the bottom of the screen in the middle.
         rippleView.setCenter(width * 0.5f, height)
         val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
@@ -286,6 +303,11 @@
     private fun View.getAppIconView(): CachingIconView {
         return this.requireViewById(R.id.app_icon)
     }
+
+    private fun expandRippleToFull(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) {
+        layoutRipple(rippleView, true)
+        rippleView.expandToFull(maxRippleHeight, onAnimationEnd)
+    }
 }
 
 val ICON_TRANSLATION_ANIM_DURATION = 30.frames
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index 6e9fc5c..87b2528 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -22,6 +22,7 @@
 import android.util.AttributeSet
 import com.android.systemui.surfaceeffects.ripple.RippleShader
 import com.android.systemui.surfaceeffects.ripple.RippleView
+import kotlin.math.pow
 
 /**
  * An expanding ripple effect for the media tap-to-transfer receiver chip.
@@ -59,4 +60,44 @@
         })
         animator.reverse()
     }
+
+    // Expands the ripple to cover full screen.
+    fun expandToFull(newHeight: Float, onAnimationEnd: Runnable? = null) {
+        if (!isStarted) {
+            return
+        }
+        // Reset all listeners to animator.
+        animator.removeAllListeners()
+        animator.removeAllUpdateListeners()
+
+        // Only show the outline as ripple expands and disappears when animation ends.
+        setRippleFill(false)
+
+        val startingPercentage = calculateStartingPercentage(newHeight)
+        animator.addUpdateListener { updateListener ->
+            val now = updateListener.currentPlayTime
+            val progress = updateListener.animatedValue as Float
+            rippleShader.progress = startingPercentage + (progress * (1 - startingPercentage))
+            rippleShader.distortionStrength = 1 - rippleShader.progress
+            rippleShader.pixelDensity = 1 - rippleShader.progress
+            rippleShader.time = now.toFloat()
+            invalidate()
+        }
+        animator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator?) {
+                animation?.let { visibility = GONE }
+                onAnimationEnd?.run()
+                isStarted = false
+            }
+        })
+        animator.start()
+    }
+
+    // Calculates the actual starting percentage according to ripple shader progress set method.
+    // Check calculations in [RippleShader.progress]
+    fun calculateStartingPercentage(newHeight: Float): Float {
+        val ratio = rippleShader.currentHeight / newHeight
+        val remainingPercentage = (1 - ratio).toDouble().pow(1 / 3.toDouble()).toFloat()
+        return 1 - remainingPercentage
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 1709043..31543df 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -44,6 +44,7 @@
 import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
 import static com.android.systemui.util.DumpUtilsKt.asIndenting;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import static java.lang.Float.isNaN;
 
@@ -139,6 +140,10 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
@@ -679,6 +684,12 @@
     private boolean mGestureWaitForTouchSlop;
     private boolean mIgnoreXTouchSlop;
     private boolean mExpandLatencyTracking;
+    private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
+
+    private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+    private boolean mIsDreamToLockscreenTransitionRunning = false;
+    private int mDreamingToLockscreenTransitionTranslationY;
+    private boolean mUnocclusionTransitionFlagEnabled = false;
 
     private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
             mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
@@ -694,6 +705,12 @@
         }
     };
 
+    private final Consumer<TransitionStep> mDreamingToLockscreenTransition =
+            (TransitionStep step) -> {
+                mIsDreamToLockscreenTransitionRunning =
+                    step.getTransitionState() == TransitionState.RUNNING;
+            };
+
     @Inject
     public NotificationPanelViewController(NotificationPanelView view,
             @Main Handler handler,
@@ -763,6 +780,8 @@
             KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
             KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
             AlternateBouncerInteractor alternateBouncerInteractor,
+            DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel,
+            KeyguardTransitionInteractor keyguardTransitionInteractor,
             DumpManager dumpManager) {
         keyguardStateController.addCallback(new KeyguardStateController.Callback() {
             @Override
@@ -778,6 +797,8 @@
         mShadeLog = shadeLogger;
         mShadeHeightLogger = shadeHeightLogger;
         mGutsManager = gutsManager;
+        mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel;
+        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
         mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
             @Override
             public void onViewAttachedToWindow(View v) {
@@ -923,6 +944,8 @@
         mNotificationPanelUnfoldAnimationController = unfoldComponent.map(
                 SysUIUnfoldComponent::getNotificationPanelUnfoldAnimationController);
 
+        mUnocclusionTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
+
         mQsFrameTranslateController = qsFrameTranslateController;
         updateUserSwitcherFlags();
         mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
@@ -1076,6 +1099,18 @@
         mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
         mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
                 controller.setup(mNotificationContainerParent));
+
+        if (mUnocclusionTransitionFlagEnabled) {
+            collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(),
+                    dreamingToLockscreenTransitionAlpha(mNotificationStackScrollLayoutController));
+
+            collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(
+                    mDreamingToLockscreenTransitionTranslationY),
+                    dreamingToLockscreenTransitionY(mNotificationStackScrollLayoutController));
+
+            collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
+                    mDreamingToLockscreenTransition);
+        }
     }
 
     @VisibleForTesting
@@ -1110,6 +1145,8 @@
         mUdfpsMaxYBurnInOffset = mResources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
         mSplitShadeScrimTransitionDistance = mResources.getDimensionPixelSize(
                 R.dimen.split_shade_scrim_transition_distance);
+        mDreamingToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize(
+                R.dimen.dreaming_to_lockscreen_transition_lockscreen_translation_y);
     }
 
     private void updateViewControllers(KeyguardStatusView keyguardStatusView,
@@ -1773,10 +1810,14 @@
     }
 
     private void updateClock() {
+        if (mIsDreamToLockscreenTransitionRunning) {
+            return;
+        }
         float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha;
         mKeyguardStatusViewController.setAlpha(alpha);
         mKeyguardStatusViewController
-                .setTranslationYExcludingMedia(mKeyguardOnlyTransitionTranslationY);
+            .setTranslationY(mKeyguardOnlyTransitionTranslationY, /* excludeMedia= */true);
+
         if (mKeyguardQsUserSwitchController != null) {
             mKeyguardQsUserSwitchController.setAlpha(alpha);
         }
@@ -2660,7 +2701,9 @@
         } else if (statusBarState == KEYGUARD
                 || statusBarState == StatusBarState.SHADE_LOCKED) {
             mKeyguardBottomArea.setVisibility(View.VISIBLE);
-            mKeyguardBottomArea.setAlpha(1f);
+            if (!mIsDreamToLockscreenTransitionRunning) {
+                mKeyguardBottomArea.setAlpha(1f);
+            }
         } else {
             mKeyguardBottomArea.setVisibility(View.GONE);
         }
@@ -3527,6 +3570,9 @@
     }
 
     private void updateNotificationTranslucency() {
+        if (mIsDreamToLockscreenTransitionRunning) {
+            return;
+        }
         float alpha = 1f;
         if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp
                 && !mHeadsUpManager.hasPinnedHeadsUp()) {
@@ -3582,6 +3628,9 @@
     }
 
     private void updateKeyguardBottomAreaAlpha() {
+        if (mIsDreamToLockscreenTransitionRunning) {
+            return;
+        }
         // There are two possible panel expansion behaviors:
         // • User dragging up to unlock: we want to fade out as quick as possible
         //   (ALPHA_EXPANSION_THRESHOLD) to avoid seeing the bouncer over the bottom area.
@@ -3614,7 +3663,9 @@
     }
 
     private void onExpandingFinished() {
-        mScrimController.onExpandingFinished();
+        if (!mUnocclusionTransitionFlagEnabled) {
+            mScrimController.onExpandingFinished();
+        }
         mNotificationStackScrollLayoutController.onExpansionStopped();
         mHeadsUpManager.onExpandingFinished();
         mConversationNotificationManager.onNotificationPanelExpandStateChanged(isFullyCollapsed());
@@ -5809,6 +5860,32 @@
         mCurrentPanelState = state;
     }
 
+    private Consumer<Float> dreamingToLockscreenTransitionAlpha(
+            NotificationStackScrollLayoutController stackScroller) {
+        return (Float alpha) -> {
+            mKeyguardStatusViewController.setAlpha(alpha);
+            stackScroller.setAlpha(alpha);
+
+            mKeyguardBottomAreaInteractor.setAlpha(alpha);
+            mLockIconViewController.setAlpha(alpha);
+
+            if (mKeyguardQsUserSwitchController != null) {
+                mKeyguardQsUserSwitchController.setAlpha(alpha);
+            }
+            if (mKeyguardUserSwitcherController != null) {
+                mKeyguardUserSwitcherController.setAlpha(alpha);
+            }
+        };
+    }
+
+    private Consumer<Float> dreamingToLockscreenTransitionY(
+                NotificationStackScrollLayoutController stackScroller) {
+        return (Float translationY) -> {
+            mKeyguardStatusViewController.setTranslationY(translationY,  /* excludeMedia= */false);
+            stackScroller.setTranslationY(translationY);
+        };
+    }
+
     @VisibleForTesting
     StatusBarStateController getStatusBarStateController() {
         return mStatusBarStateController;
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 4bcc0b6..c2c38a7 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
@@ -916,6 +916,11 @@
         return mView.getTranslationX();
     }
 
+    /** Set view y-translation */
+    public void setTranslationY(float translationY) {
+        mView.setTranslationY(translationY);
+    }
+
     public int indexOfChild(View view) {
         return mView.indexOfChild(view);
     }
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 d500f99..ee8b861 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -232,7 +232,6 @@
     private boolean mExpansionAffectsAlpha = true;
     private boolean mAnimateChange;
     private boolean mUpdatePending;
-    private boolean mTracking;
     private long mAnimationDuration = -1;
     private long mAnimationDelay;
     private Animator.AnimatorListener mAnimatorListener;
@@ -526,7 +525,6 @@
     }
 
     public void onTrackingStarted() {
-        mTracking = true;
         mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen();
         if (!mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) {
             mAnimatingPanelExpansionOnUnlock = false;
@@ -534,7 +532,6 @@
     }
 
     public void onExpandingFinished() {
-        mTracking = false;
         setUnocclusionAnimationRunning(false);
     }
 
@@ -1450,8 +1447,6 @@
         pw.print(" expansionProgress=");
         pw.println(mTransitionToLockScreenFullShadeNotificationsProgress);
 
-        pw.print("  mTracking=");
-        pw.println(mTracking);
         pw.print("  mDefaultScrimAlpha=");
         pw.println(mDefaultScrimAlpha);
         pw.print("  mPanelExpansionFraction=");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 3e3ce77..61ddf8c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -137,7 +137,7 @@
     private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
     private final AlternateBouncerInteractor mAlternateBouncerInteractor;
     private final BouncerView mPrimaryBouncerView;
-    private final Lazy<com.android.systemui.shade.ShadeController> mShadeController;
+    private final Lazy<ShadeController> mShadeController;
 
     // Local cache of expansion events, to avoid duplicates
     private float mFraction = -1f;
@@ -255,6 +255,7 @@
     final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
     private boolean mIsModernBouncerEnabled;
     private boolean mIsModernAlternateBouncerEnabled;
+    private boolean mIsUnoccludeTransitionFlagEnabled;
 
     private OnDismissAction mAfterKeyguardGoneAction;
     private Runnable mKeyguardGoneCancelAction;
@@ -335,6 +336,7 @@
         mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER);
         mIsModernAlternateBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_ALTERNATE_BOUNCER);
         mAlternateBouncerInteractor = alternateBouncerInteractor;
+        mIsUnoccludeTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
     }
 
     @Override
@@ -891,8 +893,10 @@
             // by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
             reset(isOccluding /* hideBouncerWhenShowing*/);
         }
-        if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
-            mCentralSurfaces.animateKeyguardUnoccluding();
+        if (!mIsUnoccludeTransitionFlagEnabled) {
+            if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
+                mCentralSurfaces.animateKeyguardUnoccluding();
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index 532fbaa..ad48e21 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -331,7 +331,7 @@
             return
         }
 
-        removeViewFromWindow(displayInfo)
+        removeViewFromWindow(displayInfo, removalReason)
 
         // Prune anything that's already timed out before determining if we should re-display a
         // different chipbar.
@@ -358,14 +358,14 @@
         removeViewFromWindow(displayInfo)
     }
 
-    private fun removeViewFromWindow(displayInfo: DisplayInfo) {
+    private fun removeViewFromWindow(displayInfo: DisplayInfo, removalReason: String? = null) {
         val view = displayInfo.view
         if (view == null) {
             logger.logViewRemovalIgnored(displayInfo.info.id, "View is null")
             return
         }
         displayInfo.view = null // Need other places??
-        animateViewOut(view) {
+        animateViewOut(view, removalReason) {
             windowManager.removeView(view)
             displayInfo.wakeLock?.release(displayInfo.info.wakeReason)
         }
@@ -428,7 +428,11 @@
      *
      * @param onAnimationEnd an action that *must* be run once the animation finishes successfully.
      */
-    internal open fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+    internal open fun animateViewOut(
+        view: ViewGroup,
+        removalReason: String? = null,
+        onAnimationEnd: Runnable
+    ) {
         onAnimationEnd.run()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index fd2c705..52980c3 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -211,7 +211,7 @@
         )
     }
 
-    override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+    override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
         val innerView = view.getInnerView()
         innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
         ViewHierarchyAnimator.animateRemoval(
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index 74295f0..45cb11e 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -56,6 +56,7 @@
 import com.android.systemui.user.shared.model.UserActionModel
 import com.android.systemui.user.shared.model.UserModel
 import com.android.systemui.user.utils.MultiUserActionsEvent
+import com.android.systemui.user.utils.MultiUserActionsEventHelper
 import com.android.systemui.util.kotlin.pairwise
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -372,6 +373,9 @@
         if (LegacyUserDataHelper.isUser(record)) {
             // It's safe to use checkNotNull around record.info because isUser only returns true
             // if record.info is not null.
+            uiEventLogger.log(
+                MultiUserActionsEventHelper.userSwitchMetric(checkNotNull(record.info))
+            )
             selectUser(checkNotNull(record.info).id, dialogShower)
         } else {
             executeAction(LegacyUserDataHelper.toUserActionModel(record), dialogShower)
diff --git a/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEvent.kt b/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEvent.kt
index bfedac9..ec79b6d 100644
--- a/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEvent.kt
@@ -23,7 +23,13 @@
     @UiEvent(doc = "Add User tap from User Switcher.") CREATE_USER_FROM_USER_SWITCHER(1257),
     @UiEvent(doc = "Add Guest tap from User Switcher.") CREATE_GUEST_FROM_USER_SWITCHER(1258),
     @UiEvent(doc = "Add Restricted User tap from User Switcher.")
-    CREATE_RESTRICTED_USER_FROM_USER_SWITCHER(1259);
+    CREATE_RESTRICTED_USER_FROM_USER_SWITCHER(1259),
+    @UiEvent(doc = "Switch to   User tap from User Switcher.")
+    SWITCH_TO_USER_FROM_USER_SWITCHER(1266),
+    @UiEvent(doc = "Switch to  Guest User tap from User Switcher.")
+    SWITCH_TO_GUEST_FROM_USER_SWITCHER(1267),
+    @UiEvent(doc = "Switch to Restricted User tap from User Switcher.")
+    SWITCH_TO_RESTRICTED_USER_FROM_USER_SWITCHER(1268);
 
     override fun getId(): Int {
         return value
diff --git a/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEventHelper.kt b/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEventHelper.kt
new file mode 100644
index 0000000..d12a788
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/utils/MultiUserActionsEventHelper.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.user.utils
+
+import android.content.pm.UserInfo
+
+class MultiUserActionsEventHelper {
+    /** Return MultiUserActionsEvent based on UserInfo. */
+    companion object {
+        fun userSwitchMetric(userInfo: UserInfo): MultiUserActionsEvent {
+            if (userInfo.isGuest) return MultiUserActionsEvent.SWITCH_TO_GUEST_FROM_USER_SWITCHER
+            if (userInfo.isRestricted)
+                return MultiUserActionsEvent.SWITCH_TO_RESTRICTED_USER_FROM_USER_SWITCHER
+            return MultiUserActionsEvent.SWITCH_TO_USER_FROM_USER_SWITCHER
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index c94c97c..be4bbdf 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -25,6 +25,7 @@
 import android.testing.AndroidTestingRunner;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -59,6 +60,8 @@
     @Mock
     DozeParameters mDozeParameters;
     @Mock
+    FeatureFlags mFeatureFlags;
+    @Mock
     ScreenOffAnimationController mScreenOffAnimationController;
     @Captor
     private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor;
@@ -77,6 +80,7 @@
                 mKeyguardUpdateMonitor,
                 mConfigurationController,
                 mDozeParameters,
+                mFeatureFlags,
                 mScreenOffAnimationController);
     }
 
@@ -96,9 +100,9 @@
     public void setTranslationYExcludingMedia_forwardsCallToView() {
         float translationY = 123f;
 
-        mController.setTranslationYExcludingMedia(translationY);
+        mController.setTranslationY(translationY, /* excludeMedia= */true);
 
-        verify(mKeyguardStatusView).setChildrenTranslationYExcludingMediaView(translationY);
+        verify(mKeyguardStatusView).setChildrenTranslationY(translationY, /* excludeMedia= */true);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
index ce44f4d..508aea5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
@@ -37,19 +37,23 @@
     fun setChildrenTranslationYExcludingMediaView_mediaViewIsNotTranslated() {
         val translationY = 1234f
 
-        keyguardStatusView.setChildrenTranslationYExcludingMediaView(translationY)
+        keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */true)
 
         assertThat(mediaView.translationY).isEqualTo(0)
-    }
-
-    @Test
-    fun setChildrenTranslationYExcludingMediaView_childrenAreTranslated() {
-        val translationY = 1234f
-
-        keyguardStatusView.setChildrenTranslationYExcludingMediaView(translationY)
 
         childrenExcludingMedia.forEach {
             assertThat(it.translationY).isEqualTo(translationY)
         }
     }
-}
\ No newline at end of file
+
+    @Test
+    fun setChildrenTranslationYIncludeMediaView() {
+        val translationY = 1234f
+
+        keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */false)
+
+        statusViewContainer.children.forEach {
+            assertThat(it.translationY).isEqualTo(translationY)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index 81950f1..c54e456 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -19,6 +19,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -28,6 +29,8 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA
 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -65,10 +68,9 @@
             repository.sendTransitionStep(step(0.5f))
             repository.sendTransitionStep(step(1f))
 
-            // Only two values should be present, since the dream overlay runs for a small fraction
-            // of
-            // the overall animation time
-            assertThat(values.size).isEqualTo(2)
+            // Only 3 values should be present, since the dream overlay runs for a small fraction
+            // of the overall animation time
+            assertThat(values.size).isEqualTo(3)
             assertThat(values[0])
                 .isEqualTo(
                     EMPHASIZED_ACCELERATE.getInterpolation(
@@ -81,6 +83,12 @@
                         animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y)
                     ) * pixels
                 )
+            assertThat(values[2])
+                .isEqualTo(
+                    EMPHASIZED_ACCELERATE.getInterpolation(
+                        animValue(0.5f, DREAM_OVERLAY_TRANSLATION_Y)
+                    ) * pixels
+                )
 
             job.cancel()
         }
@@ -98,8 +106,7 @@
             repository.sendTransitionStep(step(1f))
 
             // Only two values should be present, since the dream overlay runs for a small fraction
-            // of
-            // the overall animation time
+            // of the overall animation time
             assertThat(values.size).isEqualTo(2)
             assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA))
             assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA))
@@ -107,6 +114,77 @@
             job.cancel()
         }
 
+    @Test
+    fun lockscreenFadeIn() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.1f))
+            // Should start running here...
+            repository.sendTransitionStep(step(0.2f))
+            repository.sendTransitionStep(step(0.3f))
+            // ...up to here
+            repository.sendTransitionStep(step(1f))
+
+            // Only two values should be present, since the dream overlay runs for a small fraction
+            // of the overall animation time
+            assertThat(values.size).isEqualTo(2)
+            assertThat(values[0]).isEqualTo(animValue(0.2f, LOCKSCREEN_ALPHA))
+            assertThat(values[1]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+
+            job.cancel()
+        }
+
+    @Test
+    fun lockscreenTranslationY() =
+        runTest(UnconfinedTestDispatcher()) {
+            val values = mutableListOf<Float>()
+
+            val pixels = 100
+            val job =
+                underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+            repository.sendTransitionStep(step(0f))
+            repository.sendTransitionStep(step(0.3f))
+            repository.sendTransitionStep(step(0.5f))
+            repository.sendTransitionStep(step(1f))
+
+            assertThat(values.size).isEqualTo(4)
+            assertThat(values[0])
+                .isEqualTo(
+                    -pixels +
+                        EMPHASIZED_DECELERATE.getInterpolation(
+                            animValue(0f, LOCKSCREEN_TRANSLATION_Y)
+                        ) * pixels
+                )
+            assertThat(values[1])
+                .isEqualTo(
+                    -pixels +
+                        EMPHASIZED_DECELERATE.getInterpolation(
+                            animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
+                        ) * pixels
+                )
+            assertThat(values[2])
+                .isEqualTo(
+                    -pixels +
+                        EMPHASIZED_DECELERATE.getInterpolation(
+                            animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
+                        ) * pixels
+                )
+            assertThat(values[3])
+                .isEqualTo(
+                    -pixels +
+                        EMPHASIZED_DECELERATE.getInterpolation(
+                            animValue(1f, LOCKSCREEN_TRANSLATION_Y)
+                        ) * pixels
+                )
+
+            job.cancel()
+        }
+
     private fun animValue(stepValue: Float, params: AnimationParams): Float {
         val totalDuration = TO_LOCKSCREEN_DURATION
         val startValue = (params.startTime / totalDuration).toFloat()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
index 07a3109..9c4e849 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
@@ -66,7 +66,7 @@
         wakeLockBuilder,
         systemClock,
     ) {
-    override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+    override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
         // Just bypass the animation in tests
         onAnimationEnd.run()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index 03ba3d3..cefc742 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -98,6 +98,7 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
+        whenever(mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()).thenReturn(true)
 
         fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
         whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
index 17eb6e2..bbd62c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FgsManagerControllerTest.java
@@ -147,8 +147,8 @@
         setUserProfiles(0);
         setShowUserVisibleJobs(true);
 
-        UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", 0);
-        UserVisibleJobSummary j2 = new UserVisibleJobSummary(1, 0, "pkg2", 1);
+        UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", null, 0);
+        UserVisibleJobSummary j2 = new UserVisibleJobSummary(1, 0, "pkg2", null, 1);
         Assert.assertEquals(0, mFmc.getNumRunningPackages());
         mIUserVisibleJobObserver.onUserVisibleJobStateChanged(j1, true);
         Assert.assertEquals(1, mFmc.getNumRunningPackages());
@@ -167,8 +167,8 @@
 
         Binder b1 = new Binder();
         Binder b2 = new Binder();
-        UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", 0);
-        UserVisibleJobSummary j3 = new UserVisibleJobSummary(1, 0, "pkg3", 1);
+        UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", null, 0);
+        UserVisibleJobSummary j3 = new UserVisibleJobSummary(1, 0, "pkg3", null, 1);
         Assert.assertEquals(0, mFmc.getNumRunningPackages());
         mIForegroundServiceObserver.onForegroundStateChanged(b1, "pkg1", 0, true);
         Assert.assertEquals(1, mFmc.getNumRunningPackages());
@@ -359,8 +359,8 @@
         // pkg1 has only job
         // pkg2 has both job and fgs
         // pkg3 has only fgs
-        UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", 0);
-        UserVisibleJobSummary j2 = new UserVisibleJobSummary(1, 0, "pkg2", 1);
+        UserVisibleJobSummary j1 = new UserVisibleJobSummary(0, 0, "pkg1", null, 0);
+        UserVisibleJobSummary j2 = new UserVisibleJobSummary(1, 0, "pkg2", null, 1);
         Binder b2 = new Binder();
         Binder b3 = new Binder();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 4843c76..0586a0c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -105,6 +105,8 @@
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.media.controls.ui.KeyguardMediaController;
@@ -288,6 +290,8 @@
     @Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
     @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
     @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor;
+    @Mock private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
+    @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
     @Mock private MotionEvent mDownMotionEvent;
     @Captor
     private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
@@ -505,6 +509,8 @@
                 mKeyguardBottomAreaViewModel,
                 mKeyguardBottomAreaInteractor,
                 mAlternateBouncerInteractor,
+                mDreamingToLockscreenTransitionViewModel,
+                mKeyguardTransitionInteractor,
                 mDumpManager);
         mNotificationPanelViewController.initDependencies(
                 mCentralSurfaces,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
index b9a5bd7..4ef4e6c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
@@ -64,7 +64,7 @@
         wakeLockBuilder,
         systemClock,
     ) {
-    override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+    override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
         // Just bypass the animation in tests
         onAnimationEnd.run()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index ffa4e2d..9534575 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -175,6 +175,8 @@
 
             underTest.onRecordSelected(UserRecord(info = userInfos[1]), dialogShower)
 
+            verify(uiEventLogger, times(1))
+                .log(MultiUserActionsEvent.SWITCH_TO_USER_FROM_USER_SWITCHER)
             verify(dialogShower).dismiss()
             verify(activityManager).switchUser(userInfos[1].id)
             Unit
@@ -190,6 +192,33 @@
 
             underTest.onRecordSelected(UserRecord(info = userInfos.last()))
 
+            verify(uiEventLogger, times(1))
+                .log(MultiUserActionsEvent.SWITCH_TO_GUEST_FROM_USER_SWITCHER)
+            verify(activityManager).switchUser(userInfos.last().id)
+            Unit
+        }
+
+    @Test
+    fun `onRecordSelected - switch to restricted user`() =
+        runBlocking(IMMEDIATE) {
+            var userInfos = createUserInfos(count = 2, includeGuest = false).toMutableList()
+            userInfos.add(
+                UserInfo(
+                    60,
+                    "Restricted user",
+                    /* iconPath= */ "",
+                    /* flags= */ UserInfo.FLAG_FULL,
+                    UserManager.USER_TYPE_FULL_RESTRICTED,
+                )
+            )
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            underTest.onRecordSelected(UserRecord(info = userInfos.last()))
+
+            verify(uiEventLogger, times(1))
+                .log(MultiUserActionsEvent.SWITCH_TO_RESTRICTED_USER_FROM_USER_SWITCHER)
             verify(activityManager).switchUser(userInfos.last().id)
             Unit
         }
@@ -206,6 +235,8 @@
 
             underTest.onRecordSelected(UserRecord(isGuest = true), dialogShower)
 
+            verify(uiEventLogger, times(1))
+                .log(MultiUserActionsEvent.CREATE_GUEST_FROM_USER_SWITCHER)
             verify(dialogShower).dismiss()
             verify(manager).createGuest(any())
             Unit
@@ -221,6 +252,8 @@
 
             underTest.onRecordSelected(UserRecord(isAddSupervisedUser = true), dialogShower)
 
+            verify(uiEventLogger, times(1))
+                .log(MultiUserActionsEvent.CREATE_RESTRICTED_USER_FROM_USER_SWITCHER)
             verify(dialogShower, never()).dismiss()
             verify(activityStarter).startActivity(any(), anyBoolean())
         }
diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java
new file mode 100644
index 0000000..7ea1e6c
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncInCallService.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.datatransfer.contextsync;
+
+import android.telecom.Call;
+import android.telecom.InCallService;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** In-call service to sync call metadata across a user's devices. */
+public class CallMetadataSyncInCallService extends InCallService {
+
+    @VisibleForTesting
+    final Set<CrossDeviceCall> mCurrentCalls = new HashSet<>();
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mCurrentCalls.addAll(getCalls().stream().map(CrossDeviceCall::new).toList());
+    }
+
+    @Override
+    public void onCallAdded(Call call) {
+        onCallAdded(new CrossDeviceCall(call));
+    }
+
+    @VisibleForTesting
+    void onCallAdded(CrossDeviceCall call) {
+        mCurrentCalls.add(call);
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        mCurrentCalls.removeIf(crossDeviceCall -> crossDeviceCall.getCall().equals(call));
+    }
+
+    /** Data holder for a telecom call and additional metadata. */
+    public static final class CrossDeviceCall {
+        private static final AtomicLong sNextId = new AtomicLong(1);
+
+        private final Call mCall;
+        private final long mId;
+
+        public CrossDeviceCall(Call call) {
+            mCall = call;
+            mId = sNextId.getAndIncrement();
+        }
+
+        public Call getCall() {
+            return mCall;
+        }
+
+        public long getId() {
+            return mId;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/MmsServiceBroker.java b/services/core/java/com/android/server/MmsServiceBroker.java
index 59db686..f149fda 100644
--- a/services/core/java/com/android/server/MmsServiceBroker.java
+++ b/services/core/java/com/android/server/MmsServiceBroker.java
@@ -45,6 +45,7 @@
 import android.util.Slog;
 
 import com.android.internal.telephony.IMms;
+import com.android.internal.telephony.TelephonyPermissions;
 import com.android.server.uri.NeededUriGrants;
 import com.android.server.uri.UriGrantsManagerInternal;
 
@@ -337,6 +338,14 @@
                 throws RemoteException {
             Slog.d(TAG, "sendMessage() by " + callingPkg);
             mContext.enforceCallingPermission(Manifest.permission.SEND_SMS, "Send MMS message");
+
+            // Check if user is associated with the subscription
+            if (!TelephonyPermissions.checkSubscriptionAssociatedWithUser(mContext, subId,
+                    Binder.getCallingUserHandle())) {
+                // TODO(b/258629881): Display error dialog.
+		return;
+            }
+
             if (getAppOpsManager().noteOp(AppOpsManager.OP_SEND_SMS, Binder.getCallingUid(),
                     callingPkg, attributionTag, null) != AppOpsManager.MODE_ALLOWED) {
                 Slog.e(TAG, callingPkg + " is not allowed to call sendMessage()");
diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
index f35931ca..99d6228 100644
--- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java
@@ -35,6 +35,7 @@
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.AudioPlaybackConfiguration;
+import android.media.AudioPlaybackConfiguration.FormatInfo;
 import android.media.AudioPlaybackConfiguration.PlayerMuteEvent;
 import android.media.AudioSystem;
 import android.media.IPlaybackConfigDispatcher;
@@ -75,7 +76,7 @@
 public final class PlaybackActivityMonitor
         implements AudioPlaybackConfiguration.PlayerDeathMonitor, PlayerFocusEnforcer {
 
-    public static final String TAG = "AudioService.PlaybackActivityMonitor";
+    public static final String TAG = "AS.PlaybackActivityMon";
 
     /*package*/ static final boolean DEBUG = false;
     /*package*/ static final int VOLUME_SHAPER_SYSTEM_DUCK_ID = 1;
@@ -343,7 +344,7 @@
 
             if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_PORT_ID) {
                 mEventHandler.sendMessage(
-                        mEventHandler.obtainMessage(MSG_L_UPDATE_PORT_EVENT, eventValue, piid));
+                        mEventHandler.obtainMessage(MSG_II_UPDATE_PORT_EVENT, eventValue, piid));
                 return;
             } else if (event == AudioPlaybackConfiguration.PLAYER_STATE_STARTED) {
                 for (Integer uidInteger: mBannedUids) {
@@ -399,7 +400,7 @@
         }
 
         if (DEBUG) {
-            Log.v(TAG, TextUtils.formatSimple("portEvent(portId=%d, event=%s, extras=%s)",
+            Log.v(TAG, TextUtils.formatSimple("BLA portEvent(portId=%d, event=%s, extras=%s)",
                     portId, AudioPlaybackConfiguration.playerStateToString(event), extras));
         }
 
@@ -427,7 +428,12 @@
 
             if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_MUTED) {
                 mEventHandler.sendMessage(
-                        mEventHandler.obtainMessage(MSG_L_UPDATE_PLAYER_MUTED_EVENT, piid,
+                        mEventHandler.obtainMessage(MSG_IIL_UPDATE_PLAYER_MUTED_EVENT, piid,
+                                portId,
+                                extras));
+            } else if (event == AudioPlaybackConfiguration.PLAYER_UPDATE_FORMAT) {
+                mEventHandler.sendMessage(
+                        mEventHandler.obtainMessage(MSG_IIL_UPDATE_PLAYER_FORMAT, piid,
                                 portId,
                                 extras));
             }
@@ -457,7 +463,7 @@
 
                 // remove all port ids mapped to the released player
                 mEventHandler.sendMessage(
-                        mEventHandler.obtainMessage(MSG_L_CLEAR_PORTS_FOR_PIID, piid, /*arg2=*/0));
+                        mEventHandler.obtainMessage(MSG_I_CLEAR_PORTS_FOR_PIID, piid, /*arg2=*/0));
 
                 if (change && mDoNotLogPiidList.contains(piid)) {
                     // do not dispatch a change for a "do not log" player
@@ -1352,6 +1358,21 @@
         }
     }
 
+    private static final class PlayerFormatEvent extends EventLogger.Event {
+        private final int mPlayerIId;
+        private final AudioPlaybackConfiguration.FormatInfo mFormat;
+
+        PlayerFormatEvent(int piid, AudioPlaybackConfiguration.FormatInfo format) {
+            mPlayerIId = piid;
+            mFormat = format;
+        }
+
+        @Override
+        public String eventToString() {
+            return new String("player piid:" + mPlayerIId + " format update:" + mFormat);
+        }
+    }
+
     static final EventLogger
             sEventLogger = new EventLogger(100,
             "playback activity as reported through PlayerBase");
@@ -1478,7 +1499,7 @@
      *     msg.arg1: port id
      *     msg.arg2: piid
      */
-    private static final int MSG_L_UPDATE_PORT_EVENT = 2;
+    private static final int MSG_II_UPDATE_PORT_EVENT = 2;
 
     /**
      * event for player getting muted
@@ -1488,14 +1509,24 @@
      *     msg.obj: extras describing the mute reason
      *         type: PersistableBundle
      */
-    private static final int MSG_L_UPDATE_PLAYER_MUTED_EVENT = 3;
+    private static final int MSG_IIL_UPDATE_PLAYER_MUTED_EVENT = 3;
 
     /**
      * clear all ports assigned to a given piid
      * args:
      *     msg.arg1: the piid
      */
-    private static final int MSG_L_CLEAR_PORTS_FOR_PIID = 4;
+    private static final int MSG_I_CLEAR_PORTS_FOR_PIID = 4;
+
+    /**
+     * event for player reporting playback format and spatialization status
+     * args:
+     *     msg.arg1: piid
+     *     msg.arg2: port id
+     *     msg.obj: extras describing the sample rate, channel mask, spatialized
+     *         type: PersistableBundle
+     */
+    private static final int MSG_IIL_UPDATE_PLAYER_FORMAT = 5;
 
     private void initEventHandler() {
         mEventThread = new HandlerThread(TAG);
@@ -1513,12 +1544,13 @@
                         }
                         mMuteAwaitConnectionTimeoutCb.accept((AudioDeviceAttributes) msg.obj);
                         break;
-                    case MSG_L_UPDATE_PORT_EVENT:
+
+                    case MSG_II_UPDATE_PORT_EVENT:
                         synchronized (mPlayerLock) {
                             mPortIdToPiid.put(/*portId*/msg.arg1, /*piid*/msg.arg2);
                         }
                         break;
-                    case MSG_L_UPDATE_PLAYER_MUTED_EVENT:
+                    case MSG_IIL_UPDATE_PLAYER_MUTED_EVENT:
                         // TODO: replace PersistableBundle with own struct
                         PersistableBundle extras = (PersistableBundle) msg.obj;
                         if (extras == null) {
@@ -1533,14 +1565,18 @@
                             sEventLogger.enqueue(
                                     new PlayerEvent(piid, PLAYER_UPDATE_MUTED, eventValue));
 
-                            final AudioPlaybackConfiguration apc = mPlayers.get(piid);
+                            final AudioPlaybackConfiguration apc;
+                            synchronized (mPlayerLock) {
+                                apc = mPlayers.get(piid);
+                            }
                             if (apc == null || !apc.handleMutedEvent(eventValue)) {
                                 break;  // do not dispatch
                             }
                             dispatchPlaybackChange(/* iplayerReleased= */false);
                         }
                         break;
-                    case MSG_L_CLEAR_PORTS_FOR_PIID:
+
+                    case MSG_I_CLEAR_PORTS_FOR_PIID:
                         int piid = msg.arg1;
                         if (piid == AudioPlaybackConfiguration.PLAYER_PIID_INVALID) {
                             Log.w(TAG, "Received clear ports with invalid piid");
@@ -1554,6 +1590,34 @@
                             }
                         }
                         break;
+
+                    case MSG_IIL_UPDATE_PLAYER_FORMAT:
+                        final PersistableBundle formatExtras = (PersistableBundle) msg.obj;
+                        if (formatExtras == null) {
+                            Log.w(TAG, "Received format event with no extras");
+                            break;
+                        }
+                        final boolean spatialized = formatExtras.getBoolean(
+                                AudioPlaybackConfiguration.EXTRA_PLAYER_EVENT_SPATIALIZED, false);
+                        final int sampleRate = formatExtras.getInt(
+                                AudioPlaybackConfiguration.EXTRA_PLAYER_EVENT_SAMPLE_RATE, 0);
+                        final int nativeChannelMask = formatExtras.getInt(
+                                AudioPlaybackConfiguration.EXTRA_PLAYER_EVENT_CHANNEL_MASK, 0);
+                        final FormatInfo format =
+                                new FormatInfo(spatialized, nativeChannelMask, sampleRate);
+
+                        sEventLogger.enqueue(new PlayerFormatEvent(msg.arg1, format));
+
+                        final AudioPlaybackConfiguration apc;
+                        synchronized (mPlayerLock) {
+                            apc = mPlayers.get(msg.arg1);
+                        }
+                        if (apc == null || !apc.handleFormatEvent(format)) {
+                            break;  // do not dispatch
+                        }
+                        // TODO optimize for no dispatch to non-privileged listeners
+                        dispatchPlaybackChange(/* iplayerReleased= */false);
+                        break;
                     default:
                         break;
                 }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index fe1d1a6..b7b7031 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -180,6 +180,15 @@
     public static final int FLAG_DEVICE_DISPLAY_GROUP = 1 << 18;
 
     /**
+     * Flag: Indicates that the display should not become the top focused display by stealing the
+     * top focus from another display.
+     *
+     * @see Display#FLAG_STEAL_TOP_FOCUS_DISABLED
+     * @hide
+     */
+    public static final int FLAG_STEAL_TOP_FOCUS_DISABLED = 1 << 19;
+
+    /**
      * Touch attachment: Display does not receive touch.
      */
     public static final int TOUCH_NONE = 0;
@@ -649,6 +658,9 @@
         if ((flags & FLAG_OWN_FOCUS) != 0) {
             msg.append(", FLAG_OWN_FOCUS");
         }
+        if ((flags & FLAG_STEAL_TOP_FOCUS_DISABLED) != 0) {
+            msg.append(", FLAG_STEAL_TOP_FOCUS_DISABLED");
+        }
         return msg.toString();
     }
 }
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index ad426b5..dfdbce5 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -361,6 +361,9 @@
             if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_OWN_FOCUS) != 0) {
                 mBaseDisplayInfo.flags |= Display.FLAG_OWN_FOCUS;
             }
+            if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_STEAL_TOP_FOCUS_DISABLED) != 0) {
+                mBaseDisplayInfo.flags |= Display.FLAG_STEAL_TOP_FOCUS_DISABLED;
+            }
             Rect maskingInsets = getMaskingInsets(deviceInfo);
             int maskedWidth = deviceInfo.width - maskingInsets.left - maskingInsets.right;
             int maskedHeight = deviceInfo.height - maskingInsets.top - maskingInsets.bottom;
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index 7c647cf..5a5be4e 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -28,6 +28,7 @@
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED;
 import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
@@ -35,6 +36,7 @@
 import static com.android.server.display.DisplayDeviceInfo.FLAG_ALWAYS_UNLOCKED;
 import static com.android.server.display.DisplayDeviceInfo.FLAG_DEVICE_DISPLAY_GROUP;
 import static com.android.server.display.DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP;
+import static com.android.server.display.DisplayDeviceInfo.FLAG_STEAL_TOP_FOCUS_DISABLED;
 import static com.android.server.display.DisplayDeviceInfo.FLAG_TOUCH_FEEDBACK_DISABLED;
 import static com.android.server.display.DisplayDeviceInfo.FLAG_TRUSTED;
 
@@ -526,6 +528,17 @@
                                 + "VIRTUAL_DISPLAY_FLAG_TRUSTED.");
                     }
                 }
+                if ((mFlags & VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED) != 0) {
+                    if ((mFlags & VIRTUAL_DISPLAY_FLAG_TRUSTED) != 0
+                            && (mFlags & VIRTUAL_DISPLAY_FLAG_OWN_FOCUS) != 0) {
+                        mInfo.flags |= FLAG_STEAL_TOP_FOCUS_DISABLED;
+                    } else {
+                        Slog.w(TAG,
+                                "Ignoring VIRTUAL_DISPLAY_FLAG_STEAL_TOP_FOCUS_DISABLED as it "
+                                        + "requires VIRTUAL_DISPLAY_FLAG_OWN_FOCUS which requires "
+                                        + "VIRTUAL_DISPLAY_FLAG_TRUSTED.");
+                    }
+                }
 
                 mInfo.type = Display.TYPE_VIRTUAL;
                 mInfo.touch = ((mFlags & VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH) == 0) ?
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 96e7b03..6303bdc 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -899,10 +899,16 @@
 
     @ServiceThreadOnly
     void startArcAction(boolean enabled) {
+        startArcAction(enabled, null);
+    }
+
+    @ServiceThreadOnly
+    void startArcAction(boolean enabled, IHdmiControlCallback callback) {
         assertRunOnServiceThread();
         HdmiDeviceInfo info = getAvrDeviceInfo();
         if (info == null) {
             Slog.w(TAG, "Failed to start arc action; No AVR device.");
+            invokeCallback(callback, HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
             return;
         }
         if (!canStartArcUpdateAction(info.getLogicalAddress(), enabled)) {
@@ -910,19 +916,37 @@
             if (enabled && !isConnectedToArcPort(info.getPhysicalAddress())) {
                 displayOsd(OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT);
             }
+            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
+            return;
+        }
+        if (enabled && mService.earcBlocksArcConnection()) {
+            Slog.i(TAG,
+                    "ARC connection blocked because eARC connection is established or being "
+                            + "established.");
+            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
             return;
         }
 
-        // Terminate opposite action and start action if not exist.
+        // Terminate opposite action and create an action with callback.
         if (enabled) {
             removeAction(RequestArcTerminationAction.class);
-            if (!hasAction(RequestArcInitiationAction.class)) {
-                addAndStartAction(new RequestArcInitiationAction(this, info.getLogicalAddress()));
+            if (hasAction(RequestArcInitiationAction.class)) {
+                RequestArcInitiationAction existingInitiationAction =
+                        getActions(RequestArcInitiationAction.class).get(0);
+                existingInitiationAction.addCallback(callback);
+            } else {
+                addAndStartAction(
+                        new RequestArcInitiationAction(this, info.getLogicalAddress(), callback));
             }
         } else {
             removeAction(RequestArcInitiationAction.class);
-            if (!hasAction(RequestArcTerminationAction.class)) {
-                addAndStartAction(new RequestArcTerminationAction(this, info.getLogicalAddress()));
+            if (hasAction(RequestArcTerminationAction.class)) {
+                RequestArcTerminationAction existingTerminationAction =
+                        getActions(RequestArcTerminationAction.class).get(0);
+                existingTerminationAction.addCallback(callback);
+            } else {
+                addAndStartAction(
+                        new RequestArcTerminationAction(this, info.getLogicalAddress(), callback));
             }
         }
     }
@@ -1010,6 +1034,13 @@
     protected int handleInitiateArc(HdmiCecMessage message) {
         assertRunOnServiceThread();
 
+        if (mService.earcBlocksArcConnection()) {
+            Slog.i(TAG,
+                    "ARC connection blocked because eARC connection is established or being "
+                            + "established.");
+            return Constants.ABORT_NOT_IN_CORRECT_MODE;
+        }
+
         if (!canStartArcUpdateAction(message.getSource(), true)) {
             HdmiDeviceInfo avrDeviceInfo = getAvrDeviceInfo();
             if (avrDeviceInfo == null) {
@@ -1023,9 +1054,8 @@
             return Constants.ABORT_REFUSED;
         }
 
-        // In case where <Initiate Arc> is started by <Request ARC Initiation>
-        // need to clean up RequestArcInitiationAction.
-        removeAction(RequestArcInitiationAction.class);
+        // In case where <Initiate Arc> is started by <Request ARC Initiation>, this message is
+        // handled in RequestArcInitiationAction as well.
         SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
                 message.getSource(), true);
         addAndStartAction(action);
@@ -1059,9 +1089,8 @@
             return Constants.HANDLED;
         }
         // Do not check ARC configuration since the AVR might have been already removed.
-        // Clean up RequestArcTerminationAction in case <Terminate Arc> was started by
-        // <Request ARC Termination>.
-        removeAction(RequestArcTerminationAction.class);
+        // In case where <Terminate Arc> is started by <Request ARC Termination>, this
+        // message is handled in RequestArcTerminationAction as well.
         SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
                 message.getSource(), false);
         addAndStartAction(action);
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 85bd16b..f66f8ea 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -26,6 +26,7 @@
 import static com.android.server.hdmi.Constants.ADDR_UNREGISTERED;
 import static com.android.server.hdmi.Constants.DISABLED;
 import static com.android.server.hdmi.Constants.ENABLED;
+import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_ARC_PENDING;
 import static com.android.server.hdmi.Constants.OPTION_MHL_ENABLE;
 import static com.android.server.hdmi.Constants.OPTION_MHL_INPUT_SWITCHING;
 import static com.android.server.hdmi.Constants.OPTION_MHL_POWER_CHARGE;
@@ -392,7 +393,7 @@
     // and the eARC HAL is present.
     @GuardedBy("mLock")
     @VisibleForTesting
-    protected boolean mEarcSupported;
+    private boolean mEarcSupported;
 
     // Set to true while the eARC feature is enabled.
     @GuardedBy("mLock")
@@ -725,7 +726,7 @@
             if (isEarcEnabled()) {
                 initializeEarc(INITIATED_BY_BOOT_UP);
             } else {
-                setEarcEnabledInHal(false);
+                setEarcEnabledInHal(false, false);
             }
         }
 
@@ -3236,6 +3237,9 @@
     }
 
     private void invokeCallback(IHdmiControlCallback callback, int result) {
+        if (callback == null) {
+            return;
+        }
         try {
             callback.onComplete(result);
         } catch (RemoteException e) {
@@ -3518,7 +3522,7 @@
                 }
                 initializeEarc(startReason);
             } else {
-                setEarcEnabledInHal(false);
+                setEarcEnabledInHal(false, false);
             }
         }
         // TODO: Initialize MHL local devices.
@@ -4414,8 +4418,17 @@
 
     private void initializeEarc(int initiatedBy) {
         Slog.i(TAG, "eARC initialized, reason = " + initiatedBy);
-        setEarcEnabledInHal(true);
         initializeEarcLocalDevice(initiatedBy);
+
+        if (initiatedBy == INITIATED_BY_ENABLE_EARC) {
+            // Since ARC and eARC cannot be connected simultaneously, we need to terminate ARC
+            // before even enabling eARC.
+            setEarcEnabledInHal(true, true);
+        } else {
+            // On boot, wake-up, and hotplug in, eARC will always be attempted before ARC.
+            // So there is no need to explicitly terminate ARC before enabling eARC.
+            setEarcEnabledInHal(true, false);
+        }
     }
 
     @ServiceThreadOnly
@@ -4464,13 +4477,14 @@
 
     @ServiceThreadOnly
     private void onEnableEarc() {
+        // This will terminate ARC as well.
         initializeEarc(INITIATED_BY_ENABLE_EARC);
     }
 
     @ServiceThreadOnly
     private void onDisableEarc() {
         disableEarcLocalDevice();
-        setEarcEnabledInHal(false);
+        setEarcEnabledInHal(false, false);
         clearEarcLocalDevice();
     }
 
@@ -4504,12 +4518,28 @@
 
     @ServiceThreadOnly
     @VisibleForTesting
-    protected void setEarcEnabledInHal(boolean enabled) {
+    protected void setEarcEnabledInHal(boolean enabled, boolean terminateArcFirst) {
         assertRunOnServiceThread();
-        mEarcController.setEarcEnabled(enabled);
-        mCecController.setHpdSignalType(
-                enabled ? Constants.HDMI_HPD_TYPE_STATUS_BIT : Constants.HDMI_HPD_TYPE_PHYSICAL,
-                mEarcPortId);
+        if (terminateArcFirst) {
+            startArcAction(false, new IHdmiControlCallback.Stub() {
+                @Override
+                public void onComplete(int result) throws RemoteException {
+                    // Independently of the result (i.e. independently of whether the ARC RX device
+                    // responded with <Terminate ARC> or not), we always end up terminating ARC in
+                    // the HAL. As soon as we do that, we can enable eARC in the HAL.
+                    mEarcController.setEarcEnabled(enabled);
+                    mCecController.setHpdSignalType(
+                            enabled ? Constants.HDMI_HPD_TYPE_STATUS_BIT
+                                    : Constants.HDMI_HPD_TYPE_PHYSICAL,
+                            mEarcPortId);
+                }
+            });
+        } else {
+            mEarcController.setEarcEnabled(enabled);
+            mCecController.setHpdSignalType(
+                    enabled ? Constants.HDMI_HPD_TYPE_STATUS_BIT : Constants.HDMI_HPD_TYPE_PHYSICAL,
+                    mEarcPortId);
+        }
     }
 
     @ServiceThreadOnly
@@ -4540,4 +4570,21 @@
             mEarcLocalDevice.handleEarcCapabilitiesReported(rawCapabilities);
         }
     }
+
+    protected boolean earcBlocksArcConnection() {
+        if (mEarcLocalDevice == null) {
+            return false;
+        }
+        synchronized (mLock) {
+            return mEarcLocalDevice.mEarcStatus != HDMI_EARC_STATUS_ARC_PENDING;
+        }
+    }
+
+    protected void startArcAction(boolean enabled, IHdmiControlCallback callback) {
+        if (!isTvDeviceEnabled()) {
+            invokeCallback(callback, HdmiControlManager.RESULT_INCORRECT_MODE);
+        } else {
+            tv().startArcAction(enabled, callback);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java
index c7c7702..abb8439 100644
--- a/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java
+++ b/services/core/java/com/android/server/hdmi/HdmiEarcLocalDeviceTx.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_ARC_PENDING;
 import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_CONNECTED;
+import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_PENDING;
 import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_IDLE;
 
 import android.hardware.hdmi.HdmiDeviceInfo;
@@ -69,23 +70,36 @@
     HdmiEarcLocalDeviceTx(HdmiControlService service) {
         super(service, HdmiDeviceInfo.DEVICE_TV);
 
+        synchronized (mLock) {
+            mEarcStatus = HDMI_EARC_STATUS_EARC_PENDING;
+        }
         mReportCapsHandler = new Handler(service.getServiceLooper());
         mReportCapsRunnable = new ReportCapsRunnable();
     }
 
     protected void handleEarcStateChange(@Constants.EarcStatus int status) {
+        int oldEarcStatus;
         synchronized (mLock) {
             HdmiLogger.debug(TAG, "eARC state change [old:%b new %b]", mEarcStatus,
                     status);
+            oldEarcStatus = mEarcStatus;
             mEarcStatus = status;
         }
 
         mReportCapsHandler.removeCallbacksAndMessages(null);
         if (status == HDMI_EARC_STATUS_IDLE) {
             notifyEarcStatusToAudioService(false, new ArrayList<>());
+            mService.startArcAction(false, null);
         } else if (status == HDMI_EARC_STATUS_ARC_PENDING) {
             notifyEarcStatusToAudioService(false, new ArrayList<>());
+            mService.startArcAction(true, null);
+        } else if (status == HDMI_EARC_STATUS_EARC_PENDING
+                && oldEarcStatus == HDMI_EARC_STATUS_ARC_PENDING) {
+            mService.startArcAction(false, null);
         } else if (status == HDMI_EARC_STATUS_EARC_CONNECTED) {
+            if (oldEarcStatus == HDMI_EARC_STATUS_ARC_PENDING) {
+                mService.startArcAction(false, null);
+            }
             mReportCapsHandler.postDelayed(mReportCapsRunnable, REPORT_CAPS_MAX_DELAY_MS);
         }
     }
diff --git a/services/core/java/com/android/server/hdmi/RequestArcAction.java b/services/core/java/com/android/server/hdmi/RequestArcAction.java
index 3d9a290..54c8c00 100644
--- a/services/core/java/com/android/server/hdmi/RequestArcAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestArcAction.java
@@ -16,7 +16,9 @@
 
 package com.android.server.hdmi;
 
+import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
+import android.hardware.hdmi.IHdmiControlCallback;
 
 /**
  * Base feature action class for &lt;Request ARC Initiation&gt;/&lt;Request ARC Termination&gt;.
@@ -35,41 +37,19 @@
      *
      * @param source {@link HdmiCecLocalDevice} instance
      * @param avrAddress address of AV receiver. It should be AUDIO_SYSTEM type
+     * @param callback callback to inform about the status of the action
      * @throws IllegalArgumentException if device type of sourceAddress and avrAddress
      *                      is invalid
      */
-    RequestArcAction(HdmiCecLocalDevice source, int avrAddress) {
-        super(source);
+    RequestArcAction(HdmiCecLocalDevice source, int avrAddress, IHdmiControlCallback callback) {
+        super(source, callback);
         HdmiUtils.verifyAddressType(getSourceAddress(), HdmiDeviceInfo.DEVICE_TV);
         HdmiUtils.verifyAddressType(avrAddress, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
         mAvrAddress = avrAddress;
     }
 
-    @Override
-    boolean processCommand(HdmiCecMessage cmd) {
-        if (mState != STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE
-                || !HdmiUtils.checkCommandSource(cmd, mAvrAddress, TAG)) {
-            return false;
-        }
-        int opcode = cmd.getOpcode();
-        switch (opcode) {
-            // Handles only <Feature Abort> here and, both <Initiate ARC> and <Terminate ARC>
-            // are handled in HdmiControlService itself because both can be
-            // received without <Request ARC Initiation> or <Request ARC Termination>.
-            case Constants.MESSAGE_FEATURE_ABORT:
-                int originalOpcode = cmd.getParams()[0] & 0xFF;
-                if (originalOpcode == Constants.MESSAGE_REQUEST_ARC_TERMINATION) {
-                    disableArcTransmission();
-                    finish();
-                    return true;
-                } else if (originalOpcode == Constants.MESSAGE_REQUEST_ARC_INITIATION) {
-                    tv().disableArc();
-                    finish();
-                    return true;
-                }
-                return false;
-        }
-        return false;
+    RequestArcAction(HdmiCecLocalDevice source, int avrAddress) {
+        this(source, avrAddress, null);
     }
 
     protected final void disableArcTransmission() {
@@ -86,6 +66,6 @@
         }
         HdmiLogger.debug("[T] RequestArcAction.");
         disableArcTransmission();
-        finish();
+        finishWithCallback(HdmiControlManager.RESULT_TIMEOUT);
     }
 }
diff --git a/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java b/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
index 3b7f1dd..db21a33 100644
--- a/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestArcInitiationAction.java
@@ -16,6 +16,8 @@
 
 package com.android.server.hdmi;
 
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
 
 /**
@@ -35,6 +37,16 @@
         super(source, avrAddress);
     }
 
+    /**
+     * @Constructor
+     *
+     * For more details look at {@link RequestArcAction#RequestArcAction}.
+     */
+    RequestArcInitiationAction(HdmiCecLocalDevice source, int avrAddress,
+            IHdmiControlCallback callback) {
+        super(source, avrAddress, callback);
+    }
+
     @Override
     boolean start() {
         // Seq #38
@@ -49,10 +61,34 @@
                 if (error != SendMessageResult.SUCCESS) {
                     // Turn off ARC status if <Request ARC Initiation> fails.
                     tv().disableArc();
-                    finish();
+                    finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
                 }
             }
         });
         return true;
     }
+
+    @Override
+    boolean processCommand(HdmiCecMessage cmd) {
+        if (mState != STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE
+                || !HdmiUtils.checkCommandSource(cmd, mAvrAddress, TAG)) {
+            return false;
+        }
+        int opcode = cmd.getOpcode();
+        switch (opcode) {
+            case Constants.MESSAGE_FEATURE_ABORT:
+                int originalOpcode = cmd.getParams()[0] & 0xFF;
+                if (originalOpcode == Constants.MESSAGE_REQUEST_ARC_INITIATION) {
+                    tv().disableArc();
+                    finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+                    return true;
+                }
+                return false;
+            case Constants.MESSAGE_INITIATE_ARC:
+                finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
+                // This message still needs to be handled in HdmiCecLocalDeviceTv as well.
+                return false;
+        }
+        return false;
+    }
 }
diff --git a/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java b/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java
index 8b5a2931..85128b6 100644
--- a/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestArcTerminationAction.java
@@ -16,6 +16,8 @@
 
 package com.android.server.hdmi;
 
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
 
 /**
@@ -35,6 +37,16 @@
         super(source, avrAddress);
     }
 
+    /**
+     * @Constructor
+     *
+     * @see RequestArcAction#RequestArcAction
+     */
+    RequestArcTerminationAction(HdmiCecLocalDevice source, int avrAddress,
+            IHdmiControlCallback callback) {
+        super(source, avrAddress, callback);
+    }
+
     @Override
     boolean start() {
         mState = STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE;
@@ -49,10 +61,34 @@
                     // If failed to send <Request ARC Termination>, start "Disabled" ARC
                     // transmission action.
                     disableArcTransmission();
-                    finish();
+                    finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
                 }
             }
         });
         return true;
     }
+
+    @Override
+    boolean processCommand(HdmiCecMessage cmd) {
+        if (mState != STATE_WATING_FOR_REQUEST_ARC_REQUEST_RESPONSE
+                || !HdmiUtils.checkCommandSource(cmd, mAvrAddress, TAG)) {
+            return false;
+        }
+        int opcode = cmd.getOpcode();
+        switch (opcode) {
+            case Constants.MESSAGE_FEATURE_ABORT:
+                int originalOpcode = cmd.getParams()[0] & 0xFF;
+                if (originalOpcode == Constants.MESSAGE_REQUEST_ARC_TERMINATION) {
+                    disableArcTransmission();
+                    finishWithCallback(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+                    return true;
+                }
+                return false;
+            case Constants.MESSAGE_TERMINATE_ARC:
+                finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
+                // This message still needs to be handled in HdmiCecLocalDeviceTv as well.
+                return false;
+        }
+        return false;
+    }
 }
diff --git a/services/core/java/com/android/server/locksettings/AuthSecretHidlAdapter.java b/services/core/java/com/android/server/locksettings/AuthSecretHidlAdapter.java
new file mode 100644
index 0000000..3b5f340
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/AuthSecretHidlAdapter.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings;
+
+import android.hardware.authsecret.IAuthSecret;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.util.ArrayList;
+
+/**
+ * Adapt the legacy HIDL interface to present the AIDL interface.
+ */
+class AuthSecretHidlAdapter implements IAuthSecret {
+    // private final String TAG = "AuthSecretHidlAdapter";
+    private final android.hardware.authsecret.V1_0.IAuthSecret mImpl;
+
+    AuthSecretHidlAdapter(android.hardware.authsecret.V1_0.IAuthSecret impl) {
+        mImpl = impl;
+    }
+
+    @Override
+    public void setPrimaryUserCredential(byte[] secret) throws RemoteException {
+        final ArrayList<Byte> secretAsArrayList = new ArrayList<>(secret.length);
+        for (int i = 0; i < secret.length; ++i) {
+            secretAsArrayList.add(secret[i]);
+        }
+        mImpl.primaryUserCredential(secretAsArrayList);
+    }
+
+    @Override
+    public int getInterfaceVersion() throws RemoteException {
+        // Supports only V1
+        return 1;
+    }
+
+    @Override
+    public IBinder asBinder() {
+        throw new UnsupportedOperationException("AuthSecretHidlAdapter does not support asBinder");
+    }
+
+    @Override
+    public String getInterfaceHash() throws RemoteException {
+        throw new UnsupportedOperationException(
+                "AuthSecretHidlAdapter does not support getInterfaceHash");
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 2253a9a..89bc495a 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -267,8 +267,7 @@
     protected boolean mHasSecureLockScreen;
 
     protected IGateKeeperService mGateKeeperService;
-    protected IAuthSecret mAuthSecretServiceAidl;
-    protected android.hardware.authsecret.V1_0.IAuthSecret mAuthSecretServiceHidl;
+    protected IAuthSecret mAuthSecretService;
 
     private static final String GSI_RUNNING_PROP = "ro.gsid.image_running";
 
@@ -837,16 +836,19 @@
     }
 
     private void getAuthSecretHal() {
-        mAuthSecretServiceAidl = IAuthSecret.Stub.asInterface(ServiceManager.
-                                 waitForDeclaredService(IAuthSecret.DESCRIPTOR + "/default"));
-        if (mAuthSecretServiceAidl == null) {
-            Slog.i(TAG, "Device doesn't implement AuthSecret HAL(aidl), try to get hidl version");
-
+        mAuthSecretService =
+                IAuthSecret.Stub.asInterface(
+                        ServiceManager.waitForDeclaredService(IAuthSecret.DESCRIPTOR + "/default"));
+        if (mAuthSecretService != null) {
+            Slog.i(TAG, "Device implements AIDL AuthSecret HAL");
+        } else {
             try {
-                mAuthSecretServiceHidl =
-                    android.hardware.authsecret.V1_0.IAuthSecret.getService(/* retry */ true);
+                android.hardware.authsecret.V1_0.IAuthSecret authSecretServiceHidl =
+                        android.hardware.authsecret.V1_0.IAuthSecret.getService(/* retry */ true);
+                mAuthSecretService = new AuthSecretHidlAdapter(authSecretServiceHidl);
+                Slog.i(TAG, "Device implements HIDL AuthSecret HAL");
             } catch (NoSuchElementException e) {
-                Slog.i(TAG, "Device doesn't implement AuthSecret HAL(hidl)");
+                Slog.i(TAG, "Device doesn't implement AuthSecret HAL");
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to get AuthSecret HAL(hidl)", e);
             }
@@ -1243,7 +1245,8 @@
 
     private int getFrpCredentialType() {
         PersistentData data = mStorage.readPersistentDataBlock();
-        if (data.type != PersistentData.TYPE_SP && data.type != PersistentData.TYPE_SP_WEAVER) {
+        if (data.type != PersistentData.TYPE_SP_GATEKEEPER &&
+                data.type != PersistentData.TYPE_SP_WEAVER) {
             return CREDENTIAL_TYPE_NONE;
         }
         int credentialType = SyntheticPasswordManager.getFrpCredentialType(data.payload);
@@ -2577,25 +2580,16 @@
         // If the given user is the primary user, pass the auth secret to the HAL.  Only the system
         // user can be primary.  Check for the system user ID before calling getUserInfo(), as other
         // users may still be under construction.
+        if (mAuthSecretService == null) {
+            return;
+        }
         if (userId == UserHandle.USER_SYSTEM &&
                 mUserManager.getUserInfo(userId).isPrimary()) {
-            final byte[] rawSecret = sp.deriveVendorAuthSecret();
-            if (mAuthSecretServiceAidl != null) {
-                try {
-                    mAuthSecretServiceAidl.setPrimaryUserCredential(rawSecret);
-                } catch (RemoteException e) {
-                    Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL(aidl)", e);
-                }
-            } else if (mAuthSecretServiceHidl != null) {
-                try {
-                    final ArrayList<Byte> secret = new ArrayList<>(rawSecret.length);
-                    for (int i = 0; i < rawSecret.length; ++i) {
-                        secret.add(rawSecret[i]);
-                    }
-                    mAuthSecretServiceHidl.primaryUserCredential(secret);
-                } catch (RemoteException e) {
-                    Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL(hidl)", e);
-                }
+            final byte[] secret = sp.deriveVendorAuthSecret();
+            try {
+                mAuthSecretService.setPrimaryUserCredential(secret);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL", e);
             }
         }
     }
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index 473c4b6..2c28af1 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -587,7 +587,7 @@
         static final int VERSION_1_HEADER_SIZE = 1 + 1 + 4 + 4;
 
         public static final int TYPE_NONE = 0;
-        public static final int TYPE_SP = 1;
+        public static final int TYPE_SP_GATEKEEPER = 1;
         public static final int TYPE_SP_WEAVER = 2;
 
         public static final PersistentData NONE = new PersistentData(TYPE_NONE,
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index 67d1c71..ad2fa22 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -900,7 +900,7 @@
             protectorSecret = transformUnderSecdiscardable(stretchedLskf,
                     createSecdiscardable(protectorId, userId));
             // No need to pass in quality since the credential type already encodes sufficient info
-            synchronizeFrpPassword(pwd, 0, userId);
+            synchronizeGatekeeperFrpPassword(pwd, 0, userId);
         }
         if (!credential.isNone()) {
             saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
@@ -916,7 +916,7 @@
             LockscreenCredential userCredential,
             ICheckCredentialProgressCallback progressCallback) {
         PersistentData persistentData = mStorage.readPersistentDataBlock();
-        if (persistentData.type == PersistentData.TYPE_SP) {
+        if (persistentData.type == PersistentData.TYPE_SP_GATEKEEPER) {
             PasswordData pwd = PasswordData.fromBytes(persistentData.payload);
             byte[] stretchedLskf = stretchLskf(userCredential, pwd);
 
@@ -941,7 +941,7 @@
 
             return weaverVerify(weaverSlot, stretchedLskfToWeaverKey(stretchedLskf)).stripPayload();
         } else {
-            Slog.e(TAG, "persistentData.type must be TYPE_SP or TYPE_SP_WEAVER, but is "
+            Slog.e(TAG, "persistentData.type must be TYPE_SP_GATEKEEPER or TYPE_SP_WEAVER, but is "
                     + persistentData.type);
             return VerifyCredentialResponse.ERROR;
         }
@@ -960,7 +960,7 @@
             if (weaverSlot != INVALID_WEAVER_SLOT) {
                 synchronizeWeaverFrpPassword(pwd, requestedQuality, userInfo.id, weaverSlot);
             } else {
-                synchronizeFrpPassword(pwd, requestedQuality, userInfo.id);
+                synchronizeGatekeeperFrpPassword(pwd, requestedQuality, userInfo.id);
             }
         }
     }
@@ -994,13 +994,13 @@
         return true;
     }
 
-    private void synchronizeFrpPassword(@Nullable PasswordData pwd, int requestedQuality,
+    private void synchronizeGatekeeperFrpPassword(@Nullable PasswordData pwd, int requestedQuality,
             int userId) {
         if (shouldSynchronizeFrpCredential(pwd, userId)) {
             Slogf.d(TAG, "Syncing Gatekeeper-based FRP credential tied to user %d", userId);
             if (!isNoneCredential(pwd)) {
-                mStorage.writePersistentDataBlock(PersistentData.TYPE_SP, userId, requestedQuality,
-                        pwd.toBytes());
+                mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_GATEKEEPER, userId,
+                        requestedQuality, pwd.toBytes());
             } else {
                 mStorage.writePersistentDataBlock(PersistentData.TYPE_NONE, userId, 0, null);
             }
@@ -1224,7 +1224,7 @@
                             pwd.credentialType = credential.getType();
                             saveState(PASSWORD_DATA_NAME, pwd.toBytes(), protectorId, userId);
                             syncState(userId);
-                            synchronizeFrpPassword(pwd, 0, userId);
+                            synchronizeGatekeeperFrpPassword(pwd, 0, userId);
                         } else {
                             Slog.w(TAG, "Fail to re-enroll user password for user " + userId);
                             // continue the flow anyway
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 4fddc9c..0362ddd 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1470,15 +1470,9 @@
         }
 
         // Then make sure none of the activities have more than the max number of shortcuts.
-        int total = 0;
         for (int i = counts.size() - 1; i >= 0; i--) {
-            int count = counts.valueAt(i);
-            service.enforceMaxActivityShortcuts(count);
-            total += count;
+            service.enforceMaxActivityShortcuts(counts.valueAt(i));
         }
-
-        // Finally make sure that the app doesn't have more than the max number of shortcuts.
-        service.enforceMaxAppShortcuts(total);
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 12a33ee..83720f1 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -181,9 +181,6 @@
     static final int DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY = 15;
 
     @VisibleForTesting
-    static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 60;
-
-    @VisibleForTesting
     static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
 
     @VisibleForTesting
@@ -260,11 +257,6 @@
         String KEY_MAX_SHORTCUTS = "max_shortcuts";
 
         /**
-         * Key name for the max dynamic shortcuts per app. (int)
-         */
-        String KEY_MAX_SHORTCUTS_PER_APP = "max_shortcuts_per_app";
-
-        /**
          * Key name for icon compression quality, 0-100.
          */
         String KEY_ICON_QUALITY = "icon_quality";
@@ -337,14 +329,9 @@
             new SparseArray<>();
 
     /**
-     * Max number of dynamic + manifest shortcuts that each activity can have at a time.
-     */
-    private int mMaxShortcutsPerActivity;
-
-    /**
      * Max number of dynamic + manifest shortcuts that each application can have at a time.
      */
-    private int mMaxShortcutsPerApp;
+    private int mMaxShortcuts;
 
     /**
      * Max number of updating API calls that each application can make during the interval.
@@ -817,12 +804,9 @@
         mMaxUpdatesPerInterval = Math.max(0, (int) parser.getLong(
                 ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL, DEFAULT_MAX_UPDATES_PER_INTERVAL));
 
-        mMaxShortcutsPerActivity = Math.max(0, (int) parser.getLong(
+        mMaxShortcuts = Math.max(0, (int) parser.getLong(
                 ConfigConstants.KEY_MAX_SHORTCUTS, DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY));
 
-        mMaxShortcutsPerApp = Math.max(0, (int) parser.getLong(
-                ConfigConstants.KEY_MAX_SHORTCUTS_PER_APP, DEFAULT_MAX_SHORTCUTS_PER_APP));
-
         final int iconDimensionDp = Math.max(1, injectIsLowRamDevice()
                 ? (int) parser.getLong(
                 ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
@@ -1762,33 +1746,16 @@
      *                                  {@link #getMaxActivityShortcuts()}.
      */
     void enforceMaxActivityShortcuts(int numShortcuts) {
-        if (numShortcuts > mMaxShortcutsPerActivity) {
+        if (numShortcuts > mMaxShortcuts) {
             throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
         }
     }
 
     /**
-     * @throws IllegalArgumentException if {@code numShortcuts} is bigger than
-     *                                  {@link #getMaxAppShortcuts()}.
-     */
-    void enforceMaxAppShortcuts(int numShortcuts) {
-        if (numShortcuts > mMaxShortcutsPerApp) {
-            throw new IllegalArgumentException("Max number of dynamic shortcuts per app exceeded");
-        }
-    }
-
-    /**
      * Return the max number of dynamic + manifest shortcuts for each launcher icon.
      */
     int getMaxActivityShortcuts() {
-        return mMaxShortcutsPerActivity;
-    }
-
-    /**
-     * Return the max number of dynamic + manifest shortcuts for each launcher icon.
-     */
-    int getMaxAppShortcuts() {
-        return mMaxShortcutsPerApp;
+        return mMaxShortcuts;
     }
 
     /**
@@ -2221,8 +2188,6 @@
             ps.ensureNotImmutable(shortcut.getId(), /*ignoreInvisible=*/ true);
             fillInDefaultActivity(Arrays.asList(shortcut));
 
-            enforceMaxAppShortcuts(ps.getShortcutCount());
-
             if (!shortcut.hasRank()) {
                 shortcut.setRank(0);
             }
@@ -2610,7 +2575,7 @@
             throws RemoteException {
         verifyCaller(packageName, userId);
 
-        return mMaxShortcutsPerActivity;
+        return mMaxShortcuts;
     }
 
     @Override
@@ -4759,7 +4724,7 @@
                 pw.print("    maxUpdatesPerInterval: ");
                 pw.println(mMaxUpdatesPerInterval);
                 pw.print("    maxShortcutsPerActivity: ");
-                pw.println(mMaxShortcutsPerActivity);
+                pw.println(mMaxShortcuts);
                 pw.println();
 
                 mStatLogger.dump(pw, "  ");
@@ -5246,7 +5211,7 @@
 
     @VisibleForTesting
     int getMaxShortcutsForTest() {
-        return mMaxShortcutsPerActivity;
+        return mMaxShortcuts;
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index f88adab..fe0fe29 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -617,7 +617,6 @@
     private final com.android.internal.policy.LogDecelerateInterpolator mLogDecelerateInterpolator
             = new LogDecelerateInterpolator(100, 0);
 
-    private boolean mPerDisplayFocusEnabled = false;
     private volatile int mTopFocusedDisplayId = INVALID_DISPLAY;
 
     private int mPowerButtonSuppressionDelayMillis = POWER_BUTTON_SUPPRESSION_DELAY_DEFAULT_MILLIS;
@@ -2118,9 +2117,6 @@
         mHandleVolumeKeysInWM = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_handleVolumeKeysInWindowManager);
 
-        mPerDisplayFocusEnabled = mContext.getResources().getBoolean(
-                com.android.internal.R.bool.config_perDisplayFocusEnabled);
-
         mWakeUpToLastStateTimeout = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_wakeUpToLastStateTimeoutMillis);
 
@@ -4314,23 +4310,20 @@
             wakeUpFromWakeKey(event);
         }
 
-        if ((result & ACTION_PASS_TO_USER) != 0 && !mPerDisplayFocusEnabled
-                && displayId != INVALID_DISPLAY && displayId != mTopFocusedDisplayId) {
-            // If the key event is targeted to a specific display, then the user is interacting with
-            // that display. Therefore, give focus to the display that the user is interacting with,
-            // unless that display maintains its own focus.
-            Display display = mDisplayManager.getDisplay(displayId);
-            if ((display.getFlags() & Display.FLAG_OWN_FOCUS) == 0) {
-                // An event is targeting a non-focused display. Move the display to top so that
-                // it can become the focused display to interact with the user.
-                // This should be done asynchronously, once the focus logic is fully moved to input
-                // from windowmanager. Currently, we need to ensure the setInputWindows completes,
-                // which would force the focus event to be queued before the current key event.
-                // TODO(b/70668286): post call to 'moveDisplayToTop' to mHandler instead
-                Log.i(TAG, "Moving non-focused display " + displayId + " to top "
-                        + "because a key is targeting it");
-                mWindowManagerFuncs.moveDisplayToTop(displayId);
-            }
+        // If the key event is targeted to a specific display, then the user is interacting with
+        // that display. Therefore, try to give focus to the display that the user is interacting
+        // with.
+        if ((result & ACTION_PASS_TO_USER) != 0 && displayId != INVALID_DISPLAY
+                && displayId != mTopFocusedDisplayId) {
+            // An event is targeting a non-focused display. Move the display to top so that
+            // it can become the focused display to interact with the user.
+            // This should be done asynchronously, once the focus logic is fully moved to input
+            // from windowmanager. Currently, we need to ensure the setInputWindows completes,
+            // which would force the focus event to be queued before the current key event.
+            // TODO(b/70668286): post call to 'moveDisplayToTop' to mHandler instead
+            Log.i(TAG, "Moving non-focused display " + displayId + " to top "
+                    + "because a key is targeting it");
+            mWindowManagerFuncs.moveDisplayToTopIfAllowed(displayId);
         }
 
         return result;
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 88ec691..94fb840 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -330,8 +330,11 @@
         /**
          * Hint to window manager that the user is interacting with a display that should be treated
          * as the top display.
+         *
+         * Calling this method does not guarantee that the display will be moved to top. The window
+         * manager will make the final decision whether or not to move the display.
          */
-        void moveDisplayToTop(int displayId);
+        void moveDisplayToTopIfAllowed(int displayId);
 
         /**
          * Return whether the app transition state is idle.
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 4fef2a8..e2ab216 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -141,7 +141,7 @@
     private final SparseArray<WindowsForAccessibilityObserver> mWindowsForAccessibilityObserver =
             new SparseArray<>();
     private SparseArray<IBinder> mFocusedWindow = new SparseArray<>();
-    private int mFocusedDisplay = -1;
+    private int mFocusedDisplay = Display.INVALID_DISPLAY;
     private final SparseBooleanArray mIsImeVisibleArray = new SparseBooleanArray();
     // Set to true if initializing window population complete.
     private boolean mAllObserversInitialized = true;
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index eb3fdca..ff50fd4 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1663,6 +1663,12 @@
             pmInternal.grantImplicitAccess(mStartActivity.mUserId, mIntent,
                     UserHandle.getAppId(mStartActivity.info.applicationInfo.uid) /*recipient*/,
                     resultToUid /*visible*/, true /*direct*/);
+        } else if (mStartActivity.mShareIdentity) {
+            final PackageManagerInternal pmInternal =
+                    mService.getPackageManagerInternalLocked();
+            pmInternal.grantImplicitAccess(mStartActivity.mUserId, mIntent,
+                    UserHandle.getAppId(mStartActivity.info.applicationInfo.uid) /*recipient*/,
+                    r.launchedFromUid /*visible*/, true /*direct*/);
         }
         final Task startedTask = mStartActivity.getTask();
         if (newTask) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index d324df0..044357b 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -790,6 +790,12 @@
         }
     };
 
+    /**
+     * A lambda function to find the focused window of the given window.
+     *
+     * <p>The lambda returns true if a focused window was found, false otherwise. If a focused
+     * window is found it will be stored in <code>mTmpWindow</code>.
+     */
     private final ToBooleanFunction<WindowState> mFindFocusedWindow = w -> {
         final ActivityRecord focusedApp = mFocusedApp;
         ProtoLog.v(WM_DEBUG_FOCUS, "Looking for focus: %s, flags=%d, canReceive=%b, reason=%s",
@@ -3719,6 +3725,14 @@
     }
 
     /**
+     * @see Display#FLAG_STEAL_TOP_FOCUS_DISABLED
+     * @return True if this display can become the top focused display, false otherwise.
+     */
+    boolean canStealTopFocus() {
+        return (mDisplayInfo.flags & Display.FLAG_STEAL_TOP_FOCUS_DISABLED) == 0;
+    }
+
+    /**
      * Looking for the focused window on this display if the top focused display hasn't been
      * found yet (topFocusedDisplayId is INVALID_DISPLAY) or per-display focused was allowed.
      *
@@ -3730,9 +3744,15 @@
                     ? findFocusedWindow() : null;
     }
 
+    /**
+     * Find the focused window of this DisplayContent. The search takes the state of the display
+     * content into account
+     * @return The focused window, null if none was found.
+     */
     WindowState findFocusedWindow() {
         mTmpWindow = null;
 
+        // mFindFocusedWindow will populate mTmpWindow with the new focused window when found.
         forAllWindows(mFindFocusedWindow, true /* traverseTopToBottom */);
 
         if (mTmpWindow == null) {
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index b735b30..6d47eeb 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -291,7 +291,7 @@
 
         boolean dontMoveToTop = settings.mDontMoveToTop != null
                 ? settings.mDontMoveToTop : false;
-        dc.mDontMoveToTop = dontMoveToTop;
+        dc.mDontMoveToTop = !dc.canStealTopFocus() || dontMoveToTop;
 
         if (includeRotationSettings) applyRotationSettingsToDisplayLocked(dc);
     }
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 13da96f..fd5c5eb 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -442,10 +442,14 @@
         mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirerImpl(DISPLAY_OFF_SLEEP_TOKEN_TAG);
     }
 
+    /**
+     * Updates the children's focused window and the top focused display if needed.
+     */
     boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) {
         mTopFocusedAppByProcess.clear();
         boolean changed = false;
         int topFocusedDisplayId = INVALID_DISPLAY;
+        // Go through the children in z-order starting at the top-most
         for (int i = mChildren.size() - 1; i >= 0; --i) {
             final DisplayContent dc = mChildren.get(i);
             changed |= dc.updateFocusedWindowLocked(mode, updateInputWindows, topFocusedDisplayId);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 70bedc7..b83f423 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -563,7 +563,7 @@
     /** Mapping from an InputWindowHandle token to the server's Window object. */
     final HashMap<IBinder, WindowState> mInputToWindowMap = new HashMap<>();
 
-    /** Global service lock used by the package the owns this service. */
+    /** Global service lock used by the package that owns this service. */
     final WindowManagerGlobalLock mGlobalLock;
 
     /**
@@ -3165,15 +3165,41 @@
     }
 
     @Override
-    public void moveDisplayToTop(int displayId) {
+    public void moveDisplayToTopIfAllowed(int displayId) {
+        moveDisplayToTopInternal(displayId);
+        syncInputTransactions(true /* waitForAnimations */);
+    }
+
+    /**
+     * Moves the given display to the top. If it cannot be moved to the top this method does
+     * nothing.
+     */
+    void moveDisplayToTopInternal(int displayId) {
         synchronized (mGlobalLock) {
             final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
             if (displayContent != null && mRoot.getTopChild() != displayContent) {
+                // Check whether anything prevents us from moving the display to the top.
+                if (!displayContent.canStealTopFocus()) {
+                    ProtoLog.i(WM_DEBUG_FOCUS_LIGHT,
+                            "Not moving display (displayId=%d) to top. Top focused displayId=%d. "
+                                    + "Reason: FLAG_STEAL_TOP_FOCUS_DISABLED",
+                            displayId, mRoot.getTopFocusedDisplayContent().getDisplayId());
+                    return;
+                }
+
+                if (mPerDisplayFocusEnabled) {
+                    ProtoLog.i(WM_DEBUG_FOCUS_LIGHT,
+                            "Not moving display (displayId=%d) to top. Top focused displayId=%d. "
+                                    + "Reason: config_perDisplayFocusEnabled", displayId,
+                            mRoot.getTopFocusedDisplayContent().getDisplayId());
+                    return;
+                }
+
+                // Nothing prevented us from moving the display to the top. Let's do it!
                 displayContent.getParent().positionChildAt(WindowContainer.POSITION_TOP,
                         displayContent, true /* includingParents */);
             }
         }
-        syncInputTransactions(true /* waitForAnimations */);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 746f983..ae31ee8 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -6238,10 +6238,7 @@
     @Override
     public void handleTapOutsideFocusInsideSelf() {
         final DisplayContent displayContent = getDisplayContent();
-        if (!displayContent.isOnTop()) {
-            displayContent.getParent().positionChildAt(WindowContainer.POSITION_TOP, displayContent,
-                    true /* includingParents */);
-        }
+        mWmService.moveDisplayToTopInternal(getDisplayId());
         mWmService.handleTaskFocusChange(getTask(), mActivityRecord);
     }
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 7100020..8c2065e 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -19572,6 +19572,11 @@
                 }
                 admin.mtePolicy = flags;
                 saveSettingsLocked(caller.getUserId());
+
+                DevicePolicyEventLogger.createEvent(DevicePolicyEnums.SET_MTE_POLICY)
+                        .setInt(flags)
+                        .setAdmin(admin.info.getPackageName())
+                        .write();
             }
         }
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
index 0dfad43..79fbc87 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java
@@ -839,7 +839,7 @@
     private static JobStatus createJob(int uid, int jobId, @Nullable String sourcePackageName) {
         return JobStatus.createFromJobInfo(
                 new JobInfo.Builder(jobId, new ComponentName("foo", "bar")).build(), uid,
-                sourcePackageName, UserHandle.getUserId(uid), "JobConcurrencyManagerTest");
+                sourcePackageName, UserHandle.getUserId(uid), "JobConcurrencyManagerTest", null);
     }
 
     private static final class TypeConfig {
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index 6374c66..c4ff4f0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -197,7 +197,7 @@
     private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder,
             int callingUid) {
         return JobStatus.createFromJobInfo(
-                jobInfoBuilder.build(), callingUid, "com.android.test", 0, testTag);
+                jobInfoBuilder.build(), callingUid, "com.android.test", 0, "JSSTest", testTag);
     }
 
     private void grantRunLongJobsPermission(boolean grant) {
@@ -1111,7 +1111,7 @@
                     i < 300 ? JobScheduler.RESULT_SUCCESS : JobScheduler.RESULT_FAILURE;
             assertEquals("Got unexpected result for schedule #" + (i + 1),
                     expected,
-                    mService.scheduleAsPackage(job, null, 10123, null, 0, ""));
+                    mService.scheduleAsPackage(job, null, 10123, null, 0, "JSSTest", ""));
         }
     }
 
@@ -1132,7 +1132,7 @@
         for (int i = 0; i < 500; ++i) {
             assertEquals("Got unexpected result for schedule #" + (i + 1),
                     JobScheduler.RESULT_SUCCESS,
-                    mService.scheduleAsPackage(job, null, 10123, null, 0, ""));
+                    mService.scheduleAsPackage(job, null, 10123, null, 0, "JSSTest", ""));
         }
     }
 
@@ -1153,7 +1153,8 @@
         for (int i = 0; i < 500; ++i) {
             assertEquals("Got unexpected result for schedule #" + (i + 1),
                     JobScheduler.RESULT_SUCCESS,
-                    mService.scheduleAsPackage(job, null, 10123, "proxied.package", 0, ""));
+                    mService.scheduleAsPackage(job, null, 10123, "proxied.package", 0, "JSSTest",
+                            ""));
         }
     }
 
@@ -1177,7 +1178,7 @@
             assertEquals("Got unexpected result for schedule #" + (i + 1),
                     expected,
                     mService.scheduleAsPackage(job, null, 10123, job.getService().getPackageName(),
-                            0, ""));
+                            0, "JSSTest", ""));
         }
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
index 7c435be..f334a6a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BatteryControllerTest.java
@@ -194,7 +194,7 @@
     private JobStatus createJobStatus(String testTag, String packageName, int callingUid,
             JobInfo jobInfo) {
         JobStatus js = JobStatus.createFromJobInfo(
-                jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag);
+                jobInfo, callingUid, packageName, SOURCE_USER_ID, "BCTest", testTag);
         js.serviceProcessName = "testProcess";
         // Make sure tests aren't passing just because the default bucket is likely ACTIVE.
         js.setStandbyBucket(FREQUENT_INDEX);
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
index 42e22f3..32e5c83 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
@@ -1316,7 +1316,7 @@
 
     private static JobStatus createJobStatus(JobInfo.Builder job, int uid,
             long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis) {
-        return new JobStatus(job.build(), uid, null, -1, 0, null,
+        return new JobStatus(job.build(), uid, null, -1, 0, null, null,
                 earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, 0, 0, null, 0, 0);
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 398acb8..1e65b38 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -195,7 +195,7 @@
     private JobStatus createJobStatus(String testTag, JobInfo.Builder job) {
         JobInfo jobInfo = job.build();
         JobStatus js = JobStatus.createFromJobInfo(
-                jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+                jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, "FCTest", testTag);
         js.enqueueTime = FROZEN_TIME;
         return js;
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
index 7f522b0..d2ee9ff 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/JobStatusTest.java
@@ -920,12 +920,12 @@
             long latestRunTimeElapsedMillis) {
         final JobInfo job = new JobInfo.Builder(101, new ComponentName("foo", "bar"))
                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY).build();
-        return new JobStatus(job, 0, null, -1, 0, null, earliestRunTimeElapsedMillis,
+        return new JobStatus(job, 0, null, -1, 0, null, null, earliestRunTimeElapsedMillis,
                 latestRunTimeElapsedMillis, 0, 0, null, 0, 0);
     }
 
     private static JobStatus createJobStatus(JobInfo job) {
-        JobStatus jobStatus = JobStatus.createFromJobInfo(job, 0, null, -1, "JobStatusTest");
+        JobStatus jobStatus = JobStatus.createFromJobInfo(job, 0, null, -1, "JobStatusTest", null);
         jobStatus.serviceProcessName = "testProcess";
         return jobStatus;
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
index b949b3b..fb59ea2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/PrefetchControllerTest.java
@@ -184,7 +184,7 @@
     private static JobStatus createJobStatus(String testTag, String packageName, int callingUid,
             JobInfo jobInfo) {
         JobStatus js = JobStatus.createFromJobInfo(
-                jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag);
+                jobInfo, callingUid, packageName, SOURCE_USER_ID, "PCTest", testTag);
         js.serviceProcessName = "testProcess";
         js.setStandbyBucket(FREQUENT_INDEX);
         // Make sure Doze and background-not-restricted don't affect tests.
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 9aef674..6f713e0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -383,7 +383,7 @@
     private JobStatus createJobStatus(String testTag, String packageName, int callingUid,
             JobInfo jobInfo) {
         JobStatus js = JobStatus.createFromJobInfo(
-                jobInfo, callingUid, packageName, SOURCE_USER_ID, testTag);
+                jobInfo, callingUid, packageName, SOURCE_USER_ID, "QCTest", testTag);
         js.serviceProcessName = "testProcess";
         // Make sure tests aren't passing just because the default bucket is likely ACTIVE.
         js.setStandbyBucket(FREQUENT_INDEX);
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java
index 51d641b..b111757 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/StateControllerTest.java
@@ -137,7 +137,7 @@
                 .setMinimumLatency(Math.abs(jobId) + 1)
                 .build();
         return JobStatus.createFromJobInfo(
-                jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+                jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, "SCTest", testTag);
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java
index d64c1b3..27efcfa 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/TimeControllerTest.java
@@ -137,7 +137,7 @@
     private JobStatus createJobStatus(String testTag, JobInfo.Builder job) {
         JobInfo jobInfo = job.build();
         return JobStatus.createFromJobInfo(
-                jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+                jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, "TCTest", testTag);
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
index 9067285..02fdfad 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
@@ -328,6 +328,6 @@
 
     private JobStatus createJobStatus(String testTag, JobInfo jobInfo) {
         return JobStatus.createFromJobInfo(
-                jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+                jobInfo, CALLING_UID, SOURCE_PACKAGE, SOURCE_USER_ID, "TSRTest", testTag);
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
index 58cff94..579621c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java
@@ -15,19 +15,6 @@
  */
 package com.android.server.pm;
 
-import static android.os.UserHandle.USER_NULL;
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.Display.INVALID_DISPLAY;
-
-import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
-import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
-import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
-import static com.android.server.pm.UserVisibilityChangedEvent.onInvisible;
-import static com.android.server.pm.UserVisibilityChangedEvent.onVisible;
-import static com.android.server.pm.UserVisibilityMediator.INITIAL_CURRENT_USER_ID;
-
-import org.junit.Test;
-
 /**
  * Tests for {@link UserVisibilityMediator} tests for devices that support concurrent Multiple
  * Users on Multiple Displays (A.K.A {@code MUMD}).
@@ -35,208 +22,10 @@
  * <p>Run as
  * {@code atest FrameworksMockingServicesTests:com.android.server.pm.UserVisibilityMediatorMUMDTest}
  */
-public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediatorTestCase {
+public final class UserVisibilityMediatorMUMDTest
+        extends UserVisibilityMediatorVisibleBackgroundUserTestCase {
 
     public UserVisibilityMediatorMUMDTest() throws Exception {
         super(/* usersOnSecondaryDisplaysEnabled= */ true);
     }
-
-    @Test
-    public void testStartFgUser_onDefaultDisplay() throws Exception {
-        AsyncUserVisibilityListener listener = addListenerForEvents(
-                onInvisible(INITIAL_CURRENT_USER_ID),
-                onVisible(USER_ID));
-
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG,
-                DEFAULT_DISPLAY);
-        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
-        expectUserIsVisible(USER_ID);
-        expectUserIsVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
-        expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
-        expectUserIsNotVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-        expectVisibleUsers(USER_ID);
-
-        expectDisplayAssignedToUser(USER_ID, DEFAULT_DISPLAY);
-        expectUserAssignedToDisplay(DEFAULT_DISPLAY, USER_ID);
-        expectUserAssignedToDisplay(INVALID_DISPLAY, USER_ID);
-        expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
-
-        expectDisplayAssignedToUser(USER_NULL, INVALID_DISPLAY);
-
-        listener.verify();
-    }
-
-    @Test
-    public void testSwitchFgUser_onDefaultDisplay() throws Exception {
-        int previousCurrentUserId = OTHER_USER_ID;
-        int currentUserId = USER_ID;
-        AsyncUserVisibilityListener listener = addListenerForEvents(
-                onInvisible(INITIAL_CURRENT_USER_ID),
-                onVisible(previousCurrentUserId),
-                onInvisible(previousCurrentUserId),
-                onVisible(currentUserId));
-        startForegroundUser(previousCurrentUserId);
-
-        int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId, FG,
-                DEFAULT_DISPLAY);
-        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
-        expectUserIsVisible(currentUserId);
-        expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY);
-        expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY);
-        expectUserIsNotVisibleOnDisplay(currentUserId, SECONDARY_DISPLAY_ID);
-        expectVisibleUsers(currentUserId);
-
-        expectDisplayAssignedToUser(currentUserId, DEFAULT_DISPLAY);
-        expectUserAssignedToDisplay(DEFAULT_DISPLAY, currentUserId);
-        expectUserAssignedToDisplay(INVALID_DISPLAY, currentUserId);
-        expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, currentUserId);
-
-        expectUserIsNotVisibleAtAll(previousCurrentUserId);
-        expectNoDisplayAssignedToUser(previousCurrentUserId);
-
-        listener.verify();
-    }
-
-    @Test
-    public void testStartBgProfile_onDefaultDisplay_whenParentIsCurrentUser() throws Exception {
-        AsyncUserVisibilityListener listener = addListenerForEvents(
-                onInvisible(INITIAL_CURRENT_USER_ID),
-                onVisible(PARENT_USER_ID),
-                onVisible(PROFILE_USER_ID));
-        startForegroundUser(PARENT_USER_ID);
-
-        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
-                BG_VISIBLE, DEFAULT_DISPLAY);
-        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
-        expectUserIsVisible(PROFILE_USER_ID);
-        expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY);
-        expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
-        expectUserIsVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY);
-        expectVisibleUsers(PARENT_USER_ID, PROFILE_USER_ID);
-
-        expectDisplayAssignedToUser(PROFILE_USER_ID, DEFAULT_DISPLAY);
-        expectUserAssignedToDisplay(DEFAULT_DISPLAY, PARENT_USER_ID);
-
-        listener.verify();
-    }
-
-    @Test
-    public void testStartFgUser_onInvalidDisplay() throws Exception {
-        AsyncUserVisibilityListener listener = addListenerForNoEvents();
-
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG, INVALID_DISPLAY);
-
-        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
-
-        listener.verify();
-    }
-
-    @Test
-    public void testStartBgUser_onInvalidDisplay() throws Exception {
-        AsyncUserVisibilityListener listener = addListenerForNoEvents();
-
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
-                INVALID_DISPLAY);
-
-        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
-
-        expectUserIsNotVisibleAtAll(USER_ID);
-
-        listener.verify();
-    }
-
-    @Test
-    public void testStartBgUser_onSecondaryDisplay_displayAvailable() throws Exception {
-        AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID));
-
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
-                SECONDARY_DISPLAY_ID);
-        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
-        expectUserIsVisible(USER_ID);
-        expectUserIsVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-        expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
-        expectUserIsNotVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
-        expectVisibleUsers(INITIAL_CURRENT_USER_ID, USER_ID);
-
-        expectDisplayAssignedToUser(USER_ID, SECONDARY_DISPLAY_ID);
-        expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
-
-        listener.verify();
-    }
-
-    @Test
-    public void testVisibilityOfCurrentUserAndProfilesOnDisplayAssignedToAnotherUser()
-            throws Exception {
-        startDefaultProfile();
-
-        // Make sure they were visible before
-        expectUserIsNotVisibleOnDisplay("before", PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-        expectUserIsNotVisibleOnDisplay("before", PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
-
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
-                SECONDARY_DISPLAY_ID);
-        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
-
-        expectUserIsNotVisibleOnDisplay("after", PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-        expectUserIsNotVisibleOnDisplay("after", PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
-    }
-
-    @Test
-    public void testStartBgUser_onSecondaryDisplay_displayAlreadyAssigned() throws Exception {
-        AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(OTHER_USER_ID));
-        startUserInSecondaryDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
-
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
-                SECONDARY_DISPLAY_ID);
-        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
-
-        expectUserIsNotVisibleAtAll(USER_ID);
-        expectNoDisplayAssignedToUser(USER_ID);
-        expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, OTHER_USER_ID);
-
-        listener.verify();
-    }
-
-    @Test
-    public void testStartBgUser_onSecondaryDisplay_userAlreadyAssigned() throws Exception {
-        AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID));
-        startUserInSecondaryDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
-
-        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
-                SECONDARY_DISPLAY_ID);
-        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
-
-        expectUserIsVisible(USER_ID);
-        expectUserIsVisibleOnDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
-        expectUserIsNotVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
-        expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
-        expectUserIsNotVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
-        expectVisibleUsers(INITIAL_CURRENT_USER_ID, USER_ID);
-
-        expectDisplayAssignedToUser(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
-        expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, USER_ID);
-
-        listener.verify();
-    }
-
-    @Test
-    public void testStartBgProfile_onDefaultDisplay_whenParentVisibleOnSecondaryDisplay()
-            throws Exception {
-        AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(PARENT_USER_ID));
-        startUserInSecondaryDisplay(PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID);
-
-        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
-                BG_VISIBLE, DEFAULT_DISPLAY);
-        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
-
-        expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
-        expectNoDisplayAssignedToUser(PROFILE_USER_ID);
-        expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, PARENT_USER_ID);
-
-        listener.verify();
-    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
index 3d64c29..88709e1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java
@@ -61,7 +61,7 @@
         expectUserAssignedToDisplay(INVALID_DISPLAY, USER_ID);
         expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
 
-        expectDisplayAssignedToUser(USER_NULL, INVALID_DISPLAY);
+        expectNoDisplayAssignedToUser(USER_NULL);
 
         listener.verify();
     }
@@ -99,7 +99,8 @@
     }
 
     @Test
-    public void testStartBgProfile_onDefaultDisplay_whenParentIsCurrentUser() throws Exception {
+    public void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsCurrentUser()
+            throws Exception {
         AsyncUserVisibilityListener listener = addListenerForEvents(
                 onInvisible(INITIAL_CURRENT_USER_ID),
                 onVisible(PARENT_USER_ID),
@@ -123,7 +124,7 @@
     }
 
     @Test
-    public void testStartBgUser_onSecondaryDisplay() throws Exception {
+    public void testStartVisibleBgUser_onSecondaryDisplay() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
 
         int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
@@ -133,7 +134,7 @@
         expectUserIsNotVisibleAtAll(USER_ID);
         expectNoDisplayAssignedToUser(USER_ID);
 
-        expectNoUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
+        expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
 
         listener.verify();
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
index 0ac0f57..e4664d2 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java
@@ -149,7 +149,7 @@
 
         expectUserIsNotVisibleAtAll(USER_ID);
         expectNoDisplayAssignedToUser(USER_ID);
-        expectNoUserAssignedToDisplay(DEFAULT_DISPLAY);
+        expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY);
 
         listener.verify();
     }
@@ -163,13 +163,17 @@
 
         expectUserIsNotVisibleAtAll(USER_ID);
         expectNoDisplayAssignedToUser(USER_ID);
-        expectNoUserAssignedToDisplay(DEFAULT_DISPLAY);
+        expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY);
 
         listener.verify();
     }
 
     @Test
-    public final void testStartBgUser_onDefaultDisplay_visible() throws Exception {
+    public final void testStartVisibleBgUser_onDefaultDisplay() throws Exception {
+        visibleBgUserCannotBeStartedOnDefaultDisplayTest();
+    }
+
+    protected final void visibleBgUserCannotBeStartedOnDefaultDisplayTest() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
 
         int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
@@ -183,7 +187,7 @@
     }
 
     @Test
-    public final void testStartBgUser_onSecondaryDisplay_invisible() throws Exception {
+    public final void testStartBgUser_onSecondaryDisplay() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
 
         int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG,
@@ -217,7 +221,7 @@
     }
 
     @Test
-    public final void testStopVisibleProfile() throws Exception {
+    public final void testStopVisibleBgProfile() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForEvents(
                 onInvisible(INITIAL_CURRENT_USER_ID),
                 onVisible(PARENT_USER_ID),
@@ -235,7 +239,8 @@
     }
 
     @Test
-    public final void testVisibleProfileBecomesInvisibleWhenParentIsSwitchedOut() throws Exception {
+    public final void testVisibleBgProfileBecomesInvisibleWhenParentIsSwitchedOut()
+            throws Exception {
         AsyncUserVisibilityListener listener = addListenerForEvents(
                 onInvisible(INITIAL_CURRENT_USER_ID),
                 onVisible(PARENT_USER_ID),
@@ -255,7 +260,7 @@
     }
 
     @Test
-    public final void testStartBgProfile_onDefaultDisplay_whenParentIsNotStarted()
+    public final void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsNotStarted()
             throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
 
@@ -270,7 +275,7 @@
     }
 
     @Test
-    public final void testStartBgProfile_onDefaultDisplay_whenParentIsStartedOnBg()
+    public final void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsStartedOnBg()
             throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
         startBackgroundUser(PARENT_USER_ID);
@@ -282,14 +287,14 @@
         expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
 
         expectNoDisplayAssignedToUser(PROFILE_USER_ID);
-        expectNoUserAssignedToDisplay(DEFAULT_DISPLAY);
+        expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY);
 
         listener.verify();
     }
 
     // Not supported - profiles can only be started on default display
     @Test
-    public final void testStartBgProfile_onSecondaryDisplay() throws Exception {
+    public final void testStartVisibleBgProfile_onSecondaryDisplay() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
 
         int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
@@ -298,13 +303,13 @@
 
         expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
         expectNoDisplayAssignedToUser(PROFILE_USER_ID);
-        expectNoUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
+        expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
 
         listener.verify();
     }
 
     @Test
-    public final void testStartBgProfile_onSecondaryDisplay_invisible() throws Exception {
+    public final void testStartBgProfile_onSecondaryDisplay() throws Exception {
         AsyncUserVisibilityListener listener = addListenerForNoEvents();
 
         int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG,
@@ -313,7 +318,7 @@
 
         expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
         expectNoDisplayAssignedToUser(PROFILE_USER_ID);
-        expectNoUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
+        expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
 
         listener.verify();
     }
@@ -329,7 +334,7 @@
         expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
 
         expectNoDisplayAssignedToUser(PROFILE_USER_ID);
-        expectNoUserAssignedToDisplay(DEFAULT_DISPLAY);
+        expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY);
 
         listener.verify();
     }
@@ -344,7 +349,7 @@
 
         expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
         expectNoDisplayAssignedToUser(PROFILE_USER_ID);
-        expectNoUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
+        expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID);
 
         listener.verify();
     }
@@ -477,7 +482,7 @@
     }
 
     protected void expectUserIsVisible(@UserIdInt int userId) {
-        expectWithMessage("mediator.isUserVisible(%s)", userId)
+        expectWithMessage("isUserVisible(%s)", userId)
                 .that(mMediator.isUserVisible(userId))
                 .isTrue();
     }
@@ -490,13 +495,13 @@
     }
 
     protected void expectUserIsVisibleOnDisplay(@UserIdInt int userId, int displayId) {
-        expectWithMessage("mediator.isUserVisible(%s, %s)", userId, displayId)
+        expectWithMessage("isUserVisible(%s, %s)", userId, displayId)
                 .that(mMediator.isUserVisible(userId, displayId))
                 .isTrue();
     }
 
     protected void expectUserIsNotVisibleOnDisplay(@UserIdInt int userId, int displayId) {
-        expectWithMessage("mediator.isUserVisible(%s, %s)", userId, displayId)
+        expectWithMessage("isUserVisible(%s, %s)", userId, displayId)
                 .that(mMediator.isUserVisible(userId, displayId))
                 .isFalse();
     }
@@ -504,13 +509,13 @@
     protected void expectUserIsNotVisibleOnDisplay(String when, @UserIdInt int userId,
             int displayId) {
         String suffix = TextUtils.isEmpty(when) ? "" : " on " + when;
-        expectWithMessage("mediator.isUserVisible(%s, %s)%s", userId, displayId, suffix)
+        expectWithMessage("isUserVisible(%s, %s)%s", userId, displayId, suffix)
                 .that(mMediator.isUserVisible(userId, displayId))
                 .isFalse();
     }
 
     protected void expectUserIsNotVisibleAtAll(@UserIdInt int userId) {
-        expectWithMessage("mediator.isUserVisible(%s)", userId)
+        expectWithMessage("isUserVisible(%s)", userId)
                 .that(mMediator.isUserVisible(userId))
                 .isFalse();
         expectUserIsNotVisibleOnDisplay(userId, DEFAULT_DISPLAY);
@@ -534,7 +539,7 @@
                 .that(mMediator.getUserAssignedToDisplay(displayId)).isEqualTo(userId);
     }
 
-    protected void expectNoUserAssignedToDisplay(int displayId) {
+    protected void expectInitialCurrentUserAssignedToDisplay(int displayId) {
         expectWithMessage("getUserAssignedToDisplay(%s)", displayId)
                 .that(mMediator.getUserAssignedToDisplay(displayId))
                 .isEqualTo(INITIAL_CURRENT_USER_ID);
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java
new file mode 100644
index 0000000..66d7eb6
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.pm;
+
+import static android.os.UserHandle.USER_NULL;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE;
+import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE;
+import static com.android.server.pm.UserVisibilityChangedEvent.onInvisible;
+import static com.android.server.pm.UserVisibilityChangedEvent.onVisible;
+import static com.android.server.pm.UserVisibilityMediator.INITIAL_CURRENT_USER_ID;
+
+import android.annotation.UserIdInt;
+
+import org.junit.Test;
+
+/**
+ * Base class for {@link UserVisibilityMediator} test classe on devices that support starting
+ * background users on visible displays (as defined by
+ * {@link android.os.UserManagerInternal#isVisibleBackgroundUsersSupported}).
+ */
+abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase
+        extends UserVisibilityMediatorTestCase {
+
+    UserVisibilityMediatorVisibleBackgroundUserTestCase(boolean backgroundUsersOnDisplaysEnabled)
+            throws Exception {
+        super(backgroundUsersOnDisplaysEnabled);
+    }
+
+    @Test
+    public final void testStartFgUser_onDefaultDisplay() throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForEvents(
+                onInvisible(INITIAL_CURRENT_USER_ID),
+                onVisible(USER_ID));
+
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG,
+                DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+        expectUserIsVisible(USER_ID);
+        expectUserIsVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
+        expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
+        expectUserIsNotVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+        expectVisibleUsers(USER_ID);
+
+        expectDisplayAssignedToUser(USER_ID, DEFAULT_DISPLAY);
+        expectUserAssignedToDisplay(DEFAULT_DISPLAY, USER_ID);
+        expectUserAssignedToDisplay(INVALID_DISPLAY, USER_ID);
+        expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
+
+        expectDisplayAssignedToUser(USER_NULL, INVALID_DISPLAY);
+
+        listener.verify();
+    }
+
+    @Test
+    public final void testSwitchFgUser_onDefaultDisplay() throws Exception {
+        int previousCurrentUserId = OTHER_USER_ID;
+        int currentUserId = USER_ID;
+        AsyncUserVisibilityListener listener = addListenerForEvents(
+                onInvisible(INITIAL_CURRENT_USER_ID),
+                onVisible(previousCurrentUserId),
+                onInvisible(previousCurrentUserId),
+                onVisible(currentUserId));
+        startForegroundUser(previousCurrentUserId);
+
+        int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId, FG,
+                DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+        expectUserIsVisible(currentUserId);
+        expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY);
+        expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY);
+        expectUserIsNotVisibleOnDisplay(currentUserId, SECONDARY_DISPLAY_ID);
+        expectVisibleUsers(currentUserId);
+
+        expectDisplayAssignedToUser(currentUserId, DEFAULT_DISPLAY);
+        expectUserAssignedToDisplay(DEFAULT_DISPLAY, currentUserId);
+        expectUserAssignedToDisplay(INVALID_DISPLAY, currentUserId);
+        expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, currentUserId);
+
+        expectUserIsNotVisibleAtAll(previousCurrentUserId);
+        expectNoDisplayAssignedToUser(previousCurrentUserId);
+
+        listener.verify();
+    }
+
+    @Test
+    public final void testStartVisibleBgProfile_onDefaultDisplay_whenParentIsCurrentUser()
+            throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForEvents(
+                onInvisible(INITIAL_CURRENT_USER_ID),
+                onVisible(PARENT_USER_ID),
+                onVisible(PROFILE_USER_ID));
+        startForegroundUser(PARENT_USER_ID);
+
+        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+                BG_VISIBLE, DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+        expectUserIsVisible(PROFILE_USER_ID);
+        expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY);
+        expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+        expectUserIsVisibleOnDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY);
+        expectVisibleUsers(PARENT_USER_ID, PROFILE_USER_ID);
+
+        expectDisplayAssignedToUser(PROFILE_USER_ID, DEFAULT_DISPLAY);
+        expectUserAssignedToDisplay(DEFAULT_DISPLAY, PARENT_USER_ID);
+
+        listener.verify();
+    }
+
+    @Test
+    public final void testStartFgUser_onInvalidDisplay() throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForNoEvents();
+
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG, INVALID_DISPLAY);
+
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+        listener.verify();
+    }
+
+    @Test
+    public final void testStartVisibleBgUser_onInvalidDisplay() throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForNoEvents();
+
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
+                INVALID_DISPLAY);
+
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+        expectUserIsNotVisibleAtAll(USER_ID);
+
+        listener.verify();
+    }
+
+    @Test
+    public final void testStartVisibleBgUser_onSecondaryDisplay_displayAvailable()
+            throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID));
+
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
+                SECONDARY_DISPLAY_ID);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+        expectUserIsVisible(USER_ID);
+        expectUserIsVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+        expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
+        expectUserIsNotVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
+        expectVisibleUsers(INITIAL_CURRENT_USER_ID, USER_ID);
+
+        expectDisplayAssignedToUser(USER_ID, SECONDARY_DISPLAY_ID);
+        expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID);
+
+        listener.verify();
+    }
+
+    @Test
+    public final void testVisibilityOfCurrentUserAndProfilesOnDisplayAssignedToAnotherUser()
+            throws Exception {
+        startDefaultProfile();
+
+        // Make sure they were visible before
+        expectUserIsNotVisibleOnDisplay("before", PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        expectUserIsNotVisibleOnDisplay("before", PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
+                SECONDARY_DISPLAY_ID);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE);
+
+        expectUserIsNotVisibleOnDisplay("after", PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        expectUserIsNotVisibleOnDisplay("after", PROFILE_USER_ID, SECONDARY_DISPLAY_ID);
+    }
+
+    @Test
+    public final void testStartVisibleBgUser_onSecondaryDisplay_displayAlreadyAssigned()
+            throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(OTHER_USER_ID));
+        startUserInSecondaryDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID);
+
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
+                SECONDARY_DISPLAY_ID);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+        expectUserIsNotVisibleAtAll(USER_ID);
+        expectNoDisplayAssignedToUser(USER_ID);
+        expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, OTHER_USER_ID);
+
+        listener.verify();
+    }
+
+    @Test
+    public final void testStartVisibleBgUser_onSecondaryDisplay_userAlreadyAssigned()
+            throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID));
+        startUserInSecondaryDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
+
+        int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE,
+                SECONDARY_DISPLAY_ID);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE);
+
+        expectUserIsVisible(USER_ID);
+        expectUserIsVisibleOnDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
+        expectUserIsNotVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID);
+        expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY);
+        expectUserIsNotVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY);
+        expectVisibleUsers(INITIAL_CURRENT_USER_ID, USER_ID);
+
+        expectDisplayAssignedToUser(USER_ID, OTHER_SECONDARY_DISPLAY_ID);
+        expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, USER_ID);
+
+        listener.verify();
+    }
+
+    @Test
+    public final void
+            testStartVisibleBgProfile_onDefaultDisplay_whenParentVisibleOnSecondaryDisplay()
+                    throws Exception {
+        AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(PARENT_USER_ID));
+        startUserInSecondaryDisplay(PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID);
+
+        int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID,
+                BG_VISIBLE, DEFAULT_DISPLAY);
+        assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE);
+
+        expectUserIsNotVisibleAtAll(PROFILE_USER_ID);
+        expectNoDisplayAssignedToUser(PROFILE_USER_ID);
+        expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, PARENT_USER_ID);
+
+        listener.verify();
+    }
+
+    private void currentUserVisibilityWhenNoDisplayIsAssignedTest(@UserIdInt int currentUserId) {
+        // Conditions below are asserted on other tests, but they're explicitly checked in the 2
+        // tests below as well
+        expectUserIsVisible(currentUserId);
+        expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY);
+        expectUserIsNotVisibleOnDisplay(currentUserId, SECONDARY_DISPLAY_ID);
+        expectUserIsNotVisibleOnDisplay(currentUserId, OTHER_SECONDARY_DISPLAY_ID);
+        expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY);
+
+        expectDisplayAssignedToUser(currentUserId, DEFAULT_DISPLAY);
+        expectUserAssignedToDisplay(DEFAULT_DISPLAY, currentUserId);
+        expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, currentUserId);
+        expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, currentUserId);
+        expectUserAssignedToDisplay(INVALID_DISPLAY, currentUserId);
+    }
+
+    @Test
+    public final void testCurrentUserVisibilityWhenNoDisplayIsAssigned_onBoot() throws Exception {
+        currentUserVisibilityWhenNoDisplayIsAssignedTest(INITIAL_CURRENT_USER_ID);
+    }
+
+    @Test
+    public final void testCurrentUserVisibilityWhenNoDisplayIsAssigned_afterSwitch()
+            throws Exception {
+        startForegroundUser(USER_ID);
+
+        currentUserVisibilityWhenNoDisplayIsAssignedTest(USER_ID);
+        expectUserIsNotVisibleAtAll(INITIAL_CURRENT_USER_ID);
+        expectDisplayAssignedToUser(INITIAL_CURRENT_USER_ID, INVALID_DISPLAY);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 5246107..5dd29fd 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -39,6 +39,7 @@
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
+import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
 import android.media.AudioManager;
 import android.os.Looper;
@@ -96,6 +97,7 @@
     private int mTvPhysicalAddress;
     private int mTvLogicalAddress;
     private boolean mWokenUp;
+    private boolean mEarcBlocksArc;
     private List<DeviceEventListener> mDeviceEventListeners = new ArrayList<>();
 
     private class DeviceEventListener {
@@ -156,6 +158,11 @@
                     }
 
                     @Override
+                    boolean isPowerStandbyOrTransient() {
+                        return false;
+                    }
+
+                    @Override
                     AudioManager getAudioManager() {
                         return mAudioManager;
                     }
@@ -164,6 +171,11 @@
                     void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) {
                         mDeviceEventListeners.add(new DeviceEventListener(device, status));
                     }
+
+                    @Override
+                    protected boolean earcBlocksArcConnection() {
+                        return mEarcBlocksArc;
+                    }
                 };
 
         mHdmiControlService.setIoLooper(mMyLooper);
@@ -175,16 +187,18 @@
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[2];
         hdmiPortInfos[0] =
-                new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x1000, true, false, false);
+                new HdmiPortInfo(1, HdmiPortInfo.PORT_INPUT, 0x1000, true, false, false, false);
         hdmiPortInfos[1] =
-                new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true);
+                new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true, true);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mTvPhysicalAddress = 0x0000;
+        mEarcBlocksArc = false;
         mNativeWrapper.setPhysicalAddress(mTvPhysicalAddress);
+        mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED);
         mTestLooper.dispatchAll();
         mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mTvLogicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress();
@@ -196,6 +210,20 @@
         mNativeWrapper.clearResultMessages();
     }
 
+    private static class TestCallback extends IHdmiControlCallback.Stub {
+        private final ArrayList<Integer> mCallbackResult = new ArrayList<Integer>();
+
+        @Override
+        public void onComplete(int result) {
+            mCallbackResult.add(result);
+        }
+
+        private int getResult() {
+            assertThat(mCallbackResult.size()).isEqualTo(1);
+            return mCallbackResult.get(0);
+        }
+    }
+
     @Test
     public void initialPowerStateIsStandby() {
         assertThat(mHdmiCecLocalDeviceTv.getPowerStatus()).isEqualTo(
@@ -426,9 +454,10 @@
     public void startArcAction_enable_portDoesNotSupportArc() {
         // Emulate Audio device on port 0x1000 (does not support ARC)
         mNativeWrapper.setPortConnectionStatus(1, true);
-        HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
-                ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
-        mNativeWrapper.onCecMessage(hdmiCecMessage);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
 
         mHdmiCecLocalDeviceTv.startArcAction(true);
         HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
@@ -445,9 +474,10 @@
     public void startArcAction_disable_portDoesNotSupportArc() {
         // Emulate Audio device on port 0x1000 (does not support ARC)
         mNativeWrapper.setPortConnectionStatus(1, true);
-        HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
-                ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
-        mNativeWrapper.onCecMessage(hdmiCecMessage);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
 
         mHdmiCecLocalDeviceTv.startArcAction(false);
         HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
@@ -464,9 +494,10 @@
     public void startArcAction_enable_portSupportsArc() {
         // Emulate Audio device on port 0x2000 (supports ARC)
         mNativeWrapper.setPortConnectionStatus(2, true);
-        HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
-                ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
-        mNativeWrapper.onCecMessage(hdmiCecMessage);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
         mTestLooper.dispatchAll();
 
         mHdmiCecLocalDeviceTv.startArcAction(true);
@@ -485,9 +516,10 @@
     public void startArcAction_disable_portSupportsArc() {
         // Emulate Audio device on port 0x2000 (supports ARC)
         mNativeWrapper.setPortConnectionStatus(2, true);
-        HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
-                ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
-        mNativeWrapper.onCecMessage(hdmiCecMessage);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
         mTestLooper.dispatchAll();
 
         mHdmiCecLocalDeviceTv.startArcAction(false);
@@ -522,9 +554,10 @@
     public void handleInitiateArc_portDoesNotSupportArc() {
         // Emulate Audio device on port 0x1000 (does not support ARC)
         mNativeWrapper.setPortConnectionStatus(1, true);
-        HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
-                ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
-        mNativeWrapper.onCecMessage(hdmiCecMessage);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
 
         HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildInitiateArc(
                 ADDR_AUDIO_SYSTEM,
@@ -544,9 +577,10 @@
     public void handleInitiateArc_portSupportsArc() {
         // Emulate Audio device on port 0x2000 (supports ARC)
         mNativeWrapper.setPortConnectionStatus(2, true);
-        HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
-                ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
-        mNativeWrapper.onCecMessage(hdmiCecMessage);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
         mTestLooper.dispatchAll();
 
         HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildInitiateArc(
@@ -575,6 +609,66 @@
     }
 
     @Test
+    public void handleTerminateArc_noAudioDevice() {
+        HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV);
+
+        mNativeWrapper.onCecMessage(terminateArc);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage reportArcTerminated = HdmiCecMessageBuilder.buildReportArcTerminated(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(reportArcTerminated);
+    }
+
+    @Test
+    public void handleTerminateArc_portDoesNotSupportArc() {
+        // Emulate Audio device on port 0x1000 (does not support ARC)
+        mNativeWrapper.setPortConnectionStatus(1, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+
+        HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV);
+
+        mNativeWrapper.onCecMessage(terminateArc);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage reportArcTerminated = HdmiCecMessageBuilder.buildReportArcTerminated(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(reportArcTerminated);
+    }
+
+    @Test
+    public void handleTerminateArc_portSupportsArc() {
+        // Emulate Audio device on port 0x2000 (supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV);
+
+        mNativeWrapper.onCecMessage(terminateArc);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage reportArcTerminated = HdmiCecMessageBuilder.buildReportArcTerminated(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(reportArcTerminated);
+    }
+
+    @Test
     public void supportsRecordTvScreen() {
         HdmiCecMessage recordTvScreen = HdmiCecMessage.build(ADDR_RECORDER_1, mTvLogicalAddress,
                 Constants.MESSAGE_RECORD_TV_SCREEN, HdmiCecMessage.EMPTY_PARAM);
@@ -595,9 +689,10 @@
                 HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED);
         // Emulate Audio device on port 0x1000 (does not support ARC)
         mNativeWrapper.setPortConnectionStatus(1, true);
-        HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
-                ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
-        mNativeWrapper.onCecMessage(hdmiCecMessage);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
         mTestLooper.dispatchAll();
 
         HdmiCecFeatureAction systemAudioAutoInitiationAction =
@@ -636,9 +731,9 @@
     public void hotplugDetectionAction_discoversDeviceAfterMessageReceived() {
         // Playback 1 sends a message before ACKing a poll
         mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.NACK);
-        HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildActiveSource(
+        HdmiCecMessage activeSource = HdmiCecMessageBuilder.buildActiveSource(
                 ADDR_PLAYBACK_1, ADDR_TV);
-        mNativeWrapper.onCecMessage(hdmiCecMessage);
+        mNativeWrapper.onCecMessage(activeSource);
         mTestLooper.dispatchAll();
 
         // Playback 1 begins ACKing polls, allowing detection by HotplugDetectionAction
@@ -812,12 +907,12 @@
         mTestLooper.dispatchAll();
 
         // <Feature Abort>[Not in correct mode] not sent
-        HdmiCecMessage featureAbortMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+        HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
                 ADDR_TV,
                 ADDR_PLAYBACK_1,
                 Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
                 Constants.ABORT_NOT_IN_CORRECT_MODE);
-        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(featureAbortMessage);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(featureAbort);
 
         // <Set Audio Volume Level> uses volume range [0, 100]; STREAM_MUSIC uses range [0, 25]
         verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(5), anyInt());
@@ -838,12 +933,12 @@
         mTestLooper.dispatchAll();
 
         // <Feature Abort>[Not in correct mode] sent
-        HdmiCecMessage featureAbortMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+        HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
                 ADDR_TV,
                 ADDR_PLAYBACK_1,
                 Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
                 Constants.ABORT_NOT_IN_CORRECT_MODE);
-        assertThat(mNativeWrapper.getResultMessages()).contains(featureAbortMessage);
+        assertThat(mNativeWrapper.getResultMessages()).contains(featureAbort);
 
         // AudioManager not notified of volume change
         verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(),
@@ -853,11 +948,11 @@
     @Test
     public void tvSendRequestArcTerminationOnSleep() {
         // Emulate Audio device on port 0x2000 (supports ARC)
-
         mNativeWrapper.setPortConnectionStatus(2, true);
-        HdmiCecMessage hdmiCecMessage = HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
-                ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
-        mNativeWrapper.onCecMessage(hdmiCecMessage);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
         mTestLooper.dispatchAll();
 
         mHdmiCecLocalDeviceTv.startArcAction(true);
@@ -898,4 +993,560 @@
         assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
     }
 
+    @Test
+    public void startArcAction_enable_earcBlocksArc() {
+        mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+        mTestLooper.dispatchAll();
+
+        mEarcBlocksArc = true;
+
+        // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true);
+        mTestLooper.dispatchAll();
+        HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestArcInitiation);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestArcTermination);
+    }
+
+    @Test
+    public void startArcAction_enable_earcDoesNotBlockArc() {
+        mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+        mTestLooper.dispatchAll();
+
+        mEarcBlocksArc = false;
+
+        // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true);
+        mTestLooper.dispatchAll();
+        HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestArcTermination);
+    }
+
+    @Test
+    public void startArcAction_disable_earcBlocksArc() {
+        mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+        mTestLooper.dispatchAll();
+
+        mEarcBlocksArc = true;
+
+        // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        mHdmiCecLocalDeviceTv.startArcAction(false);
+        mTestLooper.dispatchAll();
+        HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestArcInitiation);
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+    }
+
+    @Test
+    public void handleInitiateArc_earcBlocksArc() {
+        mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+        mTestLooper.dispatchAll();
+
+        mEarcBlocksArc = true;
+
+        // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildInitiateArc(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV);
+
+        mNativeWrapper.onCecMessage(requestArcInitiation);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM,
+                Constants.MESSAGE_INITIATE_ARC,
+                Constants.ABORT_NOT_IN_CORRECT_MODE);
+        assertThat(mNativeWrapper.getResultMessages()).contains(featureAbort);
+    }
+
+    @Test
+    public void handleInitiateArc_earcDoesNotBlockArc() {
+        mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+        mTestLooper.dispatchAll();
+
+        mEarcBlocksArc = false;
+
+        // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildInitiateArc(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV);
+
+        mNativeWrapper.onCecMessage(requestArcInitiation);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage reportArcInitiated = HdmiCecMessageBuilder.buildReportArcInitiated(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        // <Report ARC Initiated> should only be sent after SAD querying is done
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
+
+        // Finish querying SADs
+        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+        mNativeWrapper.clearResultMessages();
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
+    }
+
+    @Test
+    public void handleTerminateArc_earcBlocksArc() {
+        mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+        mTestLooper.dispatchAll();
+
+        mEarcBlocksArc = true;
+
+        // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV);
+
+        mNativeWrapper.onCecMessage(terminateArc);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage reportArcTerminated = HdmiCecMessageBuilder.buildReportArcTerminated(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(reportArcTerminated);
+    }
+
+    @Test
+    public void startArcAction_initiation_noAvr() {
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+    }
+
+    @Test
+    public void startArcAction_initiation_portNotConnected() {
+        // Emulate Audio device on port 0x2000 (supports ARC)
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+        // Emulate port disconnect
+        mNativeWrapper.setPortConnectionStatus(2, false);
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_INCORRECT_MODE);
+    }
+
+    @Test
+    public void startArcAction_initiation_portDoesNotSupportArc() {
+        // Emulate Audio device on port 0x1000 (Doesn´t support ARC)
+        mNativeWrapper.setPortConnectionStatus(1, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_INCORRECT_MODE);
+    }
+
+    @Test
+    public void startArcAction_initiation_indirectPhysicalAddress() {
+        // Emulate Audio device on port 0x2000 (Supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2320, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_INCORRECT_MODE);
+    }
+
+    @Test
+    public void startArcAction_initiation_earcBlocksArc() {
+        // Emulate Audio device on port 0x2000 (Supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        mEarcBlocksArc = true;
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_INCORRECT_MODE);
+    }
+
+    @Test
+    public void startArcAction_initiation_messageNotAcked() {
+        // Emulate Audio device on port 0x2000 (Supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        mNativeWrapper.setMessageSendResult(
+                Constants.MESSAGE_REQUEST_ARC_INITIATION, SendMessageResult.NACK);
+        mTestLooper.dispatchAll();
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation);
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+    }
+
+    @Test
+    public void startArcAction_initiation_timeout() {
+        // Emulate Audio device on port 0x2000 (Supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation);
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TIMEOUT);
+    }
+
+    @Test
+    public void startArcAction_initiation_featureAbort() {
+        // Emulate Audio device on port 0x2000 (Supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation);
+
+        HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV,
+                Constants.MESSAGE_REQUEST_ARC_INITIATION,
+                Constants.ABORT_NOT_IN_CORRECT_MODE);
+        mNativeWrapper.onCecMessage(featureAbort);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+    }
+
+    @Test
+    public void startArcAction_initiation_success() {
+        // Emulate Audio device on port 0x2000 (Supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(true, callback);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildRequestArcInitiation(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcInitiation);
+
+        HdmiCecMessage initiateArc = HdmiCecMessageBuilder.buildInitiateArc(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV);
+        mNativeWrapper.onCecMessage(initiateArc);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
+    }
+
+    @Test
+    public void startArcAction_termination_noAvr() {
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+    }
+
+    @Test
+    public void startArcAction_termination_portDoesNotSupportArc() {
+        // Emulate Audio device on port 0x1000 (Doesn´t support ARC)
+        mNativeWrapper.setPortConnectionStatus(1, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x1000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_INCORRECT_MODE);
+    }
+
+    @Test
+    public void startArcAction_termination_messageNotAcked() {
+        // Emulate Audio device on port 0x2000 (Supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        mNativeWrapper.setMessageSendResult(
+                Constants.MESSAGE_REQUEST_ARC_TERMINATION, SendMessageResult.NACK);
+        mTestLooper.dispatchAll();
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+    }
+
+    @Test
+    public void startArcAction_termination_timeout() {
+        // Emulate Audio device on port 0x2000 (Supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+        mTestLooper.moveTimeForward(TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TIMEOUT);
+    }
+
+    @Test
+    public void startArcAction_termination_featureAbort() {
+        // Emulate Audio device on port 0x2000 (Supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+
+        HdmiCecMessage featureAbort = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV,
+                Constants.MESSAGE_REQUEST_ARC_TERMINATION,
+                Constants.ABORT_NOT_IN_CORRECT_MODE);
+        mNativeWrapper.onCecMessage(featureAbort);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE);
+    }
+
+    @Test
+    public void startArcAction_termination_success() {
+        // Emulate Audio device on port 0x2000 (Supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        TestCallback callback = new TestCallback();
+
+        mHdmiCecLocalDeviceTv.startArcAction(false, callback);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+
+        HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV);
+        mNativeWrapper.onCecMessage(terminateArc);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.getResult()).isEqualTo(HdmiControlManager.RESULT_SUCCESS);
+    }
+
+    @Test
+    public void enableEarc_terminateArc() {
+        // Emulate Audio device on port 0x2000 (supports ARC and eARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage initiateArc = HdmiCecMessageBuilder.buildInitiateArc(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV);
+
+        mNativeWrapper.onCecMessage(initiateArc);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage reportArcInitiated = HdmiCecMessageBuilder.buildReportArcInitiated(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+        // <Report ARC Initiated> should only be sent after SAD querying is done
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
+
+        // Finish querying SADs
+        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+        mNativeWrapper.clearResultMessages();
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
+        mNativeWrapper.clearResultMessages();
+
+        mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_ENABLED);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage requestArcTermination = HdmiCecMessageBuilder.buildRequestArcTermination(
+                ADDR_TV,
+                ADDR_AUDIO_SYSTEM);
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(requestArcTermination);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 2b555a0..aa49a62 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -30,9 +30,11 @@
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.TestCase.assertEquals;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
@@ -143,6 +145,7 @@
         mHdmiControlServiceSpy.setPowerManager(mPowerManager);
         mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mHdmiControlServiceSpy.setAudioManager(mAudioManager);
+        mHdmiControlServiceSpy.setEarcSupported(true);
 
         mTestLooper.dispatchAll();
     }
@@ -1087,7 +1090,6 @@
 
     @Test
     public void disableEarc_clearEarcLocalDevice() {
-        mHdmiControlServiceSpy.setEarcSupported(true);
         mHdmiControlServiceSpy.clearEarcLocalDevice();
         mHdmiControlServiceSpy.addEarcLocalDevice(
                 new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy));
@@ -1100,7 +1102,6 @@
 
     @Test
     public void disableCec_doNotClearEarcLocalDevice() {
-        mHdmiControlServiceSpy.setEarcSupported(true);
         mHdmiControlServiceSpy.clearEarcLocalDevice();
         mHdmiControlServiceSpy.addEarcLocalDevice(
                 new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy));
@@ -1113,7 +1114,6 @@
 
     @Test
     public void enableCec_initializeCecLocalDevices() {
-        mHdmiControlServiceSpy.setEarcSupported(true);
         Mockito.clearInvocations(mHdmiControlServiceSpy);
         mHdmiControlServiceSpy.setCecEnabled(HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
         mTestLooper.dispatchAll();
@@ -1125,7 +1125,6 @@
 
     @Test
     public void enableEarc_initializeEarcLocalDevices() {
-        mHdmiControlServiceSpy.setEarcSupported(true);
         Mockito.clearInvocations(mHdmiControlServiceSpy);
         mHdmiControlServiceSpy.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED);
         mTestLooper.dispatchAll();
@@ -1137,7 +1136,6 @@
 
     @Test
     public void disableCec_DoNotInformHalAboutEarc() {
-        mHdmiControlServiceSpy.setEarcSupported(true);
         mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
                 HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
@@ -1147,7 +1145,7 @@
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
                 HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
         mTestLooper.dispatchAll();
-        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(anyBoolean());
+        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(anyBoolean(), anyBoolean());
     }
 
     @Test
@@ -1155,15 +1153,14 @@
         mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.SETTING_NAME_EARC_ENABLED,
                 HdmiControlManager.EARC_FEATURE_ENABLED);
-        mHdmiControlServiceSpy.setEarcSupported(true);
         mTestLooper.dispatchAll();
         Mockito.clearInvocations(mHdmiControlServiceSpy);
         mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.SETTING_NAME_EARC_ENABLED,
                 HdmiControlManager.EARC_FEATURE_DISABLED);
         mTestLooper.dispatchAll();
-        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false);
-        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(true);
+        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false, false);
+        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(true), anyBoolean());
     }
 
     @Test
@@ -1171,14 +1168,13 @@
         mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
                 HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
-        mHdmiControlServiceSpy.setEarcSupported(true);
         mTestLooper.dispatchAll();
         Mockito.clearInvocations(mHdmiControlServiceSpy);
         mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
                 HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
         mTestLooper.dispatchAll();
-        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(anyBoolean());
+        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(anyBoolean(), anyBoolean());
     }
 
     @Test
@@ -1186,15 +1182,14 @@
         mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.SETTING_NAME_EARC_ENABLED,
                 HdmiControlManager.EARC_FEATURE_DISABLED);
-        mHdmiControlServiceSpy.setEarcSupported(true);
         mTestLooper.dispatchAll();
         Mockito.clearInvocations(mHdmiControlServiceSpy);
         mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.SETTING_NAME_EARC_ENABLED,
                 HdmiControlManager.EARC_FEATURE_ENABLED);
         mTestLooper.dispatchAll();
-        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true);
-        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(false);
+        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true, true);
+        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(false), anyBoolean());
     }
 
     @Test
@@ -1202,13 +1197,15 @@
         mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.SETTING_NAME_EARC_ENABLED,
                 HdmiControlManager.EARC_FEATURE_ENABLED);
-        mHdmiControlServiceSpy.setEarcSupported(true);
+        mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED,
+                HdmiControlManager.HDMI_CEC_CONTROL_DISABLED);
         mTestLooper.dispatchAll();
         Mockito.clearInvocations(mHdmiControlServiceSpy);
         mHdmiControlServiceSpy.initService();
         mTestLooper.dispatchAll();
-        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true);
-        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(false);
+        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true, false);
+        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(false), anyBoolean());
     }
 
     @Test
@@ -1216,13 +1213,12 @@
         mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.SETTING_NAME_EARC_ENABLED,
                 HdmiControlManager.EARC_FEATURE_DISABLED);
-        mHdmiControlServiceSpy.setEarcSupported(true);
         mTestLooper.dispatchAll();
         Mockito.clearInvocations(mHdmiControlServiceSpy);
         mHdmiControlServiceSpy.initService();
         mTestLooper.dispatchAll();
-        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false);
-        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(true);
+        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false, false);
+        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(true), anyBoolean());
     }
 
     @Test
@@ -1230,13 +1226,12 @@
         mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.SETTING_NAME_EARC_ENABLED,
                 HdmiControlManager.EARC_FEATURE_ENABLED);
-        mHdmiControlServiceSpy.setEarcSupported(true);
         mTestLooper.dispatchAll();
         Mockito.clearInvocations(mHdmiControlServiceSpy);
         mHdmiControlServiceSpy.onWakeUp(WAKE_UP_SCREEN_ON);
         mTestLooper.dispatchAll();
-        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true);
-        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(false);
+        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true, false);
+        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(false), anyBoolean());
     }
 
     @Test
@@ -1244,13 +1239,106 @@
         mHdmiControlServiceSpy.getHdmiCecConfig().setIntValue(
                 HdmiControlManager.SETTING_NAME_EARC_ENABLED,
                 HdmiControlManager.EARC_FEATURE_DISABLED);
-        mHdmiControlServiceSpy.setEarcSupported(true);
         mTestLooper.dispatchAll();
         Mockito.clearInvocations(mHdmiControlServiceSpy);
         mHdmiControlServiceSpy.onWakeUp(WAKE_UP_SCREEN_ON);
         mTestLooper.dispatchAll();
-        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false);
-        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(true);
+        verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false, false);
+        verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(true), anyBoolean());
+    }
+
+    @Test
+    public void earcIdle_blocksArcConnection() {
+        mHdmiControlServiceSpy.clearEarcLocalDevice();
+        HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_IDLE);
+        mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+        assertThat(mHdmiControlServiceSpy.earcBlocksArcConnection()).isTrue();
+    }
+
+    @Test
+    public void earcPending_blocksArcConnection() {
+        mHdmiControlServiceSpy.clearEarcLocalDevice();
+        HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_PENDING);
+        mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+        assertThat(mHdmiControlServiceSpy.earcBlocksArcConnection()).isTrue();
+    }
+
+    @Test
+    public void earcEnabled_blocksArcConnection() {
+        mHdmiControlServiceSpy.clearEarcLocalDevice();
+        HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_CONNECTED);
+        mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+        assertThat(mHdmiControlServiceSpy.earcBlocksArcConnection()).isTrue();
+    }
+
+    @Test
+    public void arcPending_doesNotBlockArcConnection() {
+        mHdmiControlServiceSpy.clearEarcLocalDevice();
+        HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_ARC_PENDING);
+        mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+        assertThat(mHdmiControlServiceSpy.earcBlocksArcConnection()).isFalse();
+    }
+
+    @Test
+    public void earcStatusBecomesIdle_terminateArc() {
+        mHdmiControlServiceSpy.clearEarcLocalDevice();
+        HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+        mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_IDLE);
+        verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(false), any());
+    }
+
+    @Test
+    public void earcStatusBecomesEnabled_doNothing() {
+        mHdmiControlServiceSpy.clearEarcLocalDevice();
+        HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+        mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_CONNECTED);
+        verify(mHdmiControlServiceSpy, times(0)).startArcAction(anyBoolean(), any());
+    }
+
+    @Test
+    public void earcStatusBecomesPending_doNothing() {
+        mHdmiControlServiceSpy.clearEarcLocalDevice();
+        HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+        mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_PENDING);
+        verify(mHdmiControlServiceSpy, times(0)).startArcAction(anyBoolean(), any());
+    }
+
+    @Test
+    public void earcStatusBecomesNotEnabled_initiateArc() {
+        mHdmiControlServiceSpy.clearEarcLocalDevice();
+        HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+        mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_ARC_PENDING);
+        verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(true), any());
+    }
+
+    @Test
+    public void earcStateWasArcPending_becomesEarcPending_terminateArc() {
+        mHdmiControlServiceSpy.clearEarcLocalDevice();
+        HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+        mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_ARC_PENDING);
+        mTestLooper.dispatchAll();
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_PENDING);
+        verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(false), any());
+    }
+
+    @Test
+    public void earcStateWasArcPending_becomesEarcEnabled_terminateArc() {
+        mHdmiControlServiceSpy.clearEarcLocalDevice();
+        HdmiEarcLocalDeviceTx localDeviceTx = new HdmiEarcLocalDeviceTx(mHdmiControlServiceSpy);
+        mHdmiControlServiceSpy.addEarcLocalDevice(localDeviceTx);
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_ARC_PENDING);
+        mTestLooper.dispatchAll();
+        localDeviceTx.handleEarcStateChange(Constants.HDMI_EARC_STATUS_EARC_CONNECTED);
+        verify(mHdmiControlServiceSpy, times(1)).startArcAction(eq(false), any());
     }
 
     protected static class MockPlaybackDevice extends HdmiCecLocalDevicePlayback {
diff --git a/services/tests/servicestests/src/com/android/server/job/JobSetTest.java b/services/tests/servicestests/src/com/android/server/job/JobSetTest.java
index 62cc111..baa5421 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobSetTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobSetTest.java
@@ -79,7 +79,7 @@
                 .setRequiresCharging(true)
                 .build();
         return JobStatus.createFromJobInfo(jobInfo, callingUid, mContext.getPackageName(),
-                mContext.getUserId(), "Test");
+                mContext.getUserId(), "Namespace", "Test");
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index 0589b3a..d90f53a 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -135,8 +135,8 @@
                 .build();
         final int uid1 = SOME_UID;
         final int uid2 = uid1 + 1;
-        final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null);
-        final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null);
+        final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null, null);
+        final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null, null);
         runWritingJobsToDisk(JobStatus1, JobStatus2);
 
         // Remove 1 job
@@ -188,8 +188,8 @@
                 .build();
         final int uid1 = SOME_UID;
         final int uid2 = uid1 + 1;
-        final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null);
-        final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null);
+        final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null, null);
+        final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null, null);
         runWritingJobsToDisk(JobStatus1, JobStatus2);
 
         // Remove all jobs
@@ -265,7 +265,7 @@
                 .setMinimumLatency(runFromMillis)
                 .setPersisted(true)
                 .build();
-        final JobStatus ts = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null);
+        final JobStatus ts = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null, null);
         ts.addInternalFlags(JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION);
         mTaskStoreUnderTest.add(ts);
         waitForPendingIo();
@@ -308,8 +308,10 @@
                 .build();
         final int uid1 = SOME_UID;
         final int uid2 = uid1 + 1;
-        final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null);
-        final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null);
+        final JobStatus taskStatus1 =
+                JobStatus.createFromJobInfo(task1, uid1, null, -1, null, null);
+        final JobStatus taskStatus2 =
+                JobStatus.createFromJobInfo(task2, uid2, null, -1, null, null);
 
         runWritingJobsToDisk(taskStatus1, taskStatus2);
     }
@@ -364,7 +366,7 @@
         extras.putInt("into", 3);
         b.setExtras(extras);
         final JobInfo task = b.build();
-        JobStatus taskStatus = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null);
+        JobStatus taskStatus = JobStatus.createFromJobInfo(task, SOME_UID, null, -1, null, null);
 
         mTaskStoreUnderTest.add(taskStatus);
         waitForPendingIo();
@@ -384,7 +386,7 @@
                 .setRequiresCharging(true)
                 .setPersisted(true);
         JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID,
-                "com.google.android.gms", 0, null);
+                "com.android.test.app", 0, null, null);
 
         mTaskStoreUnderTest.add(taskStatus);
         waitForPendingIo();
@@ -406,7 +408,8 @@
                 .setPeriodic(5*60*60*1000, 1*60*60*1000)
                 .setRequiresCharging(true)
                 .setPersisted(true);
-        JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+        JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
+                SOME_UID, null, -1, null, null);
 
         mTaskStoreUnderTest.add(taskStatus);
         waitForPendingIo();
@@ -435,7 +438,7 @@
                 invalidLateRuntimeElapsedMillis - TWO_HOURS;  // Early is (late - period).
         final Pair<Long, Long> persistedExecutionTimesUTC = new Pair<>(rtcNow, rtcNow + ONE_HOUR);
         final JobStatus js = new JobStatus(b.build(), SOME_UID, "somePackage",
-                0 /* sourceUserId */, 0, "someTag",
+                0 /* sourceUserId */, 0, "someNamespace", "someTag",
                 invalidEarlyRuntimeElapsedMillis, invalidLateRuntimeElapsedMillis,
                 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */,
                 persistedExecutionTimesUTC, 0 /* innerFlag */, 0 /* dynamicConstraints */);
@@ -464,7 +467,7 @@
                 .setOverrideDeadline(5000)
                 .setBias(42)
                 .setPersisted(true);
-        final JobStatus js = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+        final JobStatus js = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null, null);
         mTaskStoreUnderTest.add(js);
         waitForPendingIo();
 
@@ -475,13 +478,30 @@
     }
 
     @Test
+    public void testNamespacePersisted() throws Exception {
+        final String namespace = "my.test.namespace";
+        JobInfo.Builder b = new Builder(93, mComponent)
+                .setRequiresBatteryNotLow(true)
+                .setPersisted(true);
+        final JobStatus js =
+                JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, namespace, null);
+        mTaskStoreUnderTest.add(js);
+        waitForPendingIo();
+
+        final JobSet jobStatusSet = new JobSet();
+        mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+        JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
+        assertEquals("Namespace not correctly persisted.", namespace, loaded.getNamespace());
+    }
+
+    @Test
     public void testPriorityPersisted() throws Exception {
         final JobInfo job = new Builder(92, mComponent)
                 .setOverrideDeadline(5000)
                 .setPriority(JobInfo.PRIORITY_MIN)
                 .setPersisted(true)
                 .build();
-        final JobStatus js = JobStatus.createFromJobInfo(job, SOME_UID, null, -1, null);
+        final JobStatus js = JobStatus.createFromJobInfo(job, SOME_UID, null, -1, null, null);
         mTaskStoreUnderTest.add(js);
         waitForPendingIo();
 
@@ -500,12 +520,14 @@
         JobInfo.Builder b = new Builder(42, mComponent)
                 .setOverrideDeadline(10000)
                 .setPersisted(false);
-        JobStatus jsNonPersisted = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+        JobStatus jsNonPersisted = JobStatus.createFromJobInfo(b.build(),
+                SOME_UID, null, -1, null, null);
         mTaskStoreUnderTest.add(jsNonPersisted);
         b = new Builder(43, mComponent)
                 .setOverrideDeadline(10000)
                 .setPersisted(true);
-        JobStatus jsPersisted = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+        JobStatus jsPersisted = JobStatus.createFromJobInfo(b.build(),
+                SOME_UID, null, -1, null, null);
         mTaskStoreUnderTest.add(jsPersisted);
         waitForPendingIo();
 
@@ -593,7 +615,8 @@
         JobInfo.Builder b = new Builder(8, mComponent)
                 .setRequiresDeviceIdle(true)
                 .setPersisted(true);
-        JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+        JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
+                SOME_UID, null, -1, null, null);
 
         mTaskStoreUnderTest.add(taskStatus);
         waitForPendingIo();
@@ -612,7 +635,8 @@
         JobInfo.Builder b = new Builder(8, mComponent)
                 .setRequiresCharging(true)
                 .setPersisted(true);
-        JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+        JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
+                SOME_UID, null, -1, null, null);
 
         mTaskStoreUnderTest.add(taskStatus);
         waitForPendingIo();
@@ -631,7 +655,8 @@
         JobInfo.Builder b = new Builder(8, mComponent)
                 .setRequiresStorageNotLow(true)
                 .setPersisted(true);
-        JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+        JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
+                SOME_UID, null, -1, null, null);
 
         mTaskStoreUnderTest.add(taskStatus);
         waitForPendingIo();
@@ -650,7 +675,8 @@
         JobInfo.Builder b = new Builder(8, mComponent)
                 .setRequiresBatteryNotLow(true)
                 .setPersisted(true);
-        JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(), SOME_UID, null, -1, null);
+        JobStatus taskStatus = JobStatus.createFromJobInfo(b.build(),
+                SOME_UID, null, -1, null, null);
 
         mTaskStoreUnderTest.add(taskStatus);
         waitForPendingIo();
@@ -670,7 +696,7 @@
      */
     private void assertPersistedEquals(JobInfo firstInfo) throws Exception {
         mTaskStoreUnderTest.clear();
-        JobStatus first = JobStatus.createFromJobInfo(firstInfo, SOME_UID, null, -1, null);
+        JobStatus first = JobStatus.createFromJobInfo(firstInfo, SOME_UID, null, -1, null, null);
         mTaskStoreUnderTest.add(first);
         waitForPendingIo();
 
@@ -693,6 +719,8 @@
         assertEquals("Calling UID not equal", expected.getUid(), actual.getUid());
         assertEquals("Calling user not equal", expected.getUserId(), actual.getUserId());
 
+        assertEquals(expected.getNamespace(), actual.getNamespace());
+
         assertEquals("Internal flags not equal",
                 expected.getInternalFlags(), actual.getInternalFlags());
 
diff --git a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
index b7faf22..3268df2 100644
--- a/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/PendingJobQueueTest.java
@@ -28,7 +28,7 @@
 import android.platform.test.annotations.LargeTest;
 import android.util.ArraySet;
 import android.util.Log;
-import android.util.SparseArray;
+import android.util.SparseArrayMap;
 import android.util.SparseBooleanArray;
 import android.util.SparseLongArray;
 
@@ -54,8 +54,13 @@
 
     private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder,
             int callingUid) {
+        return createJobStatus(testTag, jobInfoBuilder, callingUid, "PJQTest");
+    }
+
+    private JobStatus createJobStatus(String testTag, JobInfo.Builder jobInfoBuilder,
+            int callingUid, String namespace) {
         return JobStatus.createFromJobInfo(
-                jobInfoBuilder.build(), callingUid, "com.android.test", 0, testTag);
+                jobInfoBuilder.build(), callingUid, "com.android.test", 0, namespace, testTag);
     }
 
     @Test
@@ -373,12 +378,12 @@
         jobQueue.add(rC10);
         jobQueue.add(eC11);
 
-        checkPendingJobInvariants(jobQueue);
         JobStatus job;
         final JobStatus[] expectedPureOrder = new JobStatus[]{
                 eC3, rD4, eE5, eB6, rB2, eA7, rA1, rH8, eF9, rF8, eC11, rC10, rG12, rG13, eE14};
         int idx = 0;
         jobQueue.setOptimizeIteration(false);
+        checkPendingJobInvariants(jobQueue);
         jobQueue.resetIterator();
         while ((job = jobQueue.next()) != null) {
             assertEquals("List wasn't correctly sorted @ index " + idx,
@@ -390,6 +395,93 @@
                 eC3, eC11, rD4, eE5, eE14, eB6, rB2, eA7, rA1, rH8, eF9, rF8, rC10, rG12, rG13};
         idx = 0;
         jobQueue.setOptimizeIteration(true);
+        checkPendingJobInvariants(jobQueue);
+        jobQueue.resetIterator();
+        while ((job = jobQueue.next()) != null) {
+            assertEquals("Optimized list wasn't correctly sorted @ index " + idx,
+                    expectedOptimizedOrder[idx].getJobId(), job.getJobId());
+            idx++;
+        }
+    }
+
+    @Test
+    public void testPendingJobSorting_namespacing() {
+        PendingJobQueue jobQueue = new PendingJobQueue();
+
+        // First letter in job variable name indicate regular (r) or expedited (e).
+        // Capital letters in job variable name indicate the app/UID.
+        // Third letter (x, y, z) indicates the namespace.
+        // Numbers in job variable name indicate the enqueue time.
+        // Expected sort order:
+        //   eCx3 > rDx4 > eBy6 > rBy2 > eAy7 > rAx1 > eCy8 > rEz9 > rEz5
+        // Intentions:
+        //   * A jobs test expedited is before regular, regardless of namespace
+        //   * B jobs test expedited is before regular, in the same namespace
+        //   * C jobs test sorting by priority with different namespaces
+        //   * E jobs test sorting by priority in the same namespace
+        final String namespaceX = null;
+        final String namespaceY = "y";
+        final String namespaceZ = "z";
+        JobStatus rAx1 = createJobStatus("testPendingJobSorting",
+                createJobInfo(1), 1, namespaceX);
+        JobStatus rBy2 = createJobStatus("testPendingJobSorting",
+                createJobInfo(2), 2, namespaceY);
+        JobStatus eCx3 = createJobStatus("testPendingJobSorting",
+                createJobInfo(3).setExpedited(true).setPriority(JobInfo.PRIORITY_HIGH),
+                3, namespaceX);
+        JobStatus rDx4 = createJobStatus("testPendingJobSorting",
+                createJobInfo(4), 4, namespaceX);
+        JobStatus rEz5 = createJobStatus("testPendingJobSorting",
+                createJobInfo(5).setPriority(JobInfo.PRIORITY_LOW), 5, namespaceZ);
+        JobStatus eBy6 = createJobStatus("testPendingJobSorting",
+                createJobInfo(6).setExpedited(true), 2, namespaceY);
+        JobStatus eAy7 = createJobStatus("testPendingJobSorting",
+                createJobInfo(7).setExpedited(true), 1, namespaceY);
+        JobStatus eCy8 = createJobStatus("testPendingJobSorting",
+                createJobInfo(8).setExpedited(true).setPriority(JobInfo.PRIORITY_MAX),
+                3, namespaceY);
+        JobStatus rEz9 = createJobStatus("testPendingJobSorting",
+                createJobInfo(9).setPriority(JobInfo.PRIORITY_HIGH), 5, namespaceZ);
+
+        rAx1.enqueueTime = 10;
+        rBy2.enqueueTime = 20;
+        eCx3.enqueueTime = 30;
+        rDx4.enqueueTime = 40;
+        rEz5.enqueueTime = 50;
+        eBy6.enqueueTime = 60;
+        eAy7.enqueueTime = 70;
+        eCy8.enqueueTime = 80;
+        rEz9.enqueueTime = 90;
+
+        // Add in random order so sorting is apparent.
+        jobQueue.add(rEz9);
+        jobQueue.add(eCy8);
+        jobQueue.add(rDx4);
+        jobQueue.add(rEz5);
+        jobQueue.add(rBy2);
+        jobQueue.add(rAx1);
+        jobQueue.add(eCx3);
+        jobQueue.add(eBy6);
+        jobQueue.add(eAy7);
+
+        JobStatus job;
+        final JobStatus[] expectedPureOrder = new JobStatus[]{
+                eCx3, rDx4, eBy6, rBy2, eAy7, rAx1, eCy8, rEz9, rEz5};
+        int idx = 0;
+        jobQueue.setOptimizeIteration(false);
+        checkPendingJobInvariants(jobQueue);
+        jobQueue.resetIterator();
+        while ((job = jobQueue.next()) != null) {
+            assertEquals("List wasn't correctly sorted @ index " + idx,
+                    expectedPureOrder[idx].getJobId(), job.getJobId());
+            idx++;
+        }
+
+        final JobStatus[] expectedOptimizedOrder = new JobStatus[]{
+                eCx3, eCy8, rDx4, eBy6, rBy2, eAy7, rAx1, rEz9, rEz5};
+        idx = 0;
+        jobQueue.setOptimizeIteration(true);
+        checkPendingJobInvariants(jobQueue);
         jobQueue.resetIterator();
         while ((job = jobQueue.next()) != null) {
             assertEquals("Optimized list wasn't correctly sorted @ index " + idx,
@@ -414,6 +506,22 @@
     }
 
     @Test
+    public void testPendingJobSorting_Random_namespacing() {
+        PendingJobQueue jobQueue = new PendingJobQueue();
+        Random random = new Random(1); // Always use the same series of pseudo random values.
+
+        for (int i = 0; i < 5000; ++i) {
+            JobStatus job = createJobStatus("testPendingJobSorting_Random",
+                    createJobInfo(i).setExpedited(random.nextBoolean()), random.nextInt(250),
+                    "namespace" + random.nextInt(5));
+            job.enqueueTime = random.nextInt(1_000_000);
+            jobQueue.add(job);
+        }
+
+        checkPendingJobInvariants(jobQueue);
+    }
+
+    @Test
     public void testPendingJobSortingTransitivity() {
         PendingJobQueue jobQueue = new PendingJobQueue();
         // Always use the same series of pseudo random values.
@@ -546,10 +654,11 @@
 
     private void checkPendingJobInvariants(PendingJobQueue jobQueue) {
         final SparseBooleanArray regJobSeen = new SparseBooleanArray();
-        // Latest priority enqueue times seen for each priority for each app.
-        final SparseArray<SparseLongArray> latestPriorityRegEnqueueTimesPerUid =
-                new SparseArray<>();
-        final SparseArray<SparseLongArray> latestPriorityEjEnqueueTimesPerUid = new SparseArray<>();
+        // Latest priority enqueue times seen for each priority+namespace for each app.
+        final SparseArrayMap<String, SparseLongArray> latestPriorityRegEnqueueTimesPerUid =
+                new SparseArrayMap();
+        final SparseArrayMap<String, SparseLongArray> latestPriorityEjEnqueueTimesPerUid =
+                new SparseArrayMap<>();
         final int noEntry = -1;
         int prevOverrideState = noEntry;
 
@@ -579,11 +688,12 @@
             }
 
             final int priority = job.getEffectivePriority();
-            final SparseArray<SparseLongArray> latestPriorityEnqueueTimesPerUid =
+            final SparseArrayMap<String, SparseLongArray> latestPriorityEnqueueTimesPerUid =
                     job.isRequestedExpeditedJob()
                             ? latestPriorityEjEnqueueTimesPerUid
                             : latestPriorityRegEnqueueTimesPerUid;
-            SparseLongArray latestPriorityEnqueueTimes = latestPriorityEnqueueTimesPerUid.get(uid);
+            SparseLongArray latestPriorityEnqueueTimes =
+                    latestPriorityEnqueueTimesPerUid.get(uid, job.getNamespace());
             if (latestPriorityEnqueueTimes != null) {
                 // Invariant 2
                 for (int p = priority - 1; p >= JobInfo.PRIORITY_MIN; --p) {
@@ -603,7 +713,8 @@
                 }
             } else {
                 latestPriorityEnqueueTimes = new SparseLongArray();
-                latestPriorityEnqueueTimesPerUid.put(uid, latestPriorityEnqueueTimes);
+                latestPriorityEnqueueTimesPerUid.add(
+                        uid, job.getNamespace(), latestPriorityEnqueueTimes);
             }
             latestPriorityEnqueueTimes.put(priority, job.enqueueTime);
 
@@ -618,7 +729,7 @@
     }
 
     private static String testJobToString(JobStatus job) {
-        return "testJob " + job.getSourceUid() + "/" + job.getJobId()
+        return "testJob " + job.getSourceUid() + "/" + job.getNamespace() + "/" + job.getJobId()
                 + "/o" + job.overrideState
                 + "/p" + job.getEffectivePriority()
                 + "/b" + job.lastEvaluatedBias
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
index d3b647d..f0f0632 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
@@ -154,7 +154,7 @@
                 storageManager, spManager, gsiService, recoverableKeyStoreManager,
                 userManagerInternal, deviceStateCache));
         mGateKeeperService = gatekeeper;
-        mAuthSecretServiceAidl = authSecretService;
+        mAuthSecretService = authSecretService;
     }
 
     @Override
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
index 95d0e15..03d5b17 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java
@@ -389,11 +389,11 @@
 
     @Test
     public void testPersistentData_serializeUnserialize() {
-        byte[] serialized = PersistentData.toBytes(PersistentData.TYPE_SP, SOME_USER_ID,
+        byte[] serialized = PersistentData.toBytes(PersistentData.TYPE_SP_GATEKEEPER, SOME_USER_ID,
                 DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, PAYLOAD);
         PersistentData deserialized = PersistentData.fromBytes(serialized);
 
-        assertEquals(PersistentData.TYPE_SP, deserialized.type);
+        assertEquals(PersistentData.TYPE_SP_GATEKEEPER, deserialized.type);
         assertEquals(DevicePolicyManager.PASSWORD_QUALITY_COMPLEX, deserialized.qualityForUi);
         assertArrayEquals(PAYLOAD, deserialized.payload);
     }
@@ -424,13 +424,13 @@
         // the wire format in the future.
         byte[] serializedVersion1 = new byte[] {
                 1, /* PersistentData.VERSION_1 */
-                1, /* PersistentData.TYPE_SP */
+                1, /* PersistentData.TYPE_SP_GATEKEEPER */
                 0x00, 0x00, 0x04, 0x0A,  /* SOME_USER_ID */
                 0x00, 0x03, 0x00, 0x00,  /* PASSWORD_NUMERIC_COMPLEX */
                 1, 2, -1, -2, 33, /* PAYLOAD */
         };
         PersistentData deserialized = PersistentData.fromBytes(serializedVersion1);
-        assertEquals(PersistentData.TYPE_SP, deserialized.type);
+        assertEquals(PersistentData.TYPE_SP_GATEKEEPER, deserialized.type);
         assertEquals(SOME_USER_ID, deserialized.userId);
         assertEquals(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX,
                 deserialized.qualityForUi);
@@ -438,7 +438,7 @@
 
         // Make sure the constants we use on the wire do not change.
         assertEquals(0, PersistentData.TYPE_NONE);
-        assertEquals(1, PersistentData.TYPE_SP);
+        assertEquals(1, PersistentData.TYPE_SP_GATEKEEPER);
         assertEquals(2, PersistentData.TYPE_SP_WEAVER);
     }
 
diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
index fdf69430..f90eabc 100644
--- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
+++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java
@@ -18,6 +18,7 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 
 import android.Manifest;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
 import android.content.Context;
@@ -822,4 +823,35 @@
         }
         return Integer.MAX_VALUE;
     }
+
+    /**
+     * Check if calling user is associated with the given subscription.
+     * @param context Context
+     * @param subId subscription ID
+     * @param callerUserHandle caller user handle
+     * @return  false if user is not associated with the subscription.
+     */
+    public static boolean checkSubscriptionAssociatedWithUser(@NonNull Context context, int subId,
+            @NonNull UserHandle callerUserHandle) {
+        if (!SubscriptionManager.isValidSubscriptionId(subId)) {
+            // No subscription on device, return true.
+            return true;
+        }
+
+        SubscriptionManager subManager = context.getSystemService(SubscriptionManager.class);
+        final long token = Binder.clearCallingIdentity();
+        try {
+            if ((subManager != null) &&
+                    (!subManager.isSubscriptionAssociatedWithUser(subId, callerUserHandle))) {
+                // If subId is not associated with calling user, return false.
+                Log.e(LOG_TAG,"User[User ID:" + callerUserHandle.getIdentifier()
+                        + "] is not associated with Subscription ID:" + subId);
+                return false;
+
+            }
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+        return true;
+    }
 }
diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java
index c53b463..e6f1349 100644
--- a/telephony/java/android/telephony/SmsManager.java
+++ b/telephony/java/android/telephony/SmsManager.java
@@ -2349,6 +2349,7 @@
             RESULT_SMS_SEND_RETRY_FAILED,
             RESULT_REMOTE_EXCEPTION,
             RESULT_NO_DEFAULT_SMS_APP,
+            RESULT_USER_NOT_ALLOWED,
             RESULT_RIL_RADIO_NOT_AVAILABLE,
             RESULT_RIL_SMS_SEND_FAIL_RETRY,
             RESULT_RIL_NETWORK_REJECT,
@@ -2543,6 +2544,13 @@
      */
     public static final int RESULT_NO_DEFAULT_SMS_APP = 32;
 
+    /**
+     * User is not associated with the subscription.
+     * TODO(b/263279115): Make this error code public.
+     * @hide
+     */
+    public static final int RESULT_USER_NOT_ALLOWED = 33;
+
     // Radio Error results
 
     /**
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 4afc943..0638189 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -4418,4 +4418,69 @@
         }
         return null;
     }
+
+    /**
+     * Check if subscription and user are associated with each other.
+     *
+     * @param subscriptionId the subId of the subscription
+     * @param userHandle user handle of the user
+     * @return {@code true} if subscription is associated with user
+     * {code true} if there are no subscriptions on device
+     * else {@code false} if subscription is not associated with user.
+     *
+     * @throws IllegalArgumentException if subscription is invalid.
+     * @throws SecurityException if the caller doesn't have permissions required.
+     * @throws IllegalStateException if subscription service is not available.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
+    public boolean isSubscriptionAssociatedWithUser(int subscriptionId,
+            @NonNull UserHandle userHandle) {
+        if (!isValidSubscriptionId(subscriptionId)) {
+            throw new IllegalArgumentException("[isSubscriptionAssociatedWithUser]: "
+                    + "Invalid subscriptionId: " + subscriptionId);
+        }
+
+        try {
+            ISub iSub = TelephonyManager.getSubscriptionService();
+            if (iSub != null) {
+                return iSub.isSubscriptionAssociatedWithUser(subscriptionId, userHandle);
+            } else {
+                throw new IllegalStateException("[isSubscriptionAssociatedWithUser]: "
+                        + "subscription service unavailable");
+            }
+        } catch (RemoteException ex) {
+            ex.rethrowAsRuntimeException();
+        }
+        return false;
+    }
+
+    /**
+     * Get list of subscriptions associated with user.
+     *
+     * @param userHandle user handle of the user
+     * @return list of subscriptionInfo associated with the user.
+     *
+     * @throws SecurityException if the caller doesn't have permissions required.
+     * @throws IllegalStateException if subscription service is not available.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_SUBSCRIPTION_USER_ASSOCIATION)
+    public @NonNull List<SubscriptionInfo> getSubscriptionInfoListAssociatedWithUser(
+            @NonNull UserHandle userHandle) {
+        try {
+            ISub iSub = TelephonyManager.getSubscriptionService();
+            if (iSub != null) {
+                return iSub.getSubscriptionInfoListAssociatedWithUser(userHandle);
+            } else {
+                throw new IllegalStateException("[getSubscriptionInfoListAssociatedWithUser]: "
+                        + "subscription service unavailable");
+            }
+        } catch (RemoteException ex) {
+            ex.rethrowAsRuntimeException();
+        }
+        return new ArrayList<>();
+    }
 }
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index c5f6902..25a714a 100644
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -326,4 +326,34 @@
      * @throws IllegalArgumentException if subId is invalid.
      */
      UserHandle getSubscriptionUserHandle(int subId);
+
+     /**
+      * Check if subscription and user are associated with each other.
+      *
+      * @param subscriptionId the subId of the subscription
+      * @param userHandle user handle of the user
+      * @return {@code true} if subscription is associated with user
+      * {code true} if there are no subscriptions on device
+      * else {@code false} if subscription is not associated with user.
+      *
+      * @throws IllegalArgumentException if subscription is invalid.
+      * @throws SecurityException if the caller doesn't have permissions required.
+      * @throws IllegalStateException if subscription service is not available.
+      *
+      * @hide
+      */
+      boolean isSubscriptionAssociatedWithUser(int subscriptionId, in UserHandle userHandle);
+
+      /**
+       * Get list of subscriptions associated with user.
+       *
+       * @param userHandle user handle of the user
+       * @return list of subscriptionInfo associated with the user.
+       *
+       * @throws SecurityException if the caller doesn't have permissions required.
+       * @throws IllegalStateException if subscription service is not available.
+       *
+       * @hide
+       */
+       List<SubscriptionInfo> getSubscriptionInfoListAssociatedWithUser(in UserHandle userHandle);
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
index f9a245a..f26ce25 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
@@ -22,6 +22,8 @@
 import com.android.server.wm.flicker.traces.region.RegionSubject
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.common.IComponentNameMatcher
+import com.android.server.wm.traces.common.service.PlatformConsts
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
 
 /**
  * Checks that [ComponentNameMatcher.STATUS_BAR] window is visible and above the app windows in all
@@ -202,13 +204,15 @@
  * Asserts that the [ComponentNameMatcher.STATUS_BAR] layer is at the correct position at the start
  * of the SF trace
  */
-fun FlickerTest.statusBarLayerPositionAtStart() {
+fun FlickerTest.statusBarLayerPositionAtStart(
+    wmTrace: WindowManagerTrace? = this.reader.readWmTrace()
+) {
+    // collect navbar position for the equivalent WM state
+    val state = wmTrace?.firstOrNull() ?: error("WM state missing in $this")
+    val display = state.getDisplay(PlatformConsts.DEFAULT_DISPLAY) ?: error("Display not found")
+    val navBarPosition = WindowUtils.getExpectedStatusBarPosition(display)
     assertLayersStart {
-        val display =
-            this.entry.displays.minByOrNull { it.id }
-                ?: throw RuntimeException("There is no display!")
-        this.visibleRegion(ComponentNameMatcher.STATUS_BAR)
-            .coversExactly(WindowUtils.getStatusBarPosition(display))
+        this.visibleRegion(ComponentNameMatcher.STATUS_BAR).coversExactly(navBarPosition)
     }
 }
 
@@ -216,13 +220,15 @@
  * Asserts that the [ComponentNameMatcher.STATUS_BAR] layer is at the correct position at the end of
  * the SF trace
  */
-fun FlickerTest.statusBarLayerPositionAtEnd() {
+fun FlickerTest.statusBarLayerPositionAtEnd(
+    wmTrace: WindowManagerTrace? = this.reader.readWmTrace()
+) {
+    // collect navbar position for the equivalent WM state
+    val state = wmTrace?.lastOrNull() ?: error("WM state missing in $this")
+    val display = state.getDisplay(PlatformConsts.DEFAULT_DISPLAY) ?: error("Display not found")
+    val navBarPosition = WindowUtils.getExpectedStatusBarPosition(display)
     assertLayersEnd {
-        val display =
-            this.entry.displays.minByOrNull { it.id }
-                ?: throw RuntimeException("There is no display!")
-        this.visibleRegion(ComponentNameMatcher.STATUS_BAR)
-            .coversExactly(WindowUtils.getStatusBarPosition(display))
+        this.visibleRegion(ComponentNameMatcher.STATUS_BAR).coversExactly(navBarPosition)
     }
 }
 
diff --git a/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
index afaeca1..00fc498 100644
--- a/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
+++ b/tests/JobSchedulerPerfTests/src/com/android/frameworks/perftests/job/JobStorePerfTests.java
@@ -208,6 +208,6 @@
                 .setPersisted(true)
                 .build();
         return JobStatus.createFromJobInfo(
-                jobInfo, callingUid, SOURCE_PACKAGE, SOURCE_USER_ID, testTag);
+                jobInfo, callingUid, SOURCE_PACKAGE, SOURCE_USER_ID, null, testTag);
     }
 }